PayrollManagementSystem/backend/app/services/tplus/relationship_audit.py

136 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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