sample_payment_schedule_validate.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. from __future__ import annotations
  2. from decimal import Decimal
  3. from typing import Any, Iterable, Mapping, Optional, Tuple
  4. def _dec(v: Any) -> Decimal:
  5. if v is None or v == "":
  6. return Decimal("0")
  7. return Decimal(str(v))
  8. def _status(row: Mapping[str, Any], *keys: str) -> str:
  9. for k in keys:
  10. if k in row and row[k] is not None:
  11. return str(row[k]).strip()
  12. return ""
  13. def _due_date(row: Mapping[str, Any]) -> str:
  14. for k in ("dueDate", "due_date"):
  15. if k in row and row[k] is not None:
  16. return str(row[k]).strip()
  17. return ""
  18. def schedule_required_fields_ok(
  19. *,
  20. schedules: Iterable[Mapping[str, Any]],
  21. sample_code: str = "",
  22. require_fields: bool = True,
  23. ) -> Tuple[bool, Optional[str]]:
  24. """
  25. 未作废期:应缴金额与应缴时间必填(分期场景由调用方 require_fields=True)。
  26. """
  27. if not require_fields:
  28. return True, None
  29. code = (sample_code or "").strip()
  30. prefix = f"样本 {code} " if code else ""
  31. idx = 0
  32. for s in schedules or []:
  33. if not isinstance(s, Mapping):
  34. continue
  35. st = _status(s, "scheduleStatus", "schedule_status") or "open"
  36. if st == "void":
  37. continue
  38. idx += 1
  39. amt_raw = s.get("dueAmount", s.get("due_amount"))
  40. if amt_raw is None or amt_raw == "":
  41. return False, f"{prefix}第 {idx} 期应缴金额必须填写"
  42. amt = _dec(amt_raw)
  43. if amt <= 0:
  44. return False, f"{prefix}第 {idx} 期应缴金额必须填写"
  45. if not _due_date(s):
  46. return False, f"{prefix}第 {idx} 期应缴时间必须填写"
  47. return True, None
  48. def schedule_dues_match_contract(
  49. *,
  50. contract_amount: Any,
  51. schedules: Iterable[Mapping[str, Any]],
  52. sample_code: str = "",
  53. tol: Decimal = Decimal("0.015"),
  54. require_schedule_fields: bool = False,
  55. ) -> Tuple[bool, Optional[str]]:
  56. """
  57. 未作废期 dueAmount 合计须等于合同金额。
  58. require_schedule_fields=True 时先校验每期金额与应缴日必填。
  59. 返回 (True, None) 或 (False, 错误文案)。
  60. """
  61. sched_list = [s for s in (schedules or []) if isinstance(s, Mapping)]
  62. if require_schedule_fields:
  63. ok_req, req_msg = schedule_required_fields_ok(
  64. schedules=sched_list, sample_code=sample_code, require_fields=True
  65. )
  66. if not ok_req:
  67. return False, req_msg
  68. if contract_amount is None or contract_amount == "":
  69. return True, None
  70. contract = _dec(contract_amount)
  71. total = Decimal("0")
  72. active_count = 0
  73. for s in sched_list:
  74. st = _status(s, "scheduleStatus", "schedule_status") or "open"
  75. if st == "void":
  76. continue
  77. active_count += 1
  78. total += _dec(s.get("dueAmount", s.get("due_amount")))
  79. # 尚未生成应缴批次时不拦(与前端一致)
  80. if active_count == 0:
  81. return True, None
  82. if abs(contract - total) <= tol:
  83. return True, None
  84. code = (sample_code or "").strip()
  85. prefix = f"样本 {code} " if code else ""
  86. msg = (
  87. f"{prefix}合同金额 {contract:.2f} 与各期应缴合计 {total:.2f} 不一致,请调整后再保存"
  88. )
  89. return False, msg