打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。 新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。 补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
227 lines
12 KiB
Python
227 lines
12 KiB
Python
"""来源订单与发货人工关系;仅追加结论,不修改来源或封存数据。"""
|
|
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from app.core.exceptions import BadRequestError
|
|
from app.core.tplus_lock import TPlusBusyError, sync_lock
|
|
from app.models import (
|
|
Business,
|
|
BusinessItem,
|
|
Shipment,
|
|
ShipmentItem,
|
|
TPlusBusinessDocument,
|
|
TPlusFact,
|
|
)
|
|
from app.services.audit_service import AuditRecord, audit
|
|
from app.services.tplus.workbench import digest
|
|
|
|
KIND = "SALES_RELATION"
|
|
|
|
|
|
def item_revision(item):
|
|
return digest([item.id, item.business_id, item.product_id, item.product_name, item.spec_model,
|
|
item.unit, str(item.quantity), str(item.sale_price), str(item.sale_amount)])
|
|
|
|
|
|
def document_revision(doc):
|
|
return [doc.id, doc.entity_type, doc.source_id, doc.record_id, doc.applied_record_id,
|
|
doc.fingerprint, doc.status, doc.business_id, doc.target_id]
|
|
|
|
|
|
def shipment_revision(doc, head, shipment, line):
|
|
return digest([document_revision(doc), document_revision(head), shipment.id,
|
|
shipment.business_id, shipment.ship_date.isoformat(), line.id, line.product_id,
|
|
str(line.quantity), str(line.sale_price), str(line.amount)])
|
|
|
|
|
|
def order_revision(doc, head, item):
|
|
return digest([document_revision(doc), document_revision(head), item_revision(item)])
|
|
|
|
|
|
def _head(session, doc, entity, target_id=None, *, lock=True):
|
|
parent = doc.data_json.get("source_parent_id")
|
|
query = select(TPlusBusinessDocument).where(
|
|
TPlusBusinessDocument.source_database == doc.source_database,
|
|
TPlusBusinessDocument.business_id == doc.business_id,
|
|
TPlusBusinessDocument.entity_type == entity,
|
|
TPlusBusinessDocument.status == "APPLIED",
|
|
TPlusBusinessDocument.source_id == str(parent),
|
|
)
|
|
if target_id is not None:
|
|
query = query.where(TPlusBusinessDocument.target_id == target_id)
|
|
head = session.scalar(query.with_for_update() if lock else query)
|
|
if head is None or head.record_id != head.applied_record_id:
|
|
raise BadRequestError("来源单据已变化,请刷新后核对")
|
|
return head
|
|
|
|
|
|
def _applied_document(session, document_id, business_id, entity, source, *, lock=True):
|
|
doc = session.get(TPlusBusinessDocument, document_id, with_for_update=lock)
|
|
if (doc is None or doc.business_id != business_id or doc.source_database != source
|
|
or doc.entity_type != entity or doc.status != "APPLIED"
|
|
or doc.record_id != doc.applied_record_id):
|
|
raise BadRequestError("来源明细已变化,请刷新后核对")
|
|
return doc
|
|
|
|
|
|
def _latest_business_facts(session, source, business_id):
|
|
ids = select(func.max(TPlusFact.id)).where(
|
|
TPlusFact.source_database == source, TPlusFact.kind == KIND,
|
|
TPlusFact.fact_key.like(f"sales:{business_id}:%"),
|
|
).group_by(TPlusFact.fact_key)
|
|
return list(session.scalars(select(TPlusFact).where(TPlusFact.id.in_(ids))))
|
|
|
|
|
|
def _fact_is_current(session, business, item, fact):
|
|
"""任何来源身份、版本或业务明细变化均使结论失效。"""
|
|
data = fact.data_json
|
|
try:
|
|
if (data["business_id"] != business.id or data["item_id"] != item.id
|
|
or data["item_revision"] != item_revision(item)):
|
|
return False
|
|
source = business.integration_json["source_database"]
|
|
doc = _applied_document(session, data["shipment_document_id"], business.id, "shipment_items", source, lock=False)
|
|
line = session.get(ShipmentItem, doc.target_id)
|
|
shipment = session.get(Shipment, line.shipment_id) if line else None
|
|
if (line is None or shipment is None or shipment.business_id != business.id
|
|
or line.product_id != item.product_id or line.quantity != item.quantity or line.amount != item.sale_amount):
|
|
return False
|
|
head = _head(session, doc, "shipments", shipment.id, lock=False)
|
|
if shipment_revision(doc, head, shipment, line) != data["shipment_revision"]:
|
|
return False
|
|
if data["disposition"] == "INDEPENDENT":
|
|
return data.get("order_document_id") is None and data.get("order_revision") is None
|
|
if data["disposition"] != "ORDER":
|
|
return False
|
|
order = _applied_document(session, data["order_document_id"], business.id, "business_items", source, lock=False)
|
|
order_item = session.get(BusinessItem, order.target_id)
|
|
if order_item is None or order_item.business_id != business.id or order_item.product_id != item.product_id:
|
|
return False
|
|
order_head = _head(session, order, "businesses", lock=False)
|
|
return order_revision(order, order_head, order_item) == data["order_revision"]
|
|
except (BadRequestError, KeyError, TypeError, ValueError):
|
|
return False
|
|
|
|
|
|
def resolve_items(session, business, items, order_ids):
|
|
"""返回待核对状态和可计入销售额的明细,不猜测来源关系。"""
|
|
orders = set(order_ids)
|
|
if not orders:
|
|
return False, items
|
|
extra = [item for item in items if item.id not in orders]
|
|
if not extra:
|
|
return False, items
|
|
facts = {fact.fact_key: fact for fact in _latest_business_facts(
|
|
session, business.integration_json["source_database"], business.id)}
|
|
included = [item for item in items if item.id in orders]
|
|
used = set()
|
|
for item in extra:
|
|
fact = facts.get(f"sales:{business.id}:{item.id}")
|
|
if fact is None or not _fact_is_current(session, business, item, fact):
|
|
return True, []
|
|
doc_id = fact.data_json["shipment_document_id"]
|
|
if doc_id in used:
|
|
return True, []
|
|
used.add(doc_id)
|
|
if fact.data_json["disposition"] == "INDEPENDENT":
|
|
included.append(item)
|
|
return False, included
|
|
|
|
|
|
def confirm(session, business_id, payload, operator, meta=None):
|
|
"""绑定明确选择及版本;调用者须具备业务更新和核算准备权限。"""
|
|
record = AuditRecord(module="业务管理", action="核对订单发货关系", summary=f"业务 #{business_id} 来源关系",
|
|
request_data=payload.model_dump(mode="json"))
|
|
try:
|
|
with audit(session, operator=operator, meta=meta, record=record), sync_lock(session):
|
|
business = session.get(Business, business_id, with_for_update=True)
|
|
if business is None or not business.integration_json:
|
|
raise BadRequestError("仅可核对来源业务")
|
|
source = business.integration_json["source_database"]
|
|
item = session.get(BusinessItem, payload.item_id, with_for_update=True)
|
|
if item is None or item.business_id != business_id or item_revision(item) != payload.item_revision:
|
|
raise BadRequestError("业务明细已变化,请刷新后核对")
|
|
existing_order = session.scalar(select(TPlusBusinessDocument.id).where(
|
|
TPlusBusinessDocument.business_id == business_id,
|
|
TPlusBusinessDocument.entity_type == "business_items",
|
|
TPlusBusinessDocument.target_id == item.id,
|
|
).limit(1))
|
|
if existing_order is not None:
|
|
raise BadRequestError("订单明细不能作为待核对发货明细")
|
|
doc = _applied_document(session, payload.shipment_document_id, business_id, "shipment_items", source)
|
|
line = session.get(ShipmentItem, doc.target_id, with_for_update=True)
|
|
shipment = session.get(Shipment, line.shipment_id, with_for_update=True) if line else None
|
|
if (line is None or shipment is None or shipment.business_id != business_id
|
|
or line.product_id != item.product_id or line.quantity != item.quantity or line.amount != item.sale_amount):
|
|
raise BadRequestError("发货明细与业务明细不一致,请重新核对")
|
|
head = _head(session, doc, "shipments", shipment.id)
|
|
if shipment_revision(doc, head, shipment, line) != payload.shipment_revision:
|
|
raise BadRequestError("来源发货已变化,请刷新后核对")
|
|
order = order_head = order_item = None
|
|
if payload.disposition == "ORDER":
|
|
order = _applied_document(session, payload.order_document_id, business_id, "business_items", source)
|
|
order_item = session.get(BusinessItem, order.target_id, with_for_update=True)
|
|
if order_item is None or order_item.business_id != business_id or order_item.product_id != item.product_id:
|
|
raise BadRequestError("订单明细不属于本业务或产品不一致")
|
|
order_head = _head(session, order, "businesses")
|
|
if order_revision(order, order_head, order_item) != payload.order_revision:
|
|
raise BadRequestError("来源订单已变化,请刷新后核对")
|
|
facts = _latest_business_facts(session, source, business_id)
|
|
key = f"sales:{business_id}:{item.id}"
|
|
current = next((fact for fact in facts if fact.fact_key == key), None)
|
|
if (current.version if current else 0) != payload.expected_version:
|
|
raise BadRequestError("核对结论已变化,请刷新后重试")
|
|
if any(fact.fact_key != key and fact.data_json.get("disposition") != "RELEASED"
|
|
and fact.data_json.get("shipment_document_id") == doc.id for fact in facts):
|
|
raise BadRequestError("该发货明细已被其他业务明细确认")
|
|
fact = TPlusFact(source_database=source, kind=KIND, fact_key=key,
|
|
version=payload.expected_version + 1, evidence=payload.evidence, requested_by=operator.id,
|
|
data_json={"business_id": business_id, "item_id": item.id,
|
|
"shipment_document_id": doc.id, "order_document_id": order.id if order else None,
|
|
"disposition": payload.disposition, "item_revision": payload.item_revision,
|
|
"shipment_revision": payload.shipment_revision, "order_revision": payload.order_revision})
|
|
session.add(fact)
|
|
session.flush()
|
|
from app.services.tplus.business_sync import _refresh_business_totals
|
|
|
|
_refresh_business_totals(session, business)
|
|
session.commit()
|
|
return {"item_id": item.id, "version": fact.version}
|
|
except (TPlusBusyError, IntegrityError):
|
|
session.rollback()
|
|
raise BadRequestError("来源数据或核对结论正在更新,请稍后重试") from None
|
|
|
|
|
|
def release(session, business_id, payload, operator, meta=None):
|
|
"""追加撤销记录,释放发货占用,同时保留既往核对依据。"""
|
|
record = AuditRecord(module="业务管理", action="撤销订单发货关系", summary=f"业务 #{business_id} 来源关系",
|
|
request_data=payload.model_dump(mode="json"))
|
|
try:
|
|
with audit(session, operator=operator, meta=meta, record=record), sync_lock(session):
|
|
business = session.get(Business, business_id, with_for_update=True)
|
|
if business is None or not business.integration_json:
|
|
raise BadRequestError("仅可核对来源业务")
|
|
source = business.integration_json["source_database"]
|
|
key = f"sales:{business_id}:{payload.item_id}"
|
|
current = next((fact for fact in _latest_business_facts(session, source, business_id)
|
|
if fact.fact_key == key), None)
|
|
if current is None or current.version != payload.expected_version:
|
|
raise BadRequestError("核对结论已变化,请刷新后重试")
|
|
if current.data_json.get("disposition") == "RELEASED":
|
|
raise BadRequestError("核对结论已撤销")
|
|
fact = TPlusFact(source_database=source, kind=KIND, fact_key=key,
|
|
version=current.version + 1, evidence=payload.evidence, requested_by=operator.id,
|
|
data_json={"business_id": business_id, "item_id": payload.item_id,
|
|
"disposition": "RELEASED"})
|
|
session.add(fact)
|
|
session.flush()
|
|
from app.services.tplus.business_sync import _refresh_business_totals
|
|
|
|
_refresh_business_totals(session, business)
|
|
session.commit()
|
|
return {"item_id": payload.item_id, "version": fact.version}
|
|
except (TPlusBusyError, IntegrityError):
|
|
session.rollback()
|
|
raise BadRequestError("来源数据或核对结论正在更新,请稍后重试") from None
|