From 434cfe950c45839656f3897e5832c537594ce96d Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 31 May 2026 14:18:53 +0800 Subject: [PATCH] feat: billing - add stats summary and Excel download --- wwwroot/.nfs0000000003108f8e00000001 | Bin 12288 -> 0 bytes wwwroot/billing.dspy | 20 ++- wwwroot/billing.ui | 225 +++++++++++++++------------ wwwroot/billing_download.dspy | 83 ++++++++++ 4 files changed, 226 insertions(+), 102 deletions(-) delete mode 100644 wwwroot/.nfs0000000003108f8e00000001 create mode 100644 wwwroot/billing_download.dspy diff --git a/wwwroot/.nfs0000000003108f8e00000001 b/wwwroot/.nfs0000000003108f8e00000001 deleted file mode 100644 index fa849fba1646042014621339a4c8e3587c666d70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2TWl0n7{?C~E(-$Hz6ctEhnSXVyW19VDG$VJ@PP+OB_@Py&d!|Ooie*K&dk!1 zT2T@sL5XdQQbj~D(3lWSglMdnD8>hUFa~41J}KR{iHVm)j3%i6Gka!scBfs5@d3@5 zlEom~3jtUE9{CJG%+47D9G^a!vnZkGCus>DfXG+$nOG8KSG%Wt3cx zl{DVbZ*A&fth?CZuNIq<)RV!INr&5(n`D}1l}nyc$|nZ~1{}-sl3Bwv@!VPdTeGqC zRfAGMDKLiuWiQu}UQT+tyHaArI(psO)~oKBW3Xyg3Md7X0!jg;fKosypcGIFC`KWG60i}RaKq;UUPzopolmbctrGQdEDWDWk3j7BZU^*cm zE+Ay#A~=u#{}2BC|Hn!~rolPz0XPkw2P0rVcmR|@KiCd#18v}Ha3#2~f{-u3Sug=k zgJ;0~AO|wwUa%G10&W1U;J4+3{0zPa74Rka6ub*w0*62Wbb@xU4y*yI!Jk(UavppJ z-UFw=7}yE!0A1i(umt?H46%Wazyx>|jDdZ?0C$6%!5Xj(%m=?OCFE;x7L0>~U^mzf zwt$VG6SRO|6NG#VJ_fIY=fK0j0R~`TD@cOXU@`b{2_c_@cfcv|A~*u}0|WGdEubB& z1Q!+)@*VgJoC6c!1#l4T0XeV@bc0o35x9sv{{p@N?}Jld3>*L!=mVQUJ6H==fmW~r zECq{!sFjNgkt6T}P?u6bDRB8KFiaA=NFsq-?T6nG>!^-N_`@+ccuXXyHel#pf%bNF zi&ex$O_t>*?WO16etPEkQF>$8p)-$pdu@)soM|yH(p5I*=+L=FdpFe9FnKBO+e_El z=9uako+)g_{3x-aXwNcJe5Dq8F(e%wuC3f8x|v5tFN^46e#q!pQG7{vDZ%xm!VCqe ztBj6KpL{5soS>1^xtd}m=l=6JA{E#?ibjJ$HZ(1x8#UpYMA5Y&7RJ>0=+u#y|AjDay3}mMEQZ<0_H)h4 z_>Vzmn+3Bm7EI;vYcq$>%pQzq-~&^S741lOSYfku%4E#iCAWXfUL+;)dCg*pA90n# zCo0b#i#QwEC-}8?wj^j6Mnk)dps_V{zQI7m`MN36Z<;f>sk^jyXI1y`ETeo-Gp(}D zb@Wlk8pxE&#S9h4y*RV8XmPm{%&xBH53_8~uu51_wrNZ&@Qi5`4UhKrR7-%`EYCAy z3EG>MB}Tb!cq}VU;2bkuE=R&?mMJ^p{V+7d@u{!F#bbM0^xNOyn_u0v_qMGc9+m=A z&4mGkXqr6OO0>X@d;zY|u2ky!V4!>{McQb69MpggRNBeF&$7wnk`$Bk+>?3fi*yME zs)AN!!)wwP&($2m7A|NI$})MUY318(=CGo>ezR9FTr@EKc0?YuQaRIO4oqPtv+d4V zy+Ll6CNdBlJ5^h9>ruW38e+a%&WeNt%?Lp>ax*8J{Ny3tQdk?ZGGqbBP*4bY%`zGkp>1yq@UO9GR`jz8>sRWf3d0&~=PH9;y*|O*PwX@TYcb9Z8G*-VSs+!ac zO}+U<<>;}7Ef$#LB|Q_}09A`JJ))wUuRZzJgOlTr&apO5Ajg|!^@hzwgICw|nUj-; zo{Z|!SZ#GBb*JSNefi+CNL;nEx@Y~Y&^)t|fV4d*#FtgCF-P}j%~J8AWW^svVZGPc Vudj8nRmD2>WxnH9YY?qN= ${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)