"""从源库只读整理核对资料,不把费用空值或旧初始化余额当作已确认事实。""" import hashlib import json from datetime import date, datetime from decimal import Decimal from pathlib import Path from sqlalchemy import text from app.core.exceptions import BadRequestError from app.core.month import validate_month QUERIES = ("customer", "shipments", "items", "receipts", "opening", "history", "expenses") ROOT = Path(__file__).resolve().parents[4] / "db/queries/tplus/materials" def summarize(records, customer_id, month): """空记录只构成候选余额依据,不自动替代业务确认。""" if len(records["customer"]) != 1: raise BadRequestError("源客户不存在或身份不唯一") truncated = [name for name, rows in records.items() if len(rows) > 200] missing = [ {"key": "GUIDANCE", "title": "发货月份人工指导成本", "reason": "T+成本不用于绩效,需填写并封存人工指导成本"}, {"key": "PRODUCT_CATEGORY", "title": "产品绩效核算分类", "reason": "源产品分类不等于市政水利/轨道交通核算分类"}, {"key": "HISTORICAL_PAYROLL", "title": "样本是否已在旧软件核算或发放", "reason": "源应收及收款记录不能证明绩效已发放"}, {"key": "MANAGER_HANDOVER", "title": "样本当时的分管及交接依据", "reason": "保留源业务员工号,不按当前部门推断历史经理或接手人"}, ] if not records["customer"][0].get("customerType"): missing.append({"key": "CUSTOMER_CATEGORY", "title": "客户绩效分类", "reason": "源客户绩效属性为空"}) missing.append({"key": "FREIGHT", "title": "该批发货运费是否已剔除", "reason": "费用表和金额仅作证据,缺少关联或空值不自动等于零运费"}) no_old_rows = not records["opening"] and not records["history"] return { "customer_id": customer_id, "month": month, "captured_at": datetime.now().isoformat(), "source_read_only": True, "formal_payroll_written": False, "found": {name: len(rows) for name, rows in records.items()}, "opening_candidate": "0.00" if no_old_rows else None, "opening_confirmed": False, "opening_explanation": "未查到初始化及切换日前应收记录,仅作为候选零余额依据" if no_old_rows else "需将初始化记录与截至2026-02-28的应收变动核对,不直接使用2017年期初", "truncated": truncated, "ready_for_amount": False, "missing": missing, "evidence": {name: rows[:200] for name, rows in records.items()}, } def collect_materials(source, customer_id, month): validate_month(month) if customer_id <= 0: raise BadRequestError("客户ID必须为正整数") year, mon = map(int, month.split("-")) until = date(year + (mon == 12), 1 if mon == 12 else mon + 1, 1) args = {"customer_id": customer_id, "until": until, "cutoff": date(2026, 3, 1)} records = {name: [dict(r) for r in source.execute(text((ROOT / f"{name}.sql").read_text(encoding="utf-8")), args).mappings()] for name in QUERIES} result = summarize(records, customer_id, month) normalized = json.loads(json.dumps(result, ensure_ascii=False, default=str)) normalized["evidence_hash"] = hashlib.sha256(json.dumps(normalized["evidence"], sort_keys=True, ensure_ascii=False).encode()).hexdigest() return normalized def render_markdown(report): def money(value): return "待核对" if value is None else f"{Decimal(str(value)):,.2f}" customer = report["evidence"]["customer"][0] lines = [f"# 样本资料核对:{customer['name']}({report['month']})", "", f"核对时间:{report['captured_at']}。源库只读;未写入正式工资。", "", "## 已从恢复库整理", "", "| 类别 | 数量 |", "|---|---:|"] labels = {"customer": "客户", "shipments": "发货单", "items": "发货明细", "receipts": "回款凭证关联行", "opening": "初始化应收", "history": "切换日前应收分组", "expenses": "销售费用分摊"} lines += [f"| {labels[name]} | {count} |" for name, count in report["found"].items()] lines += ["", "## 单据与归属", "", "| 发货单 | 日期 | 含税金额 | 业务员工号 |", "|---|---|---:|---|"] lines += [f"| {r['code']} | {str(r['voucherdate'])[:10]} | {money(r['taxAmount'])} | {r.get('personnel_code') or '待核对'} |" for r in report["evidence"]["shipments"]] lines += ["", "| 产品代码 | 品名/规格 | 数量 | 含税单价 | 含税金额 | 运费 |", "|---|---|---:|---:|---:|---:|"] lines += [f"| {r.get('product_code', '待核对')} | {r.get('product_name', '')} {r.get('specification', '')} | {r.get('quantity', '待核对')} | {money(r.get('taxPrice'))} | {money(r.get('taxAmount'))} | {money(r.get('expenseAmount'))} |" for r in report["evidence"]["items"]] lines += ["", "| 回款单 | 日期 | 金额 | 总账凭证 |", "|---|---|---:|---|"] lines += [f"| {r['code']} | {str(r['voucherDate'])[:10]} | {money(r['amount'])} | {r.get('accounting_voucher_code') or '待核对'} |" for r in report["evidence"]["receipts"]] lines += ["", "## 可推导但尚未确认", "", report["opening_explanation"], f"候选期初余额:{report['opening_candidate'] or '待核对'}。本报告不自动确认期初余额。", "", "## 仅需补充的资料", ""] lines += [f"- {item['title']}:{item['reason']}。" for item in report["missing"]] if report.get("customer_reply"): lines += ["", "## 本轮补充回复", "", report["customer_reply"]] if report["truncated"]: lines += ["", "存在超出200行的资料,当前核对包不完整,不能作为完整金额验收依据。"] lines += ["", "指导成本、分类或关键依据缺失,实际工资金额尚未验收,不填默认值。", "", f"证据指纹:`{report['evidence_hash']}`", ""] return "\n".join(lines)