Files
dealerhub/backend/apps/finance/statement_html.py
T

133 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""对账单客户侧 HTML 渲染(匿名分享页,手机优先,可直接打印)。
安全:所有字段经 django.utils.html.escape;不含任何 Django 模板执行。
"""
from __future__ import annotations
from decimal import Decimal
from django.utils.html import escape
def _money(value) -> str:
try:
d = Decimal(str(value or 0))
except Exception:
return escape(str(value))
return f"{d:,.2f}"
_PAGE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>对账单 {customer_name} {date_from}~{date_to}</title>
<style>
* {{ box-sizing: border-box; }}
body {{ font-family: "Microsoft YaHei", -apple-system, sans-serif; margin: 0;
background: #f5f6f8; color: #1f2329; font-size: 14px; }}
.sheet {{ max-width: 900px; margin: 0 auto; background: #fff; padding: 20px 18px 40px; }}
h1 {{ font-size: 20px; margin: 0 0 4px; text-align: center; }}
.sub {{ text-align: center; color: #6b7280; font-size: 12px; margin-bottom: 14px; }}
.meta {{ display: flex; flex-wrap: wrap; gap: 10px 24px; margin: 12px 0 16px;
padding: 10px 12px; background: #fafbfc; border: 1px solid #e5e7eb; border-radius: 6px; }}
.meta div {{ font-size: 13px; }}
.meta b {{ color: #111827; }}
.cards {{ display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }}
.card {{ flex: 1 1 140px; border: 1px solid #e5e7eb; border-radius: 6px; padding: 10px 12px; }}
.card .k {{ color: #6b7280; font-size: 12px; }}
.card .v {{ font-size: 18px; font-weight: 700; margin-top: 4px; }}
.card.closing .v {{ color: #d4380d; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ border: 1px solid #e5e7eb; padding: 6px 8px; text-align: left; font-size: 13px; }}
th {{ background: #f3f4f6; font-weight: 600; white-space: nowrap; }}
td.num, th.num {{ text-align: right; font-variant-numeric: tabular-nums; }}
tr.debit td.type {{ color: #d4380d; }}
tr.credit td.type {{ color: #15803d; }}
.empty {{ text-align: center; color: #9ca3af; padding: 18px 0; }}
.foot {{ margin-top: 18px; color: #9ca3af; font-size: 12px; text-align: center; }}
.toolbar {{ max-width: 900px; margin: 12px auto 0; text-align: right; }}
.toolbar button {{ padding: 7px 16px; border: 1px solid #d1d5db; background: #fff;
border-radius: 6px; cursor: pointer; font-size: 13px; }}
@media print {{ body {{ background: #fff; }} .toolbar {{ display: none; }}
.sheet {{ padding: 0; max-width: none; }} }}
</style>
</head>
<body>
<div class="sheet">
<h1>对 账 单</h1>
<div class="sub">本页由系统自动生成,仅供对账参考</div>
<div class="meta">
<div>客户:<b>{customer_name}</b>{customer_code}</div>
<div>区间:<b>{date_from} ~ {date_to}</b></div>
<div>生成时间:{generated_at}</div>
</div>
<div class="cards">
<div class="card"><div class="k">期初余额(元)</div><div class="v">{opening}</div></div>
<div class="card"><div class="k">本期应收(元)</div><div class="v">{period_debit}</div></div>
<div class="card"><div class="k">本期收款(元)</div><div class="v">{period_credit}</div></div>
<div class="card closing"><div class="k">期末余额(元)</div><div class="v">{closing}</div></div>
</div>
<table>
<thead>
<tr><th>日期</th><th>类型</th><th>单号</th><th>摘要</th>
<th class="num">应收</th><th class="num">收款</th><th class="num">余额</th></tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div class="foot">如对本对账单有异议,请在收到后 3 个工作日内与业务员联系核对。</div>
</div>
<div class="toolbar"><button onclick="window.print()">打印 / 保存为 PDF</button></div>
</body>
</html>
"""
def render_statement_html(data: dict, *, generated_at: str) -> str:
"""把 customer_statement 的返回值渲染成客户侧 HTML 页面。"""
lines = data.get("lines") or []
debit_total = Decimal("0")
credit_total = Decimal("0")
rows = []
for ln in lines:
d = Decimal(str(ln.get("debit") or 0))
c = Decimal(str(ln.get("credit") or 0))
debit_total += d
credit_total += c
kind = "应收" if ln.get("type") == "receivable" else "收款"
rows.append(
'<tr class="{cls}"><td>{date}</td><td class="type">{kind}</td>'
'<td>{ref}</td><td>{desc}</td><td class="num">{debit}</td>'
'<td class="num">{credit}</td><td class="num">{balance}</td></tr>'.format(
cls="debit" if d else "credit",
date=escape(str(ln.get("date") or "")),
kind=kind,
ref=escape(str(ln.get("ref") or "")),
desc=escape(str(ln.get("description") or "")),
debit=_money(d) if d else "",
credit=_money(c) if c else "",
balance=_money(ln.get("balance")),
)
)
if not rows:
rows.append('<tr><td colspan="7" class="empty">本期无往来明细</td></tr>')
customer = data.get("customer") or {}
code = customer.get("code")
return _PAGE.format(
customer_name=escape(str(customer.get("name") or "")),
customer_code=f"({escape(str(code))})" if code else "",
date_from=escape(str(data.get("date_from") or "")),
date_to=escape(str(data.get("date_to") or "")),
generated_at=escape(generated_at),
opening=_money(data.get("opening_balance")),
period_debit=_money(debit_total),
period_credit=_money(credit_total),
closing=_money(data.get("closing_balance")),
rows="\n ".join(rows),
)