汇总近期业务改动:催缴菜单与后端、样本信息快捷登记与进度、运费关联、客户档案扩展、协议优惠/期次与押金划转,并同步 SQL 迁移与管理端页面收敛。 Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
3.2 KiB
Python
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
|