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

196 lines
8.7 KiB
Python
Raw Normal View History

"""转客户更正:成对确认、原始单据保留,同一回款只归一个业务。"""
from decimal import Decimal
from sqlalchemy import select
from app.core.exceptions import BadRequestError
from app.core.tplus_lock import TPlusBusyError, sync_lock
from app.models import TPlusFact, TPlusSourceLink
from app.services.tplus.fifo import money
from app.services.tplus.snapshots import source_records
from app.services.tplus.workbench import (
WorkbenchError,
_work_source_database,
digest,
fact_out,
latest_facts,
)
def correction_fact(facts, entity, source_id):
"""实体前缀避免发货ID与收款ID相同相互覆盖;旧事实只供核对。"""
found = facts.get(("CORRECTION", f"{entity}:{source_id}")) or facts.get(
("CORRECTION", str(source_id))
)
return found if found and found.data_json.get("entity") == entity else None
def verified_correction(session, source, facts, entity, record):
fact = correction_fact(facts, entity, record.source_id)
if fact is None:
return {}
data = fact.data_json
if data.get("source_hash") != record.row_hash:
raise WorkbenchError("CORRECTION_RECONFIRM_REQUIRED")
if entity == "SHIPMENT":
if data.get("details_hash") != _details_hash(session, source, record):
raise WorkbenchError("CORRECTION_RECONFIRM_REQUIRED")
other = correction_fact(facts, entity, data.get("paired_source_id"))
if not other or (
other.data_json.get("paired_source_id") != record.source_id
or other.data_json.get("pair_key") != data["pair_key"]
or other.data_json.get("source_hash") != data.get("paired_hash")
):
raise WorkbenchError("CORRECTION_PAIR_REQUIRED")
peers = source_records(session, source, "shipments", ids=[data["paired_source_id"]])
if len(peers) != 1 or peers[0].row_hash != data["paired_hash"]:
raise WorkbenchError("CORRECTION_RECONFIRM_REQUIRED")
if other.data_json.get("details_hash") != _details_hash(session, source, peers[0]):
raise WorkbenchError("CORRECTION_RECONFIRM_REQUIRED")
neg, pos = _amount(session, source, record), _amount(session, source, peers[0])
if neg + pos != 0 or neg * pos >= 0:
raise WorkbenchError("CORRECTION_RECONFIRM_REQUIRED")
return data
def _records(session, source, entity, ids):
rows = source_records(session, source, entity, ids=ids)
if {r.source_id for r in rows} != set(ids):
raise BadRequestError("所选更正单或实际回款不在当前完整源快照中")
return {r.source_id: r for r in rows}
def _amount(session, source, head):
lines = source_records(session, source, "shipment_items", parent_ids=[head.source_id])
if not lines or any(r.payload_json.get("taxAmount") is None for r in lines):
raise BadRequestError("更正单缺少完整含税金额明细,不能成对排除")
return sum((money(r.payload_json["taxAmount"]) for r in lines), Decimal(0))
def _details_hash(session, source, head):
return digest(
[
(r.source_id, r.row_hash)
for r in source_records(session, source, "shipment_items", parent_ids=[head.source_id])
]
)
def save_pair(session, payload, actor_id):
"""沿用源级互斥锁,全部事实同事务追加;同一提交幂等。"""
source = _work_source_database(session)
try:
with sync_lock(session):
session.rollback()
facts = latest_facts(session, source)
heads = _records(
session,
source,
"shipments",
[
payload.negative_shipment_id,
payload.positive_shipment_id,
],
)
negative, positive = (
heads[payload.negative_shipment_id],
heads[payload.positive_shipment_id],
)
if negative.payload_json.get("idcustomer") == positive.payload_json.get("idcustomer"):
raise BadRequestError("转客户更正对必须分别属于原客户和新客户")
if any(h.payload_json.get("voucherState") != 189 for h in heads.values()):
raise BadRequestError("更正销货单尚未生效")
neg, pos = _amount(session, source, negative), _amount(session, source, positive)
if not (neg < 0 < pos and neg + pos == 0):
raise BadRequestError("必须选择金额相抵的一张负数及一张正数更正单")
target = facts.get(("GROUP", payload.target_group_key))
if not target or str(target.data_json["customer_id"]) != str(
negative.payload_json["idcustomer"]
):
raise BadRequestError("目标平账组必须属于负数更正单的原客户")
cash = _records(session, source, "receipts", payload.receipt_ids)
if any(
str(r.payload_json.get("idpartner")) != str(positive.payload_json["idcustomer"])
or r.payload_json.get("isReceiveFlag") != 1
or money(r.payload_json.get("amount", "0")) <= 0
for r in cash.values()
):
raise BadRequestError("实际回款须为新客户名下的正数收款单")
pair_key = digest([source, negative.source_id, positive.source_id])
proposed = []
for head, peer in ((negative, positive), (positive, negative)):
proposed.append(
(
f"SHIPMENT:{head.source_id}",
{
"entity": "SHIPMENT",
"pair_key": pair_key,
"exclude_performance": True,
"paired_source_id": peer.source_id,
"source_hash": head.row_hash,
"paired_hash": peer.row_hash,
"details_hash": _details_hash(session, source, head),
},
)
)
for row in cash.values():
proposed.append(
(
f"RECEIPT:{row.source_id}",
{
"entity": "RECEIPT",
"pair_key": pair_key,
"exclude_performance": False,
"redirect_business_key": payload.target_group_key,
"source_hash": row.row_hash,
},
)
)
result = []
for key, data in proposed:
entity, source_id = key.split(":", 1)
current = correction_fact(facts, entity, source_id)
if current and current.data_json.get("pair_key") != pair_key:
raise BadRequestError("该源单据已有其他更正配对,不能重复处理")
# 已导入单据只能重放同一事实;修改需来源差异及当月调整处理。
linked = session.scalar(
select(TPlusSourceLink).where(
TPlusSourceLink.source_database == source,
TPlusSourceLink.entity_type
== ("shipments" if entity == "SHIPMENT" else "receipts"),
TPlusSourceLink.source_id == source_id,
)
)
same = bool(current and all(current.data_json.get(k) == v for k, v in data.items()))
if linked and not same:
raise BadRequestError(
"该单据已正式导入,请核对来源差异;已生成工资的差额使用当月调整单"
)
if same:
result.append(current)
continue
fact = TPlusFact(
source_database=source,
kind="CORRECTION",
fact_key=key,
version=(current.version if current else 0) + 1,
data_json=data,
evidence=payload.evidence,
requested_by=actor_id,
)
session.add(fact)
result.append(fact)
session.flush()
session.commit()
return {"pair_key": pair_key, "facts": [fact_out(f) for f in result]}
except TPlusBusyError:
session.rollback()
raise BadRequestError("资料或任务正在更新,请稍后重试") from None
except WorkbenchError:
session.rollback()
raise BadRequestError("请先完整采集更正发货、发货明细及收款数据") from None
except Exception:
session.rollback()
raise