feat: billing - add stats summary and Excel download

This commit is contained in:
yumoqing 2026-05-31 14:18:53 +08:00
parent f62e397c5a
commit 434cfe950c
4 changed files with 226 additions and 102 deletions

Binary file not shown.

View File

@ -6,7 +6,7 @@ start_date = params_kw.get('start_date', '')
end_date = params_kw.get('end_date', '') end_date = params_kw.get('end_date', '')
if not start_date or not 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 = { ns = {
'orgid': userorgid, 'orgid': userorgid,
@ -27,4 +27,22 @@ where a.orgid = ${orgid}$
and d.acc_date >= ${start_date}$ and d.acc_date >= ${start_date}$
and d.acc_date <= ${end_date}$""" and d.acc_date <= ${end_date}$"""
ret = await sor.sqlExe(sql, ns) 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) return json.dumps(ret, ensure_ascii=False, default=str)

View File

@ -1,132 +1,155 @@
{ {
"widgettype":"VBox", "widgettype": "VBox",
"id":"billing_page", "id": "billing_page",
"options":{ "options": {
"width":"100%", "width": "100%",
"height":"100%", "height": "100%",
"css":"card", "gap": "10px"
"gap":"8px"
}, },
"subwidgets":[ "subwidgets": [
{ {
"widgettype":"Form", "widgettype": "Form",
"options":{ "id": "billing_form",
"title":"账单查询", "options": {
"fields":[ "title": "账单查询",
"fields": [
{ {
"name":"start_date", "name": "start_date",
"label":"开始日期", "label": "开始日期",
"uitype":"date" "type": "date",
"required": true
}, },
{ {
"name":"end_date", "name": "end_date",
"label":"结束日期", "label": "结束日期",
"uitype":"date" "type": "date",
"required": true
} }
], ],
"buttons":[ "submit_text": "查询"
{
"name":"query",
"label":"查询",
"bgcolor":"#1890ff"
}
]
}, },
"binds":[ "binds": [
{ {
"wid":"self", "event": "submit",
"event":"submit", "target": "billing_tabular",
"actiontype":"script", "actiontype": "script",
"target":"app.billing_tabular", "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); } }; }"
"script":"this.render(params)"
} }
] ]
}, },
{ {
"widgettype":"Tabular", "widgettype": "HBox",
"id":"billing_tabular", "id": "billing_stats_box",
"options":{ "options": {
"width":"100%", "width": "100%",
"height":"100%", "height": "40px",
"css":"filler", "gap": "20px",
"data_url":"{{entire_url('/accounting/billing.dspy')}}", "align_items": "center"
"editable":false, },
"page_rows":80, "subwidgets": [
"cache_limit":3, {
"row_options":{ "widgettype": "Text",
"browserfields":{ "id": "billing_stats",
"exclouded":["row_num_"] "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", "name": "acc_date",
"title":"日期", "title": "日期",
"type":"date", "type": "date",
"uitype":"date", "uitype": "date",
"datatype":"date", "datatype": "date",
"label":"日期", "label": "日期",
"cwidth":12 "cwidth": 12
}, },
{ {
"name":"acc_timestamp", "name": "acc_timestamp",
"title":"时间", "title": "时间",
"type":"timestamp", "type": "timestamp",
"uitype":"timestamp", "uitype": "timestamp",
"datatype":"timestamp", "datatype": "timestamp",
"label":"时间", "label": "时间",
"cwidth":16 "cwidth": 16
}, },
{ {
"name":"subject_name", "name": "subject_name",
"title":"科目", "title": "科目",
"type":"str", "type": "str",
"length":50, "length": 50,
"uitype":"str", "uitype": "str",
"datatype":"str", "datatype": "str",
"label":"科目", "label": "科目",
"cwidth":14 "cwidth": 14
}, },
{ {
"name":"acc_dir", "name": "acc_dir",
"title":"方向", "title": "方向",
"type":"str", "type": "str",
"length":4, "length": 4,
"uitype":"str", "uitype": "str",
"datatype":"str", "datatype": "str",
"label":"方向", "label": "方向",
"cwidth":8 "cwidth": 8
}, },
{ {
"name":"summary", "name": "summary",
"title":"摘要", "title": "摘要",
"type":"str", "type": "str",
"length":100, "length": 100,
"uitype":"str", "uitype": "str",
"datatype":"str", "datatype": "str",
"label":"摘要", "label": "摘要",
"cwidth":30 "cwidth": 30
}, },
{ {
"name":"amount", "name": "amount",
"title":"金额", "title": "金额",
"type":"float", "type": "float",
"length":18, "length": 18,
"dec":4, "dec": 4,
"uitype":"float", "uitype": "float",
"datatype":"float", "datatype": "float",
"label":"金额", "label": "金额",
"cwidth":12 "cwidth": 12
}, },
{ {
"name":"balance", "name": "balance",
"title":"余额", "title": "余额",
"type":"float", "type": "float",
"length":18, "length": 18,
"dec":4, "dec": 4,
"uitype":"float", "uitype": "float",
"datatype":"float", "datatype": "float",
"label":"余额", "label": "余额",
"cwidth":12 "cwidth": 12
} }
] ]
} }

View File

@ -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)