PayrollManagementSystem/backend/app/services/tplus/guidance.py
liuwenxin 1e53792ad6 feat(T+): 完成数据贯通与自动接入闭环
打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。
新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。
补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
2026-11-05 18:47:13 +08:00

88 lines
3.8 KiB
Python

"""解析已登记的月度指导成本,不按名称或单位猜测。"""
from datetime import date
from decimal import Decimal, InvalidOperation
from sqlalchemy import func, select
from app.core.exceptions import BadRequestError
from app.models import PayrollInput, TPlusFact
def latest_guidance_facts(session, source):
"""只读取指定账套每个指导成本事实键的最新版本。"""
ids = select(func.max(TPlusFact.id)).where(
TPlusFact.source_database == source, TPlusFact.kind == "GUIDANCE"
).group_by(TPlusFact.fact_key)
return {(fact.kind, fact.fact_key): fact for fact in session.scalars(select(TPlusFact).where(TPlusFact.id.in_(ids)))}
def resolve_guidance(facts, product_id, line_id, month, unit):
"""返回匹配状态和已封存事实;存在多条候选时不选择其中一条。"""
candidates = [
fact.data_json for (kind, _), fact in facts.items()
if kind == "GUIDANCE"
and fact.data_json.get("product_id") == product_id
and fact.data_json.get("month") == month
and (fact.data_json.get("basis") == "UNIT" or fact.data_json.get("shipment_line_id") == line_id)
]
if not candidates:
return "MISSING", None
if len(candidates) != 1:
return "AMBIGUOUS", None
guidance = candidates[0]
if guidance.get("unit") != unit:
return "UNIT_MISMATCH", None
if not guidance.get("is_finalized"):
return "UNFINALIZED", None
return "FINALIZED", guidance
def check_unit_guidance_conflicts(facts, product_id, month, unit, amount, frozen_shipments):
"""新增产品月度单价前检查重叠事实和全部相关的历史封存明细。"""
if any(
kind == "GUIDANCE" and fact.data_json.get("product_id") == product_id
and fact.data_json.get("month") == month
for (kind, _), fact in facts.items()
):
raise BadRequestError("指导成本存在重叠记录,请先核对")
try:
expected = Decimal(str(amount))
if not expected.is_finite() or expected < 0:
raise ValueError
for shipment in frozen_shipments:
old = Decimal(str(shipment.get("unit_cost")))
if not old.is_finite() or old < 0 or old != expected or shipment.get("unit") != unit:
raise ValueError
except (InvalidOperation, TypeError, ValueError):
raise BadRequestError("已封存发货的成本或单位与指导价不一致,请先核对") from None
def frozen_guidance_shipments(session, source, product_id, month):
"""按不可变快照中的来源产品读取历史,不能依赖当前正式明细。"""
query = (
select(PayrollInput.data_json)
.where(PayrollInput.source_database == source)
.order_by(PayrollInput.id)
.execution_options(yield_per=100)
)
for data in session.scalars(query):
shipments = data.get("shipments") if isinstance(data, dict) else None
if not isinstance(shipments, list):
raise BadRequestError("已封存发货数据异常,请先核对")
for shipment in shipments:
if not isinstance(shipment, dict) or not shipment.get("line_id"):
raise BadRequestError("已封存发货数据异常,请先核对")
try:
ship_date = date.fromisoformat(shipment["date"])
except (KeyError, TypeError, ValueError):
raise BadRequestError("已封存发货日期异常,请先核对") from None
if ship_date.strftime("%Y-%m") != month:
continue
frozen_product = shipment.get("product_id")
if frozen_product is None or str(frozen_product).strip() in ("", "None"):
raise BadRequestError("已封存发货缺少来源产品身份,请先核对")
if str(frozen_product) == str(product_id):
yield shipment