Files
zkxw/backend/app/services/sample_payment_schedule_validate.py
T
zlwuandCursor db31990ebe feat: 细胞存储缴费催缴、统一样本信息与协议支付能力增强
汇总近期业务改动:催缴菜单与后端、样本信息快捷登记与进度、运费关联、客户档案扩展、协议优惠/期次与押金划转,并同步 SQL 迁移与管理端页面收敛。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 21:20:37 +08:00

102 lines
3.2 KiB
Python

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