补齐源快照、资料预演、正式导入及工资核算的依赖链路。 新增版本化输入、逐段 FIFO、生成锁和结构化当月调整, 并提供默认关闭的周期采集、有限重试及 Windows 托管。 原始客户资料、私有配置及首页改动不纳入本次提交。 BREAKING CHANGE: 升级前需应用新增迁移,并通过 PAYROLL_MAKER_USER_ID 绑定实际工资制作人,否则业务只读。 旧单批次核算默认关闭;历史兼容需显式启用 PAYROLL_ALLOW_LEGACY_CALCULATION。生产启用和真实金额待验收。
148 lines
5.9 KiB
Python
148 lines
5.9 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",
|
||
"月度指导成本",
|
||
"采用人工含税指导成本;单位成本还是明细总成本、取哪个月份仍待明确",
|
||
),
|
||
(
|
||
"voucher_state",
|
||
"凭证例外核对",
|
||
"已按收款单与总账凭证的实际关联识别生成资格;无关联、作废或制单异常的记录保留待核对,不以已审核代替",
|
||
),
|
||
(
|
||
"opening_balance",
|
||
"历史承接",
|
||
"发货日期已明确取销货单日期;截至2026年2月28日的应收和已核算款待提供",
|
||
),
|
||
("allocation", "FIFO平账范围", "先发货先平账已明确;同客户全部业务还是分项目平账仍待确认"),
|
||
("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
|