baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""对账单客户侧 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),
|
||||
)
|
||||
Reference in New Issue
Block a user