"""读取实际系统字典并统计带单据类型的收款关联,不推断计提口径。""" import re from datetime import datetime from pathlib import Path from sqlalchemy import text from sqlalchemy.orm import Session from app.core.logging import scrub from app.services.tplus.reader import TPlusReader _COLUMNS = { "source_version": ("source_version",), "voucher_types": ("id", "code", "name"), "state_enums": ("id", "code", "name", "enum_name"), "receipt_kinds": ("voucher_type_code", "voucher_type_name", "receive_flag", "row_count"), "receipt_line_kinds": ("voucher_type_id", "voucher_type_name", "receive_flag", "row_count"), "sales_links": ( "row_count", "delivery_matches", "detail_matches", "direct_order_matches", "detail_order_matches", "unambiguous_order_matches", "invalid_order_ids", "conflicting_order_ids", ), "order_coverage": ("row_count", "without_order", "single_order", "multiple_orders"), } PENDING_CONFIRMATIONS = ( ("cost_basis", "绩效成本取值", "订单最新成本或出库成本的类型、时点和含税口径需业务确认"), ("receipt_date", "到账日", "单据日、审核日和明细 arrivalDate 的业务含义需确认"), ("ship_date", "发货日", "回款天数应使用哪个发货日期需确认"), ("allocation", "多单分摊", "一笔收款涉及多张销货单或订单时,不能自动把整笔金额记给一单"), ("negative_events", "退货与红冲", "负数、冲销及已结算业务的调整方式需确认"), ) def public_report(raw: dict | None) -> dict | None: """历史报告也按当前字段白名单输出,禁止透传未知嵌套数据。""" if not isinstance(raw, dict): return None result = { key: TPlusRelationshipAudit._safe_value(raw.get(key)) for key in ( "schema_version", "captured_at", "source_version", "is_target_engine", "relationship_status", ) } result["compatibility_status"] = "PENDING" for section, columns in _COLUMNS.items(): if section in ("source_version", "voucher_types"): continue value = raw.get(section) if isinstance(value, list): result[section] = [ {key: TPlusRelationshipAudit._safe_value(row.get(key)) for key in columns} for row in value[:200] if isinstance(row, dict) ] elif isinstance(value, dict): result[section] = { key: TPlusRelationshipAudit._safe_value(value.get(key)) for key in columns } else: result[section] = None result["pending_confirmations"] = [ {"key": k, "title": t, "reason": r} for k, t, r in PENDING_CONFIRMATIONS ] return result class TPlusRelationshipAudit: """执行固定核验查询;结果只包含系统字典和数量,不包含客户记录。""" def __init__(self, session: Session, query_dir: Path | None = None) -> None: self.session = session root = query_dir or TPlusReader._default_query_dir() source = (root / "audit" / "queries.sql").read_text(encoding="utf-8") sections = re.split(r"(?m)^-- @query (\w+)\s*$", source) self.queries = dict(zip(sections[1::2], sections[2::2], strict=True)) if set(self.queries) != set(_COLUMNS): raise ValueError("关联核验查询清单不完整") def fetch(self, name: str, **params) -> list[dict]: """只接受登记的查询名与绑定参数,过滤非合同字段。""" if name not in self.queries: raise ValueError("不支持的核验查询") result = self.session.execute(text(self.queries[name]), params) try: return [ {key: self._safe_value(row.get(key)) for key in _COLUMNS[name]} for row in result.mappings().all() ] finally: result.close() @staticmethod def _safe_value(value): if value is None or isinstance(value, (int, bool)): return value return scrub(re.sub(r"[\x00-\x1f]", " ", str(value)))[:200] def run(self) -> dict: """生成事实报告;类型缺失或有歧义时不执行该关联链路。""" version = self.fetch("source_version")[0]["source_version"] types = self.fetch("voucher_types") report = { "schema_version": 1, "captured_at": datetime.now().isoformat(), "source_version": version, "is_target_engine": str(version).startswith("10.50."), "compatibility_status": "PENDING", "state_enums": self.fetch("state_enums"), "receipt_kinds": self.fetch("receipt_kinds"), "receipt_line_kinds": self.fetch("receipt_line_kinds"), "sales_links": None, "order_coverage": None, "relationship_status": "UNRESOLVED", "relationship_basis": "主表按 isReceiveFlag=1(是否收款)筛选;明细按 SM_VoucherType 的 SA04 类型关联。主表 idvouchertype 是来源类型,不能据此筛本单类型。", "pending_confirmations": [ {"key": k, "title": t, "reason": r} for k, t, r in PENDING_CONFIRMATIONS ], } sale = [r["id"] for r in types if r["code"] == "SA04"] if len(sale) == 1: params = {"sale_delivery_type_id": sale[0], "receive_flag": 1} report["sales_links"] = self.fetch("sales_links", **params)[0] report["order_coverage"] = self.fetch("order_coverage", **params)[0] report["relationship_status"] = "OBSERVED" return report