176 lines
6.0 KiB
Python
176 lines
6.0 KiB
Python
# 接口 3 整批校验与落表。不跑合同作业。
|
||
#
|
||
# 入参 camelCase,落表 snake_case。幂等键是对方 fid。
|
||
# 请求里不录合同。
|
||
|
||
from __future__ import annotations
|
||
|
||
from . import messages
|
||
from .parse import parse_date, parse_decimal, parse_fid, parse_int, parse_oms_id, parse_str
|
||
|
||
_STRING_FIELDS = (
|
||
("supplierId", "supplier_id", 20),
|
||
("supplierName", "supplier_name", 200),
|
||
("goodsId", "goods_id", 20),
|
||
("goodsName", "goods_name", 200),
|
||
("regNo", "reg_no", 100),
|
||
("spec", "spec", 100),
|
||
("unit", "unit", 20),
|
||
("goodsSourceId", "goods_source_id", 20),
|
||
("purchaserId", "purchaser_id", 20),
|
||
("purchaserName", "purchaser_name", 100),
|
||
("plannerId", "planner_id", 20),
|
||
("plannerName", "planner_name", 100),
|
||
("orgCode", "org_code", 10),
|
||
("goodsMdmCode", "goods_mdm_code", 100),
|
||
("supplierMdmCode", "supplier_mdm_code", 100),
|
||
)
|
||
|
||
_DATE_FIELDS = (
|
||
("contractSignDate", "contract_sign_date"),
|
||
("contractStartDate", "contract_start_date"),
|
||
("contractEndDate", "contract_end_date"),
|
||
)
|
||
|
||
_DECIMAL_FIELDS = (
|
||
("qty", "qty"),
|
||
("purchaseTaxRate", "purchase_tax_rate"),
|
||
("unitPrice", "unit_price"),
|
||
)
|
||
|
||
|
||
# 整批接收采购合同:先校验再落库,返回与入参下标对齐的结果。
|
||
def receive_purchase_contracts(rows: list, repo) -> list[dict]:
|
||
results: list[dict | None] = [None] * len(rows)
|
||
pending: list[tuple[int, dict]] = []
|
||
seen: set[int] = set()
|
||
for index, row in enumerate(rows):
|
||
decided = _classify(row, seen)
|
||
if "resultCode" in decided:
|
||
results[index] = decided
|
||
else:
|
||
pending.append((index, decided))
|
||
if pending:
|
||
_persist(results, pending, repo)
|
||
return [item for item in results if item is not None]
|
||
|
||
|
||
# 内存判定:对象/字段/同批 fid 冲突当场出结果,通过则返回待插入记录。
|
||
def _classify(row: object, seen: set[int]) -> dict:
|
||
if not isinstance(row, dict):
|
||
return _result(None, "REJECTED", messages.ROW_NOT_OBJECT)
|
||
|
||
fid, fid_error = parse_fid(row.get("fid"))
|
||
if fid is not None and fid in seen:
|
||
return _result(fid, "CONFLICT", messages.CONTRACT_CONFLICT)
|
||
if fid is not None:
|
||
seen.add(fid)
|
||
|
||
parsed, error = _parse_row(row, fid, fid_error)
|
||
if error:
|
||
_camel, message = error
|
||
return _result(row.get("fid"), "REJECTED", message)
|
||
return parsed
|
||
|
||
|
||
# 对通过校验的行一次查重、一次插入,把 DUPLICATE / ACCEPTED 填回 results。
|
||
def _persist(results: list[dict | None], pending: list[tuple[int, dict]], repo) -> None:
|
||
existing = repo.existing_fids([record["fid"] for _index, record in pending])
|
||
to_insert: list[dict] = []
|
||
insert_at: list[int] = []
|
||
for index, record in pending:
|
||
if record["fid"] in existing:
|
||
results[index] = _result(
|
||
record["fid"], "DUPLICATE", messages.CONTRACT_DUPLICATE
|
||
)
|
||
else:
|
||
to_insert.append(record)
|
||
insert_at.append(index)
|
||
collided = repo.insert_many(to_insert) if to_insert else set()
|
||
for index, record in zip(insert_at, to_insert):
|
||
if record["fid"] in collided:
|
||
results[index] = _result(
|
||
record["fid"], "DUPLICATE", messages.CONTRACT_DUPLICATE
|
||
)
|
||
else:
|
||
results[index] = _result(
|
||
record["fid"], "ACCEPTED", messages.CONTRACT_ACCEPTED
|
||
)
|
||
|
||
|
||
# 把一条明细收成表 3 行。失败返回 (None, (字段, 说明))。
|
||
def _parse_row(row: dict, fid: int | None, fid_error: str | None):
|
||
if fid_error:
|
||
return None, ("fid", fid_error)
|
||
|
||
oms_id, oms_error = parse_oms_id(row.get("omsId"))
|
||
if oms_error:
|
||
return None, ("omsId", oms_error)
|
||
|
||
record: dict = {"fid": fid, "oms_id": oms_id, "process_status": 0}
|
||
for camel, snake, max_len in _STRING_FIELDS:
|
||
value, error = parse_str(row.get(camel), camel, max_len)
|
||
if error:
|
||
return None, (camel, error)
|
||
record[snake] = value
|
||
|
||
for camel, snake in _DATE_FIELDS:
|
||
value, error = parse_date(row.get(camel), camel)
|
||
if error:
|
||
return None, (camel, error)
|
||
record[snake] = value
|
||
|
||
if record["contract_end_date"] < record["contract_start_date"]:
|
||
return None, ("contractEndDate", "contractEndDate 不得早于 contractStartDate")
|
||
|
||
payment_term, error = parse_int(row.get("paymentTerm"), "paymentTerm")
|
||
if error:
|
||
return None, ("paymentTerm", error)
|
||
record["payment_term"] = payment_term
|
||
|
||
for camel, snake in _DECIMAL_FIELDS:
|
||
value, error = parse_decimal(row.get(camel), camel)
|
||
if error:
|
||
return None, (camel, error)
|
||
record[snake] = value
|
||
|
||
customer_type, error = parse_int(row.get("customerType"), "customerType")
|
||
if error:
|
||
return None, ("customerType", error)
|
||
record["customer_type"] = customer_type
|
||
|
||
entry_status, error = _parse_entry_status(row.get("contractEntryStatus"))
|
||
if error:
|
||
return None, ("contractEntryStatus", error)
|
||
record["contract_entry_status"] = entry_status
|
||
|
||
contract_no, error = _parse_contract_no(row.get("contractNo"), entry_status)
|
||
if error:
|
||
return None, ("contractNo", error)
|
||
record["contract_no"] = contract_no
|
||
|
||
return record, None
|
||
|
||
|
||
# 录入合同状态:缺省当 None;有值必须是 0 或 1。
|
||
def _parse_entry_status(value) -> tuple[int | None, str | None]:
|
||
if value is None or value == "":
|
||
return None, None
|
||
if type(value) is int and value in (0, 1):
|
||
return value, None
|
||
return None, "contractEntryStatus 必须是 0 或 1"
|
||
|
||
|
||
# contractEntryStatus=0 时合同号必填;其它情况可空。
|
||
def _parse_contract_no(value, entry_status: int | None) -> tuple[str | None, str | None]:
|
||
if entry_status == 0:
|
||
return parse_str(value, "contractNo", 40)
|
||
if value is None or value == "":
|
||
return None, None
|
||
return parse_str(value, "contractNo", 40)
|
||
|
||
|
||
# 拼一条接口 3 的 results[] 项。
|
||
def _result(fid, result_code: str, message: str) -> dict:
|
||
return {"fid": fid, "resultCode": result_code, "message": message}
|