136 lines
5.7 KiB
Python
136 lines
5.7 KiB
Python
"""读取实际系统字典并统计带单据类型的收款关联,不推断计提口径。"""
|
||
|
||
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
|