diff --git a/wwwroot/.nfs0000000003108f8e00000001 b/wwwroot/.nfs0000000003108f8e00000001 deleted file mode 100644 index fa849fb..0000000 Binary files a/wwwroot/.nfs0000000003108f8e00000001 and /dev/null differ diff --git a/wwwroot/billing.dspy b/wwwroot/billing.dspy index 2d1bda4..fdfb853 100644 --- a/wwwroot/billing.dspy +++ b/wwwroot/billing.dspy @@ -6,7 +6,7 @@ start_date = params_kw.get('start_date', '') end_date = params_kw.get('end_date', '') if not start_date or not end_date: - return json.dumps({'total': 0, 'rows': []}, ensure_ascii=False, default=str) + return json.dumps({'total': 0, 'rows': [], 'stats': {'total_count': 0, 'debit_sum': 0, 'credit_sum': 0}}, ensure_ascii=False, default=str) ns = { 'orgid': userorgid, @@ -27,4 +27,22 @@ where a.orgid = ${orgid}$ and d.acc_date >= ${start_date}$ and d.acc_date <= ${end_date}$""" ret = await sor.sqlExe(sql, ns) + + # 统计数据 + stats_sql = """select + count(*) as total_count, + coalesce(sum(case when d.acc_dir = '1' then d.amount else 0 end), 0) as debit_sum, + coalesce(sum(case when d.acc_dir = '0' then d.amount else 0 end), 0) as credit_sum +from acc_detail d +join account a on d.accountid = a.id COLLATE utf8mb4_unicode_ci +where a.orgid = ${orgid}$ + and d.acc_date >= ${start_date}$ + and d.acc_date <= ${end_date}$""" + stats_recs = await sor.sqlExe(stats_sql, ns) + stats = { + 'total_count': int(stats_recs[0].total_count) if stats_recs else 0, + 'debit_sum': float(stats_recs[0].debit_sum) if stats_recs else 0, + 'credit_sum': float(stats_recs[0].credit_sum) if stats_recs else 0 + } + ret['stats'] = stats return json.dumps(ret, ensure_ascii=False, default=str) diff --git a/wwwroot/billing.ui b/wwwroot/billing.ui index 44dc899..029935a 100644 --- a/wwwroot/billing.ui +++ b/wwwroot/billing.ui @@ -1,132 +1,155 @@ { - "widgettype":"VBox", - "id":"billing_page", - "options":{ - "width":"100%", - "height":"100%", - "css":"card", - "gap":"8px" + "widgettype": "VBox", + "id": "billing_page", + "options": { + "width": "100%", + "height": "100%", + "gap": "10px" }, - "subwidgets":[ + "subwidgets": [ { - "widgettype":"Form", - "options":{ - "title":"账单查询", - "fields":[ + "widgettype": "Form", + "id": "billing_form", + "options": { + "title": "账单查询", + "fields": [ { - "name":"start_date", - "label":"开始日期", - "uitype":"date" + "name": "start_date", + "label": "开始日期", + "type": "date", + "required": true }, { - "name":"end_date", - "label":"结束日期", - "uitype":"date" + "name": "end_date", + "label": "结束日期", + "type": "date", + "required": true } ], - "buttons":[ - { - "name":"query", - "label":"查询", - "bgcolor":"#1890ff" - } - ] + "submit_text": "查询" }, - "binds":[ + "binds": [ { - "wid":"self", - "event":"submit", - "actiontype":"script", - "target":"app.billing_tabular", - "script":"this.render(params)" + "event": "submit", + "target": "billing_tabular", + "actiontype": "script", + "script": "this.render(params); const statsResp = await fetch('{{entire_url(\"/accounting/billing.dspy\")}}?start_date=' + params.start_date + '&end_date=' + params.end_date); const statsData = await statsResp.json(); const statsText = bricks.getWidgetById('billing_stats'); if (statsText && statsData.stats) { statsText.dom_element.textContent = '总条数: ' + statsData.stats.total_count + ' | 借方合计: ¥' + parseFloat(statsData.stats.debit_sum).toFixed(2) + ' | 贷方合计: ¥' + parseFloat(statsData.stats.credit_sum).toFixed(2); } const dlBtn = bricks.getWidgetById('billing_download_btn'); if (dlBtn) { dlBtn.dom_element.style.display = 'inline-block'; dlBtn.dom_element.onclick = async function() { const resp = await fetch('{{entire_url(\"/accounting/billing_download.dspy\")}}?start_date=' + params.start_date + '&end_date=' + params.end_date); const data = await resp.json(); if (data.status === 'ok') { const byteChars = atob(data.data.content); const byteNumbers = new Array(byteChars.length); for (let i = 0; i < byteChars.length; i++) { byteNumbers[i] = byteChars.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = data.data.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } }; }" } ] }, { - "widgettype":"Tabular", - "id":"billing_tabular", - "options":{ - "width":"100%", - "height":"100%", - "css":"filler", - "data_url":"{{entire_url('/accounting/billing.dspy')}}", - "editable":false, - "page_rows":80, - "cache_limit":3, - "row_options":{ - "browserfields":{ - "exclouded":["row_num_"] + "widgettype": "HBox", + "id": "billing_stats_box", + "options": { + "width": "100%", + "height": "40px", + "gap": "20px", + "align_items": "center" + }, + "subwidgets": [ + { + "widgettype": "Text", + "id": "billing_stats", + "options": { + "text": "请输入日期范围进行查询", + "css": "font-size: 14px; color: #666;" + } + }, + { + "widgettype": "Button", + "id": "billing_download_btn", + "options": { + "text": "下载Excel", + "css": "display: none; background-color: #52c41a; color: white; padding: 5px 15px; border-radius: 4px; cursor: pointer;" + } + } + ] + }, + { + "widgettype": "Tabular", + "id": "billing_tabular", + "options": { + "width": "100%", + "height": "100%", + "css": "filler", + "data_url": "{{entire_url('/accounting/billing.dspy')}}", + "editable": false, + "page_rows": 80, + "cache_limit": 3, + "row_options": { + "browserfields": { + "exclouded": ["row_num_"] }, - "fields":[ + "fields": [ { - "name":"acc_date", - "title":"日期", - "type":"date", - "uitype":"date", - "datatype":"date", - "label":"日期", - "cwidth":12 + "name": "acc_date", + "title": "日期", + "type": "date", + "uitype": "date", + "datatype": "date", + "label": "日期", + "cwidth": 12 }, { - "name":"acc_timestamp", - "title":"时间", - "type":"timestamp", - "uitype":"timestamp", - "datatype":"timestamp", - "label":"时间", - "cwidth":16 + "name": "acc_timestamp", + "title": "时间", + "type": "timestamp", + "uitype": "timestamp", + "datatype": "timestamp", + "label": "时间", + "cwidth": 16 }, { - "name":"subject_name", - "title":"科目", - "type":"str", - "length":50, - "uitype":"str", - "datatype":"str", - "label":"科目", - "cwidth":14 + "name": "subject_name", + "title": "科目", + "type": "str", + "length": 50, + "uitype": "str", + "datatype": "str", + "label": "科目", + "cwidth": 14 }, { - "name":"acc_dir", - "title":"方向", - "type":"str", - "length":4, - "uitype":"str", - "datatype":"str", - "label":"方向", - "cwidth":8 + "name": "acc_dir", + "title": "方向", + "type": "str", + "length": 4, + "uitype": "str", + "datatype": "str", + "label": "方向", + "cwidth": 8 }, { - "name":"summary", - "title":"摘要", - "type":"str", - "length":100, - "uitype":"str", - "datatype":"str", - "label":"摘要", - "cwidth":30 + "name": "summary", + "title": "摘要", + "type": "str", + "length": 100, + "uitype": "str", + "datatype": "str", + "label": "摘要", + "cwidth": 30 }, { - "name":"amount", - "title":"金额", - "type":"float", - "length":18, - "dec":4, - "uitype":"float", - "datatype":"float", - "label":"金额", - "cwidth":12 + "name": "amount", + "title": "金额", + "type": "float", + "length": 18, + "dec": 4, + "uitype": "float", + "datatype": "float", + "label": "金额", + "cwidth": 12 }, { - "name":"balance", - "title":"余额", - "type":"float", - "length":18, - "dec":4, - "uitype":"float", - "datatype":"float", - "label":"余额", - "cwidth":12 + "name": "balance", + "title": "余额", + "type": "float", + "length": 18, + "dec": 4, + "uitype": "float", + "datatype": "float", + "label": "余额", + "cwidth": 12 } ] } diff --git a/wwwroot/billing_download.dspy b/wwwroot/billing_download.dspy new file mode 100644 index 0000000..376e788 --- /dev/null +++ b/wwwroot/billing_download.dspy @@ -0,0 +1,83 @@ +import io +import base64 +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment + +userid = await get_user() +userorgid = await get_userorgid() + +start_date = params_kw.get('start_date', '') +end_date = params_kw.get('end_date', '') + +if not start_date or not end_date: + return json.dumps({'status': 'error', 'data': {'message': '缺少日期参数'}}, ensure_ascii=False) + +ns = { + 'orgid': userorgid, + 'start_date': start_date, + 'end_date': end_date +} + +async with get_sor_context(request._run_ns, 'accounting') as sor: + sql = """select d.acc_date, d.acc_timestamp, s.name as subject_name, d.acc_dir, +d.summary, d.amount, d.balance +from acc_detail d +join account a on d.accountid = a.id COLLATE utf8mb4_unicode_ci +join subject s on a.subjectid = s.id COLLATE utf8mb4_unicode_ci +where a.orgid = ${orgid}$ + and d.acc_date >= ${start_date}$ + and d.acc_date <= ${end_date}$ +order by d.acc_date desc""" + recs = await sor.sqlExe(sql, ns) + + # 生成 xlsx + wb = Workbook() + ws = wb.active + ws.title = '账单明细' + + # 表头 + headers = ['日期', '时间', '科目', '方向', '摘要', '金额', '余额'] + ws.append(headers) + header_font = Font(bold=True) + for cell in ws[1]: + cell.font = header_font + cell.alignment = Alignment(horizontal='center') + + # 数据 + for rec in recs: + direction = '借' if rec.acc_dir == '1' else '贷' + ws.append([ + str(rec.acc_date), + str(rec.acc_timestamp), + rec.subject_name, + direction, + rec.summary, + float(rec.amount), + float(rec.balance) + ]) + + # 调整列宽 + ws.column_dimensions['A'].width = 12 + ws.column_dimensions['B'].width = 20 + ws.column_dimensions['C'].width = 15 + ws.column_dimensions['D'].width = 8 + ws.column_dimensions['E'].width = 40 + ws.column_dimensions['F'].width = 15 + ws.column_dimensions['G'].width = 15 + + # 保存到内存 + output = io.BytesIO() + wb.save(output) + output.seek(0) + + # Base64 编码 + b64_data = base64.b64encode(output.read()).decode('utf-8') + filename = f'账单明细_{start_date}_{end_date}.xlsx' + + return json.dumps({ + 'status': 'ok', + 'data': { + 'filename': filename, + 'content': b64_data + } + }, ensure_ascii=False)