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

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

88 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""缴费模式切换校验(与前端 protocol-pay-mode-switch.ts 规则一致)"""
from __future__ import annotations
from decimal import Decimal
from typing import Any, Dict, List, Optional
from app.services.protocol_payment_installment import is_legacy_installment_ratios
LOCKED_STATUSES = frozenset({"jfqrz", "yjf", "tfqrz", "ytf"})
# 仅阻断进行中的缴费/退费确认;已退费(ytf)为终态,允许随协议单保存(明细只读)
BLOCKING_STATUSES = frozenset({"jfqrz", "tfqrz"})
def _dec(v: Any) -> float:
if v is None:
return 0.0
return float(Decimal(str(v)))
def dj_due_amount_mismatch_hint(
contract_amount: Optional[Any],
details: Optional[list],
) -> Optional[str]:
"""趸交:应缴合计与合同金额不一致时返回提醒文案(不阻断保存)。"""
rows = [dict(d) for d in (details or [])]
contract = _dec(contract_amount)
if not contract:
return None
total_due = sum(
_dec(r.get("due_amount") if r.get("due_amount") is not None else r.get("dueAmount"))
for r in rows
)
if abs(total_due - contract) > 0.015:
return f"趸交各明细应缴金额之和应为 {contract:.2f},当前为 {total_due:.2f},请核对"
return None
def validate_payment_details_for_save(
pay_mode: str,
contract_amount: Optional[Any],
installment_count: Optional[int],
details: Optional[list],
installment_ratios: Optional[List[float]] = None,
installment_advance_days: Optional[int] = None,
) -> None:
"""保存缴费明细前的 P0 校验。
趸交应缴合计与合同金额不一致仅作前端提醒,不在此阻断;按用户填写原样保存。
"""
rows = [dict(d) for d in (details or [])]
contract = _dec(contract_amount)
for r in rows:
status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
if status in BLOCKING_STATUSES:
raise ValueError("存在缴费确认中或退费处理中的明细,无法保存")
yjf_rows = [r for r in rows if (r.get("pay_status") or r.get("payStatus")) == "yjf"]
if pay_mode in ("fq", "djfq", "fgd"):
count = int(installment_count or 0)
if count < 1:
raise ValueError("分期/分改趸模式须填写期数")
if installment_advance_days is not None and int(installment_advance_days) < 0:
raise ValueError("应缴费提前天数不能为负数")
if installment_ratios is not None:
values = [float(x) for x in installment_ratios]
if len(values) != count:
raise ValueError("分期金额数量须与分期期数一致")
if is_legacy_installment_ratios(values, Decimal(str(contract)) if contract else None):
if abs(sum(values) - 100.0) > 0.01:
raise ValueError("缴费比例之和须等于 100%")
if len(rows) != count:
raise ValueError(f"分期明细条数应等于分期期数({count})")
fq_forbidden = frozenset({"jfqrz", "tfqrz"})
for r in rows:
status = (r.get("pay_status") or r.get("payStatus") or "dfj").strip()
if status in fq_forbidden:
raise ValueError("分期模式下存在不可保存的缴费状态")
if yjf_rows:
max_paid_no = max(
int(r.get("installment_no") or r.get("installmentNo") or 0)
for r in yjf_rows
)
if max_paid_no > count:
raise ValueError("分期期数不能少于已缴费的最大期次")