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', '')
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)

View File

@ -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
}
]
}

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)