| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- from __future__ import annotations
- from decimal import Decimal
- from typing import Any, Iterable, Mapping, Optional, Tuple
- def _dec(v: Any) -> Decimal:
- if v is None or v == "":
- return Decimal("0")
- return Decimal(str(v))
- def _status(row: Mapping[str, Any], *keys: str) -> str:
- for k in keys:
- if k in row and row[k] is not None:
- return str(row[k]).strip()
- return ""
- def _due_date(row: Mapping[str, Any]) -> str:
- for k in ("dueDate", "due_date"):
- if k in row and row[k] is not None:
- return str(row[k]).strip()
- return ""
- def schedule_required_fields_ok(
- *,
- schedules: Iterable[Mapping[str, Any]],
- sample_code: str = "",
- require_fields: bool = True,
- ) -> Tuple[bool, Optional[str]]:
- """
- 未作废期:应缴金额与应缴时间必填(分期场景由调用方 require_fields=True)。
- """
- if not require_fields:
- return True, None
- code = (sample_code or "").strip()
- prefix = f"样本 {code} " if code else ""
- idx = 0
- for s in schedules or []:
- if not isinstance(s, Mapping):
- continue
- st = _status(s, "scheduleStatus", "schedule_status") or "open"
- if st == "void":
- continue
- idx += 1
- amt_raw = s.get("dueAmount", s.get("due_amount"))
- if amt_raw is None or amt_raw == "":
- return False, f"{prefix}第 {idx} 期应缴金额必须填写"
- amt = _dec(amt_raw)
- if amt <= 0:
- return False, f"{prefix}第 {idx} 期应缴金额必须填写"
- if not _due_date(s):
- return False, f"{prefix}第 {idx} 期应缴时间必须填写"
- return True, None
- def schedule_dues_match_contract(
- *,
- contract_amount: Any,
- schedules: Iterable[Mapping[str, Any]],
- sample_code: str = "",
- tol: Decimal = Decimal("0.015"),
- require_schedule_fields: bool = False,
- ) -> Tuple[bool, Optional[str]]:
- """
- 未作废期 dueAmount 合计须等于合同金额。
- require_schedule_fields=True 时先校验每期金额与应缴日必填。
- 返回 (True, None) 或 (False, 错误文案)。
- """
- sched_list = [s for s in (schedules or []) if isinstance(s, Mapping)]
- if require_schedule_fields:
- ok_req, req_msg = schedule_required_fields_ok(
- schedules=sched_list, sample_code=sample_code, require_fields=True
- )
- if not ok_req:
- return False, req_msg
- if contract_amount is None or contract_amount == "":
- return True, None
- contract = _dec(contract_amount)
- total = Decimal("0")
- active_count = 0
- for s in sched_list:
- st = _status(s, "scheduleStatus", "schedule_status") or "open"
- if st == "void":
- continue
- active_count += 1
- total += _dec(s.get("dueAmount", s.get("due_amount")))
- # 尚未生成应缴批次时不拦(与前端一致)
- if active_count == 0:
- return True, None
- if abs(contract - total) <= tol:
- return True, None
- code = (sample_code or "").strip()
- prefix = f"样本 {code} " if code else ""
- msg = (
- f"{prefix}合同金额 {contract:.2f} 与各期应缴合计 {total:.2f} 不一致,请调整后再保存"
- )
- return False, msg
|