936 lines
32 KiB
Python
936 lines
32 KiB
Python
"""按样本缴费方案 / 应缴批次 / 转账笔 CRUD(payment_model_version=2)"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
from sqlalchemy import and_, func, or_
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.utils import generate_uuid, parse_datetime
|
||
from app.crud.collection_protocol import urls_to_attachment_list
|
||
from app.models.models import (
|
||
BizCollectionProtocol,
|
||
BizPresignTransfer,
|
||
BizProtocolPartyCustomer,
|
||
BizProtocolPaymentSchedule,
|
||
BizProtocolPaymentTransfer,
|
||
BizProtocolSamplePayment,
|
||
)
|
||
from app.services.protocol_transfer_status import (
|
||
can_confirm_receipt,
|
||
can_submit_receipt,
|
||
schedule_paid_and_arrears,
|
||
)
|
||
|
||
|
||
def _dec(v: Any) -> Optional[Decimal]:
|
||
if v is None or v == "":
|
||
return None
|
||
return Decimal(str(v))
|
||
|
||
|
||
def _now() -> datetime:
|
||
return datetime.now()
|
||
|
||
|
||
def _get_sample_payment_or_raise(
|
||
db: Session, *, sample_payment_id: str, tenant_no: str
|
||
) -> BizProtocolSamplePayment:
|
||
obj = (
|
||
db.query(BizProtocolSamplePayment)
|
||
.filter(
|
||
BizProtocolSamplePayment.id == sample_payment_id,
|
||
BizProtocolSamplePayment.tenant_no == tenant_no,
|
||
BizProtocolSamplePayment.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if not obj:
|
||
raise ValueError("样本缴费方案不存在")
|
||
return obj
|
||
|
||
|
||
def _get_schedule_or_raise(
|
||
db: Session, *, schedule_id: str, tenant_no: str
|
||
) -> BizProtocolPaymentSchedule:
|
||
obj = (
|
||
db.query(BizProtocolPaymentSchedule)
|
||
.filter(
|
||
BizProtocolPaymentSchedule.id == schedule_id,
|
||
BizProtocolPaymentSchedule.tenant_no == tenant_no,
|
||
BizProtocolPaymentSchedule.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if not obj:
|
||
raise ValueError("应缴批次不存在")
|
||
return obj
|
||
|
||
|
||
def _get_transfer_or_raise(
|
||
db: Session, *, transfer_id: str, tenant_no: str
|
||
) -> BizProtocolPaymentTransfer:
|
||
obj = (
|
||
db.query(BizProtocolPaymentTransfer)
|
||
.filter(
|
||
BizProtocolPaymentTransfer.id == transfer_id,
|
||
BizProtocolPaymentTransfer.tenant_no == tenant_no,
|
||
BizProtocolPaymentTransfer.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if not obj:
|
||
raise ValueError("转账笔不存在")
|
||
return obj
|
||
|
||
|
||
def has_blocking_transfer_for_sample_payment(
|
||
db: Session, *, sample_payment_id: str, tenant_no: str
|
||
) -> bool:
|
||
row = (
|
||
db.query(BizProtocolPaymentTransfer.id)
|
||
.filter(
|
||
BizProtocolPaymentTransfer.sample_payment_id == sample_payment_id,
|
||
BizProtocolPaymentTransfer.tenant_no == tenant_no,
|
||
BizProtocolPaymentTransfer.del_flag == 0,
|
||
BizProtocolPaymentTransfer.pay_status == "jfqrz",
|
||
)
|
||
.first()
|
||
)
|
||
return row is not None
|
||
|
||
|
||
def assert_sample_payment_editable(
|
||
db: Session, *, sample_payment_id: str, tenant_no: str
|
||
) -> None:
|
||
if has_blocking_transfer_for_sample_payment(
|
||
db, sample_payment_id=sample_payment_id, tenant_no=tenant_no
|
||
):
|
||
raise ValueError("存在缴费确认中的转账笔,无法修改缴费方案或批次")
|
||
|
||
|
||
def upsert_sample_payment(
|
||
db: Session,
|
||
*,
|
||
collection_protocol_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
sample_code: str,
|
||
pay_mode: str,
|
||
contract_amount: Any = None,
|
||
protocol_sample_id: Optional[str] = None,
|
||
installment_count: Optional[int] = None,
|
||
installment_interval_months: Any = None,
|
||
installment_start_date: Optional[str] = None,
|
||
installment_advance_days: Optional[int] = None,
|
||
installment_ratios: Optional[str] = None,
|
||
lump_sum_deadline: Optional[str] = None,
|
||
remark: Optional[str] = None,
|
||
sample_payment_id: Optional[str] = None,
|
||
) -> BizProtocolSamplePayment:
|
||
code = (sample_code or "").strip()
|
||
if not code:
|
||
raise ValueError("样本编码不能为空")
|
||
mode = (pay_mode or "").strip()
|
||
if mode not in ("dj", "fq", "djfq"):
|
||
raise ValueError("缴费模式无效")
|
||
|
||
obj: Optional[BizProtocolSamplePayment] = None
|
||
if sample_payment_id:
|
||
obj = _get_sample_payment_or_raise(
|
||
db, sample_payment_id=sample_payment_id, tenant_no=tenant_no
|
||
)
|
||
else:
|
||
obj = (
|
||
db.query(BizProtocolSamplePayment)
|
||
.filter(
|
||
BizProtocolSamplePayment.collection_protocol_id == collection_protocol_id,
|
||
BizProtocolSamplePayment.tenant_no == tenant_no,
|
||
BizProtocolSamplePayment.sample_code == code,
|
||
BizProtocolSamplePayment.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
|
||
now = _now()
|
||
if obj:
|
||
assert_sample_payment_editable(db, sample_payment_id=obj.id, tenant_no=tenant_no)
|
||
if (obj.pay_mode or "") != mode:
|
||
assert_sample_payment_editable(db, sample_payment_id=obj.id, tenant_no=tenant_no)
|
||
obj.protocol_sample_id = (protocol_sample_id or "").strip() or None
|
||
obj.sample_code = code
|
||
obj.pay_mode = mode
|
||
obj.contract_amount = _dec(contract_amount)
|
||
obj.installment_count = installment_count
|
||
obj.installment_interval_months = _dec(installment_interval_months)
|
||
obj.installment_start_date = (installment_start_date or "").strip() or None
|
||
obj.installment_advance_days = installment_advance_days
|
||
obj.installment_ratios = (installment_ratios or "").strip() or None
|
||
obj.lump_sum_deadline = (lump_sum_deadline or "").strip() or None
|
||
if remark is not None:
|
||
obj.remark = (remark or "").strip() or None
|
||
obj.update_by = user_id
|
||
obj.update_time = now
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
|
||
obj = BizProtocolSamplePayment(
|
||
id=generate_uuid(),
|
||
collection_protocol_id=collection_protocol_id,
|
||
protocol_sample_id=(protocol_sample_id or "").strip() or None,
|
||
sample_code=code,
|
||
pay_mode=mode,
|
||
contract_amount=_dec(contract_amount),
|
||
installment_count=installment_count,
|
||
installment_interval_months=_dec(installment_interval_months),
|
||
installment_start_date=(installment_start_date or "").strip() or None,
|
||
installment_advance_days=installment_advance_days,
|
||
installment_ratios=(installment_ratios or "").strip() or None,
|
||
lump_sum_deadline=(lump_sum_deadline or "").strip() or None,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=(remark or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
|
||
|
||
def replace_schedules(
|
||
db: Session,
|
||
*,
|
||
sample_payment_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
schedules: List[Dict[str, Any]],
|
||
) -> List[BizProtocolPaymentSchedule]:
|
||
"""软删原批次(及其下 dfj 转账),重建批次。存在 jfqrz 时禁止。"""
|
||
sp = _get_sample_payment_or_raise(
|
||
db, sample_payment_id=sample_payment_id, tenant_no=tenant_no
|
||
)
|
||
assert_sample_payment_editable(
|
||
db, sample_payment_id=sample_payment_id, tenant_no=tenant_no
|
||
)
|
||
now = _now()
|
||
old_schedules = (
|
||
db.query(BizProtocolPaymentSchedule)
|
||
.filter(
|
||
BizProtocolPaymentSchedule.sample_payment_id == sample_payment_id,
|
||
BizProtocolPaymentSchedule.tenant_no == tenant_no,
|
||
BizProtocolPaymentSchedule.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
old_ids = [s.id for s in old_schedules]
|
||
if old_ids:
|
||
transfers = (
|
||
db.query(BizProtocolPaymentTransfer)
|
||
.filter(
|
||
BizProtocolPaymentTransfer.schedule_id.in_(old_ids),
|
||
BizProtocolPaymentTransfer.tenant_no == tenant_no,
|
||
BizProtocolPaymentTransfer.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
for t in transfers:
|
||
if t.pay_status == "jfqrz":
|
||
raise ValueError("存在缴费确认中的转账笔,无法重建批次")
|
||
if t.pay_status == "dfj":
|
||
t.del_flag = 1
|
||
t.update_by = user_id
|
||
t.update_time = now
|
||
db.add(t)
|
||
# yjf 转账保留:所属旧批次软删后仍可历史追溯;新批次不含这些笔
|
||
for s in old_schedules:
|
||
s.del_flag = 1
|
||
s.update_by = user_id
|
||
s.update_time = now
|
||
db.add(s)
|
||
|
||
created: List[BizProtocolPaymentSchedule] = []
|
||
for idx, raw in enumerate(schedules or []):
|
||
due_date = (raw.get("due_date") or raw.get("dueDate") or "").strip() or None
|
||
schedule_status = (
|
||
raw.get("schedule_status") or raw.get("scheduleStatus") or "open"
|
||
)
|
||
obj = BizProtocolPaymentSchedule(
|
||
id=generate_uuid(),
|
||
sample_payment_id=sp.id,
|
||
collection_protocol_id=sp.collection_protocol_id,
|
||
sample_code=sp.sample_code,
|
||
schedule_no=raw.get("schedule_no")
|
||
if raw.get("schedule_no") is not None
|
||
else raw.get("scheduleNo"),
|
||
due_amount=_dec(raw.get("due_amount") if "due_amount" in raw else raw.get("dueAmount")),
|
||
due_date=due_date,
|
||
schedule_status=(str(schedule_status).strip() or "open"),
|
||
tenant_no=tenant_no,
|
||
order_num=idx,
|
||
remark=(raw.get("remark") or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
created.append(obj)
|
||
db.flush()
|
||
return created
|
||
|
||
|
||
def add_transfer(
|
||
db: Session,
|
||
*,
|
||
schedule_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
transfer_amount: Any = None,
|
||
transfer_time: Any = None,
|
||
transfer_type: Optional[str] = None,
|
||
transfer_attachments: Optional[str] = None,
|
||
remark: Optional[str] = None,
|
||
commit: bool = True,
|
||
) -> BizProtocolPaymentTransfer:
|
||
sch = _get_schedule_or_raise(db, schedule_id=schedule_id, tenant_no=tenant_no)
|
||
now = _now()
|
||
tt = None
|
||
if transfer_time not in (None, ""):
|
||
tt = (
|
||
transfer_time
|
||
if isinstance(transfer_time, datetime)
|
||
else parse_datetime(str(transfer_time))
|
||
)
|
||
obj = BizProtocolPaymentTransfer(
|
||
id=generate_uuid(),
|
||
schedule_id=sch.id,
|
||
sample_payment_id=sch.sample_payment_id,
|
||
collection_protocol_id=sch.collection_protocol_id,
|
||
sample_code=sch.sample_code,
|
||
transfer_amount=_dec(transfer_amount),
|
||
transfer_time=tt,
|
||
transfer_type=(transfer_type or "").strip() or None,
|
||
transfer_attachments=(transfer_attachments or "").strip() or None,
|
||
pay_status="dfj",
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=(remark or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
if commit:
|
||
db.commit()
|
||
db.refresh(obj)
|
||
else:
|
||
db.flush()
|
||
return obj
|
||
|
||
|
||
def update_transfer_draft(
|
||
db: Session,
|
||
*,
|
||
transfer_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
fields: Dict[str, Any],
|
||
) -> BizProtocolPaymentTransfer:
|
||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||
if t.pay_status != "dfj":
|
||
raise ValueError("仅待缴费状态可编辑转账笔")
|
||
if "transfer_type" in fields or "transferType" in fields:
|
||
v = fields.get("transfer_type", fields.get("transferType"))
|
||
t.transfer_type = (str(v) if v is not None else "").strip() or None
|
||
if "transfer_amount" in fields or "transferAmount" in fields:
|
||
v = fields.get("transfer_amount", fields.get("transferAmount"))
|
||
amt = _dec(v)
|
||
if amt is not None and amt < 0:
|
||
raise ValueError("转账金额无效")
|
||
t.transfer_amount = amt
|
||
if "transfer_time" in fields or "transferTime" in fields:
|
||
v = fields.get("transfer_time", fields.get("transferTime"))
|
||
if v in (None, ""):
|
||
t.transfer_time = None
|
||
elif isinstance(v, datetime):
|
||
t.transfer_time = v
|
||
else:
|
||
t.transfer_time = parse_datetime(str(v))
|
||
if "transfer_attachments" in fields or "transferAttachments" in fields:
|
||
v = fields.get("transfer_attachments", fields.get("transferAttachments"))
|
||
t.transfer_attachments = (str(v) if v is not None else "").strip() or None
|
||
if "remark" in fields:
|
||
t.remark = (fields.get("remark") or "").strip() or None
|
||
t.update_by = user_id
|
||
t.update_time = _now()
|
||
db.add(t)
|
||
db.commit()
|
||
db.refresh(t)
|
||
return t
|
||
|
||
|
||
def _validate_transfer_submit_ready(t: BizProtocolPaymentTransfer) -> None:
|
||
if not (t.transfer_type or "").strip():
|
||
raise ValueError("请选择转账方式")
|
||
if t.transfer_amount is None or Decimal(str(t.transfer_amount)) <= 0:
|
||
raise ValueError("请填写转账金额")
|
||
|
||
|
||
def submit_receipt_finance(
|
||
db: Session,
|
||
*,
|
||
transfer_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
draft_fields: Optional[Dict[str, Any]] = None,
|
||
) -> BizProtocolPaymentTransfer:
|
||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||
if draft_fields:
|
||
if t.pay_status != "dfj":
|
||
raise ValueError("仅待缴费状态可编辑转账笔")
|
||
# 就地改草稿字段(不单独 commit)
|
||
if "transfer_type" in draft_fields or "transferType" in draft_fields:
|
||
v = draft_fields.get("transfer_type", draft_fields.get("transferType"))
|
||
t.transfer_type = (str(v) if v is not None else "").strip() or None
|
||
if "transfer_amount" in draft_fields or "transferAmount" in draft_fields:
|
||
t.transfer_amount = _dec(
|
||
draft_fields.get("transfer_amount", draft_fields.get("transferAmount"))
|
||
)
|
||
if "transfer_time" in draft_fields or "transferTime" in draft_fields:
|
||
v = draft_fields.get("transfer_time", draft_fields.get("transferTime"))
|
||
if v in (None, ""):
|
||
t.transfer_time = None
|
||
elif isinstance(v, datetime):
|
||
t.transfer_time = v
|
||
else:
|
||
t.transfer_time = parse_datetime(str(v))
|
||
if "transfer_attachments" in draft_fields or "transferAttachments" in draft_fields:
|
||
v = draft_fields.get(
|
||
"transfer_attachments", draft_fields.get("transferAttachments")
|
||
)
|
||
t.transfer_attachments = (str(v) if v is not None else "").strip() or None
|
||
if not can_submit_receipt(t.pay_status):
|
||
raise ValueError("仅待缴费状态可提交财务确认")
|
||
_validate_transfer_submit_ready(t)
|
||
t.pay_status = "jfqrz"
|
||
t.update_by = user_id
|
||
t.update_time = _now()
|
||
db.add(t)
|
||
db.commit()
|
||
db.refresh(t)
|
||
return t
|
||
|
||
|
||
def confirm_receipt(
|
||
db: Session,
|
||
*,
|
||
transfer_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
finance_confirm_attachments: Optional[str] = None,
|
||
finance_remark: Optional[str] = None,
|
||
) -> BizProtocolPaymentTransfer:
|
||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||
if t.pay_status == "yjf":
|
||
raise ValueError("该转账笔已确认收款")
|
||
if not can_confirm_receipt(t.pay_status):
|
||
raise ValueError("当前状态不可确认收款")
|
||
t.pay_status = "yjf"
|
||
if finance_confirm_attachments is not None:
|
||
t.finance_confirm_attachments = finance_confirm_attachments.strip() or None
|
||
if finance_remark is not None:
|
||
t.finance_remark = finance_remark.strip() or None
|
||
t.update_by = user_id
|
||
t.update_time = _now()
|
||
db.add(t)
|
||
db.commit()
|
||
db.refresh(t)
|
||
return t
|
||
|
||
|
||
def reject_receipt(
|
||
db: Session,
|
||
*,
|
||
transfer_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
) -> BizProtocolPaymentTransfer:
|
||
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
||
if t.pay_status != "jfqrz":
|
||
raise ValueError("当前状态不可退回")
|
||
t.pay_status = "dfj"
|
||
t.update_by = user_id
|
||
t.update_time = _now()
|
||
db.add(t)
|
||
db.commit()
|
||
db.refresh(t)
|
||
return t
|
||
|
||
|
||
def _transfer_to_dict(
|
||
db: Session, t: BizProtocolPaymentTransfer, tenant_no: str
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"id": t.id,
|
||
"scheduleId": t.schedule_id,
|
||
"samplePaymentId": t.sample_payment_id,
|
||
"collectionProtocolId": t.collection_protocol_id,
|
||
"sampleCode": t.sample_code or "",
|
||
"transferAmount": float(t.transfer_amount) if t.transfer_amount is not None else None,
|
||
"transferTime": t.transfer_time.strftime("%Y-%m-%d %H:%M:%S") if t.transfer_time else "",
|
||
"transferType": t.transfer_type or "",
|
||
"transferAttachments": t.transfer_attachments or "",
|
||
"attachments": urls_to_attachment_list(t.transfer_attachments, db, tenant_no),
|
||
"payStatus": t.pay_status or "",
|
||
"financeConfirmAttachments": t.finance_confirm_attachments or "",
|
||
"financeConfirmAttachmentFiles": urls_to_attachment_list(
|
||
t.finance_confirm_attachments, db, tenant_no
|
||
),
|
||
"financeRemark": t.finance_remark or "",
|
||
"remark": t.remark or "",
|
||
"source": "formal",
|
||
"createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
|
||
"updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
|
||
}
|
||
|
||
|
||
def _schedule_to_dict(
|
||
db: Session,
|
||
s: BizProtocolPaymentSchedule,
|
||
transfers: List[BizProtocolPaymentTransfer],
|
||
tenant_no: str,
|
||
) -> Dict[str, Any]:
|
||
confirmed = [
|
||
t.transfer_amount
|
||
for t in transfers
|
||
if (t.pay_status or "") == "yjf"
|
||
]
|
||
paid, arrears = schedule_paid_and_arrears(
|
||
due=s.due_amount, confirmed_transfers=confirmed
|
||
)
|
||
return {
|
||
"id": s.id,
|
||
"samplePaymentId": s.sample_payment_id,
|
||
"collectionProtocolId": s.collection_protocol_id,
|
||
"sampleCode": s.sample_code or "",
|
||
"scheduleNo": s.schedule_no,
|
||
"dueAmount": float(s.due_amount) if s.due_amount is not None else None,
|
||
"dueDate": s.due_date or "",
|
||
"scheduleStatus": s.schedule_status or "",
|
||
"paidAmount": float(paid),
|
||
"arrearsAmount": float(arrears),
|
||
"remark": s.remark or "",
|
||
"transfers": [_transfer_to_dict(db, t, tenant_no) for t in transfers],
|
||
}
|
||
|
||
|
||
def _sample_payment_to_dict(
|
||
db: Session,
|
||
p: BizProtocolSamplePayment,
|
||
schedules: List[BizProtocolPaymentSchedule],
|
||
transfers_by_schedule: Dict[str, List[BizProtocolPaymentTransfer]],
|
||
tenant_no: str,
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"id": p.id,
|
||
"collectionProtocolId": p.collection_protocol_id,
|
||
"protocolSampleId": p.protocol_sample_id or "",
|
||
"sampleCode": p.sample_code or "",
|
||
"payMode": p.pay_mode or "",
|
||
"contractAmount": float(p.contract_amount) if p.contract_amount is not None else None,
|
||
"installmentCount": p.installment_count,
|
||
"installmentIntervalMonths": float(p.installment_interval_months)
|
||
if p.installment_interval_months is not None
|
||
else None,
|
||
"installmentStartDate": p.installment_start_date or "",
|
||
"installmentAdvanceDays": p.installment_advance_days,
|
||
"installmentRatios": p.installment_ratios or "",
|
||
"lumpSumDeadline": p.lump_sum_deadline or "",
|
||
"remark": p.remark or "",
|
||
"schedules": [
|
||
_schedule_to_dict(
|
||
db, s, transfers_by_schedule.get(s.id, []), tenant_no
|
||
)
|
||
for s in schedules
|
||
],
|
||
}
|
||
|
||
|
||
def _normalize_installment_ratios(v: Any) -> Optional[str]:
|
||
if v is None or v == "":
|
||
return None
|
||
if isinstance(v, (list, dict)):
|
||
return json.dumps(v, ensure_ascii=False)
|
||
return str(v).strip() or None
|
||
|
||
|
||
def _soft_delete_sample_payment(
|
||
db: Session,
|
||
*,
|
||
sp: BizProtocolSamplePayment,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
) -> None:
|
||
assert_sample_payment_editable(db, sample_payment_id=sp.id, tenant_no=tenant_no)
|
||
now = _now()
|
||
schedules = (
|
||
db.query(BizProtocolPaymentSchedule)
|
||
.filter(
|
||
BizProtocolPaymentSchedule.sample_payment_id == sp.id,
|
||
BizProtocolPaymentSchedule.tenant_no == tenant_no,
|
||
BizProtocolPaymentSchedule.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
sids = [s.id for s in schedules]
|
||
if sids:
|
||
transfers = (
|
||
db.query(BizProtocolPaymentTransfer)
|
||
.filter(
|
||
BizProtocolPaymentTransfer.schedule_id.in_(sids),
|
||
BizProtocolPaymentTransfer.tenant_no == tenant_no,
|
||
BizProtocolPaymentTransfer.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
for t in transfers:
|
||
if t.pay_status == "jfqrz":
|
||
raise ValueError("存在缴费确认中的转账笔,无法删除样本缴费方案")
|
||
if t.pay_status == "dfj":
|
||
t.del_flag = 1
|
||
t.update_by = user_id
|
||
t.update_time = now
|
||
db.add(t)
|
||
for s in schedules:
|
||
s.del_flag = 1
|
||
s.update_by = user_id
|
||
s.update_time = now
|
||
db.add(s)
|
||
sp.del_flag = 1
|
||
sp.update_by = user_id
|
||
sp.update_time = now
|
||
db.add(sp)
|
||
|
||
|
||
def save_sample_payments_for_protocol(
|
||
db: Session,
|
||
*,
|
||
collection_protocol_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
sample_payments: List[Dict[str, Any]],
|
||
) -> None:
|
||
"""按样本保存缴费方案:upsert 方案 → 重建批次 → 写入待缴费转账笔。"""
|
||
existing = (
|
||
db.query(BizProtocolSamplePayment)
|
||
.filter(
|
||
BizProtocolSamplePayment.collection_protocol_id == collection_protocol_id,
|
||
BizProtocolSamplePayment.tenant_no == tenant_no,
|
||
BizProtocolSamplePayment.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
keep_ids: set = set()
|
||
for raw in sample_payments or []:
|
||
if not isinstance(raw, dict):
|
||
continue
|
||
sample_code = (raw.get("sample_code") or raw.get("sampleCode") or "").strip()
|
||
if not sample_code:
|
||
raise ValueError("样本缴费方案缺少样本编码")
|
||
pay_mode = (raw.get("pay_mode") or raw.get("payMode") or "").strip()
|
||
sp_id = (raw.get("id") or raw.get("samplePaymentId") or "").strip() or None
|
||
ratios = raw.get("installment_ratios")
|
||
if ratios is None:
|
||
ratios = raw.get("installmentRatios")
|
||
sp = upsert_sample_payment(
|
||
db,
|
||
collection_protocol_id=collection_protocol_id,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
sample_code=sample_code,
|
||
pay_mode=pay_mode,
|
||
contract_amount=raw.get("contract_amount", raw.get("contractAmount")),
|
||
protocol_sample_id=raw.get("protocol_sample_id") or raw.get("protocolSampleId"),
|
||
installment_count=raw.get("installment_count", raw.get("installmentCount")),
|
||
installment_interval_months=raw.get(
|
||
"installment_interval_months", raw.get("installmentIntervalMonths")
|
||
),
|
||
installment_start_date=raw.get(
|
||
"installment_start_date", raw.get("installmentStartDate")
|
||
),
|
||
installment_advance_days=raw.get(
|
||
"installment_advance_days", raw.get("installmentAdvanceDays")
|
||
),
|
||
installment_ratios=_normalize_installment_ratios(ratios),
|
||
lump_sum_deadline=raw.get("lump_sum_deadline", raw.get("lumpSumDeadline")),
|
||
remark=raw.get("remark"),
|
||
sample_payment_id=sp_id,
|
||
)
|
||
keep_ids.add(sp.id)
|
||
schedules_payload = raw.get("schedules") or []
|
||
created = replace_schedules(
|
||
db,
|
||
sample_payment_id=sp.id,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
schedules=schedules_payload,
|
||
)
|
||
for i, sch_raw in enumerate(schedules_payload):
|
||
if i >= len(created) or not isinstance(sch_raw, dict):
|
||
continue
|
||
for t_raw in sch_raw.get("transfers") or []:
|
||
if not isinstance(t_raw, dict):
|
||
continue
|
||
status = (
|
||
t_raw.get("pay_status") or t_raw.get("payStatus") or "dfj"
|
||
).strip()
|
||
# 仅重建待缴费草稿;jfqrz/yjf 由既有流程保留在旧批次或独立接口维护
|
||
if status != "dfj":
|
||
continue
|
||
add_transfer(
|
||
db,
|
||
schedule_id=created[i].id,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
transfer_amount=t_raw.get(
|
||
"transfer_amount", t_raw.get("transferAmount")
|
||
),
|
||
transfer_time=t_raw.get(
|
||
"transfer_time", t_raw.get("transferTime")
|
||
),
|
||
transfer_type=t_raw.get(
|
||
"transfer_type", t_raw.get("transferType")
|
||
),
|
||
transfer_attachments=t_raw.get(
|
||
"transfer_attachments", t_raw.get("transferAttachments")
|
||
),
|
||
remark=t_raw.get("remark"),
|
||
commit=False,
|
||
)
|
||
for sp in existing:
|
||
if sp.id not in keep_ids:
|
||
_soft_delete_sample_payment(
|
||
db, sp=sp, tenant_no=tenant_no, user_id=user_id
|
||
)
|
||
db.commit()
|
||
|
||
|
||
def get_presign_deposit_hint(
|
||
db: Session, *, source_presign_order_id: Optional[str], tenant_no: str
|
||
) -> Optional[Dict[str, Any]]:
|
||
pid = (source_presign_order_id or "").strip()
|
||
if not pid:
|
||
return None
|
||
rows = (
|
||
db.query(BizPresignTransfer)
|
||
.filter(
|
||
BizPresignTransfer.presign_order_id == pid,
|
||
BizPresignTransfer.tenant_no == tenant_no,
|
||
BizPresignTransfer.del_flag == 0,
|
||
BizPresignTransfer.pay_status == "yjf",
|
||
)
|
||
.all()
|
||
)
|
||
total = Decimal("0")
|
||
for r in rows:
|
||
if r.transfer_amount is not None:
|
||
total += Decimal(str(r.transfer_amount))
|
||
return {
|
||
"presignOrderId": pid,
|
||
"confirmedDepositAmount": float(total),
|
||
"confirmedTransferCount": len(rows),
|
||
}
|
||
|
||
|
||
def get_sample_payments_bundle(
|
||
db: Session, collection_protocol_id: str, tenant_no: str
|
||
) -> List[Dict[str, Any]]:
|
||
payments = (
|
||
db.query(BizProtocolSamplePayment)
|
||
.filter(
|
||
BizProtocolSamplePayment.collection_protocol_id == collection_protocol_id,
|
||
BizProtocolSamplePayment.tenant_no == tenant_no,
|
||
BizProtocolSamplePayment.del_flag == 0,
|
||
)
|
||
.order_by(
|
||
BizProtocolSamplePayment.order_num.asc(),
|
||
BizProtocolSamplePayment.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
if not payments:
|
||
return []
|
||
pids = [p.id for p in payments]
|
||
schedules = (
|
||
db.query(BizProtocolPaymentSchedule)
|
||
.filter(
|
||
BizProtocolPaymentSchedule.sample_payment_id.in_(pids),
|
||
BizProtocolPaymentSchedule.tenant_no == tenant_no,
|
||
BizProtocolPaymentSchedule.del_flag == 0,
|
||
)
|
||
.order_by(
|
||
BizProtocolPaymentSchedule.order_num.asc(),
|
||
BizProtocolPaymentSchedule.schedule_no.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
sids = [s.id for s in schedules]
|
||
transfers: List[BizProtocolPaymentTransfer] = []
|
||
if sids:
|
||
transfers = (
|
||
db.query(BizProtocolPaymentTransfer)
|
||
.filter(
|
||
BizProtocolPaymentTransfer.schedule_id.in_(sids),
|
||
BizProtocolPaymentTransfer.tenant_no == tenant_no,
|
||
BizProtocolPaymentTransfer.del_flag == 0,
|
||
)
|
||
.order_by(BizProtocolPaymentTransfer.create_time.asc())
|
||
.all()
|
||
)
|
||
by_schedule: Dict[str, List[BizProtocolPaymentTransfer]] = {}
|
||
for t in transfers:
|
||
by_schedule.setdefault(t.schedule_id, []).append(t)
|
||
schedules_by_payment: Dict[str, List[BizProtocolPaymentSchedule]] = {}
|
||
for s in schedules:
|
||
schedules_by_payment.setdefault(s.sample_payment_id, []).append(s)
|
||
return [
|
||
_sample_payment_to_dict(
|
||
db,
|
||
p,
|
||
schedules_by_payment.get(p.id, []),
|
||
by_schedule,
|
||
tenant_no,
|
||
)
|
||
for p in payments
|
||
]
|
||
|
||
|
||
def list_transfers_for_finance(
|
||
db: Session,
|
||
*,
|
||
pay_status: str,
|
||
tenant_no: str,
|
||
keyword: Optional[str] = None,
|
||
page: int = 1,
|
||
page_size: int = 10,
|
||
user_info=None,
|
||
due_date_start: Optional[str] = None,
|
||
due_date_end: Optional[str] = None,
|
||
) -> Tuple[List[Dict[str, Any]], int]:
|
||
if not tenant_no:
|
||
return [], 0
|
||
st = (pay_status or "").strip()
|
||
q = (
|
||
db.query(
|
||
BizProtocolPaymentTransfer,
|
||
BizCollectionProtocol,
|
||
BizProtocolPartyCustomer,
|
||
BizProtocolPaymentSchedule,
|
||
BizProtocolSamplePayment,
|
||
)
|
||
.join(
|
||
BizCollectionProtocol,
|
||
and_(
|
||
BizCollectionProtocol.id
|
||
== BizProtocolPaymentTransfer.collection_protocol_id,
|
||
BizCollectionProtocol.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizProtocolPartyCustomer,
|
||
and_(
|
||
BizProtocolPartyCustomer.collection_protocol_id
|
||
== BizProtocolPaymentTransfer.collection_protocol_id,
|
||
BizProtocolPartyCustomer.tenant_no == tenant_no,
|
||
BizProtocolPartyCustomer.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizProtocolPaymentSchedule,
|
||
and_(
|
||
BizProtocolPaymentSchedule.id == BizProtocolPaymentTransfer.schedule_id,
|
||
BizProtocolPaymentSchedule.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizProtocolSamplePayment,
|
||
and_(
|
||
BizProtocolSamplePayment.id
|
||
== BizProtocolPaymentTransfer.sample_payment_id,
|
||
BizProtocolSamplePayment.del_flag == 0,
|
||
),
|
||
)
|
||
.filter(
|
||
BizProtocolPaymentTransfer.tenant_no == tenant_no,
|
||
BizProtocolPaymentTransfer.del_flag == 0,
|
||
BizProtocolPaymentTransfer.pay_status == st,
|
||
)
|
||
)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q = q.filter(
|
||
or_(
|
||
BizCollectionProtocol.protocol_no.like(like),
|
||
BizProtocolPaymentTransfer.sample_code.like(like),
|
||
BizProtocolPartyCustomer.mother_name.like(like),
|
||
BizProtocolPartyCustomer.mother_id_card.like(like),
|
||
BizProtocolPartyCustomer.newborn_name.like(like),
|
||
BizProtocolPartyCustomer.newborn_id_card.like(like),
|
||
)
|
||
)
|
||
ds = (due_date_start or "").strip().replace("-", "")[:8]
|
||
de = (due_date_end or "").strip().replace("-", "")[:8]
|
||
if ds:
|
||
q = q.filter(BizProtocolPaymentSchedule.due_date >= ds)
|
||
if de:
|
||
q = q.filter(BizProtocolPaymentSchedule.due_date <= de)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
|
||
q = apply_data_scope(
|
||
q, BizProtocolPaymentTransfer, user_info, db, tenant_no
|
||
)
|
||
total = q.order_by(None).with_entities(func.count(BizProtocolPaymentTransfer.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = (
|
||
q.order_by(BizProtocolPaymentTransfer.create_time.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
out: List[Dict[str, Any]] = []
|
||
for t, proto, party, sch, sp in rows:
|
||
item = _transfer_to_dict(db, t, tenant_no)
|
||
item["protocolNo"] = (proto.protocol_no if proto else "") or ""
|
||
item["protocolStatus"] = (proto.protocol_status if proto else "") or ""
|
||
item["voidType"] = (proto.void_type if proto else "") or ""
|
||
item["motherName"] = (party.mother_name if party else "") or ""
|
||
item["motherIdCard"] = (party.mother_id_card if party else "") or ""
|
||
item["newbornName"] = (party.newborn_name if party else "") or ""
|
||
item["newbornIdCard"] = (party.newborn_id_card if party else "") or ""
|
||
item["installmentNo"] = sch.schedule_no if sch else None
|
||
item["installmentDueDate"] = (sch.due_date if sch else "") or ""
|
||
item["dueAmount"] = (
|
||
float(sch.due_amount) if sch and sch.due_amount is not None else None
|
||
)
|
||
item["payMode"] = (sp.pay_mode if sp else "") or ""
|
||
out.append(item)
|
||
return out, int(total)
|