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
|