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