# 合同班合单:可录判定、按供应商+货主+三个日期分组、草稿映射。 from __future__ import annotations from collections import OrderedDict from dataclasses import replace from datetime import date, datetime from decimal import Decimal, ROUND_HALF_UP from typing import Any from contract_draft import ContractDraft from contract_flow_steps import ContractGoodsLine from persist import LOOKUP_ONLY_RESULT from repo import PurchaseContractRow # 判断是否只录协议行:contract_entry_status=0 且已有合同号。 # # 合同班跳过这些行,留给返利班。 def is_protocol_rerun(row: PurchaseContractRow) -> bool: if row.contract_entry_status is not None and int(row.contract_entry_status) == 0: return bool((row.contract_no or "").strip()) return False # 判断是否可领:process_status=0 且不是协议重录行。 # # 过期的 process_status=3 由仓库租约逻辑另判,本函数只认新鲜待领。 def is_claimable(row: PurchaseContractRow) -> bool: if int(row.process_status or 0) != 0: return False return not is_protocol_rerun(row) # 合单键:供应商、货主、签订日、开始日、结束日。任一不同则各自成组。 def _group_key(row: PurchaseContractRow) -> tuple: return ( row.supplier_id, row.org_code, row.contract_sign_date, row.contract_start_date, row.contract_end_date, ) # 按合单键分组,组内按 fid 升序,头字段取该组第一行。 def group_by_supplier(rows: list[PurchaseContractRow]) -> list[list[PurchaseContractRow]]: groups: OrderedDict[tuple, list[PurchaseContractRow]] = OrderedDict() for row in sorted(rows, key=lambda item: item.fid): groups.setdefault(_group_key(row), []).append(row) return list(groups.values()) # 检查合单是否缺供应商或货品 MDM 码。缺则返回原因,齐则返回 None。 def missing_mdm_reason(rows: list[PurchaseContractRow]) -> str | None: if not rows: return "缺少供应商MDM码" if not (rows[0].supplier_mdm_code or "").strip(): return "缺少供应商MDM码" for row in rows: if not (row.goods_mdm_code or "").strip(): return "缺少货品MDM码" return None # 把金额收成四位小数字符串,四舍五入。 def money4(value) -> str: amount = Decimal(str(value)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP) return f"{amount:.4f}" # 把日期收成 YYYY-MM-DD。空值返回空字符串。 def iso_date(value) -> str: if value is None: return "" if isinstance(value, datetime): return value.date().isoformat() if isinstance(value, date): return value.isoformat() return str(value)[:10] def draft_from_group(rows: list[PurchaseContractRow]) -> ContractDraft: """把同一供应商一组表 3 行收成页面草稿。头字段取第一行。 结果里已有「只查号」时,下一班只查已有合同,不再起草。 """ first = rows[0] sign = iso_date(first.contract_sign_date) total = sum( (Decimal(str(row.qty)) * Decimal(str(row.unit_price)) for row in rows), Decimal("0"), ) goods = [ ContractGoodsLine( operation_code=(row.goods_mdm_code or "").strip(), goods_code=(row.goods_mdm_code or "").strip(), goods_name=row.goods_name, approval_no=row.reg_no, spec=row.spec, unit=row.unit, quantity=float(row.qty), mdm_code=(row.goods_mdm_code or "").strip(), unit_price=money4(row.unit_price), ) for row in rows ] return ContractDraft( supplier_name=first.supplier_name, order_date=sign, source_type="", customer_code=(first.supplier_mdm_code or "").strip(), customer_name=first.supplier_name, amount=money4(total), contract_sign_date=sign, contract_start_date=iso_date(first.contract_start_date), contract_end_date=iso_date(first.contract_end_date), belong_year=sign[:4] if sign else "", payment_days=str(first.payment_term), goods_lines=goods, lookup_only=any(LOOKUP_ONLY_RESULT in (row.process_result or "") for row in rows), ) # 判断是不是订单中心接口 3 信封:顶层有 data 数组。 def is_oms_envelope(data: object) -> bool: if not isinstance(data, dict) or "data" not in data: return False items = data.get("data") if not isinstance(items, list): return False if not items: return True first = items[0] return isinstance(first, dict) and ( "omsId" in first or "supplierMdmCode" in first or "goodsMdmCode" in first ) # 把接口 3 一行收成表 3 内存行。不落库,给合单和页面草稿用。 def row_from_oms_item(item: dict[str, Any]) -> PurchaseContractRow: entry = item.get("contractEntryStatus") if entry in (None, ""): entry_status = None else: entry_status = int(entry) contract_no = item.get("contractNo") if contract_no is None or str(contract_no).strip() == "": contract_no = None else: contract_no = str(contract_no).strip() return PurchaseContractRow( fid=_oms_int(item.get("fid")), oms_id=_oms_int(item.get("omsId")), supplier_id=_oms_str(item.get("supplierId")), supplier_name=_oms_str(item.get("supplierName")), contract_sign_date=_oms_date(item.get("contractSignDate")), contract_start_date=_oms_date(item.get("contractStartDate")), contract_end_date=_oms_date(item.get("contractEndDate")), payment_term=_oms_int(item.get("paymentTerm")), goods_id=_oms_str(item.get("goodsId")), goods_name=_oms_str(item.get("goodsName")), reg_no=_oms_str(item.get("regNo")), spec=_oms_str(item.get("spec")), unit=_oms_str(item.get("unit")), qty=_oms_decimal(item.get("qty")), unit_price=_oms_decimal(item.get("unitPrice")), purchaser_id=_oms_str(item.get("purchaserId")), purchaser_name=_oms_str(item.get("purchaserName")), planner_id=_oms_str(item.get("plannerId")), planner_name=_oms_str(item.get("plannerName")), contract_entry_status=entry_status, contract_no=contract_no, process_status=0, org_code=_oms_str(item.get("orgCode")) or None, goods_mdm_code=_oms_str(item.get("goodsMdmCode")) or None, supplier_mdm_code=_oms_str(item.get("supplierMdmCode")) or None, ) # 把接口 3 信封收成页面草稿。协议重录行跳过,合单规则与班次相同。 # # 顶层 submitMode 会写进每份草稿;缺则仍是起草默认的 submit。 def drafts_from_oms_payload(payload: dict[str, Any]) -> list[ContractDraft]: items = payload.get("data") if isinstance(payload, dict) else None if not isinstance(items, list): return [] rows: list[PurchaseContractRow] = [] for item in items: if not isinstance(item, dict): continue row = row_from_oms_item(item) if is_claimable(row): rows.append(row) drafts = [draft_from_group(group) for group in group_by_supplier(rows)] mode = _oms_str(payload.get("submitMode")) if isinstance(payload, dict) else "" if mode: drafts = [replace(draft, submit_mode=mode) for draft in drafts] return drafts # 把接口 3 字段收成整数。空当 0。 def _oms_int(value: Any) -> int: if value in (None, ""): return 0 return int(value) # 把接口 3 字段收成去两端空白的字符串。None 当空。 def _oms_str(value: Any) -> str: if value is None: return "" return str(value).strip() # 把接口 3 日期收成 date。空当 None。 def _oms_date(value: Any) -> date | None: if value in (None, ""): return None if isinstance(value, datetime): return value.date() if isinstance(value, date): return value return date.fromisoformat(str(value)[:10]) # 把接口 3 金额/数量收成 Decimal。空当 0。 def _oms_decimal(value: Any) -> Decimal: if value in (None, ""): return Decimal("0") return Decimal(str(value))