baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+132
View File
@@ -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),
)