Files
zkxw/backend/app/crud/protocol_sample_payment.py
T

702 lines
24 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.
"""按样本缴费方案 / 应缴批次 / 转账笔 CRUD(payment_model_version=2)"""
from __future__ import annotations
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,
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,
) -> 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)
db.commit()
db.refresh(obj)
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 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,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
st = (pay_status or "").strip()
q = (
db.query(BizProtocolPaymentTransfer, BizCollectionProtocol, BizProtocolPartyCustomer)
.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,
),
)
.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),
)
)
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 in rows:
item = _transfer_to_dict(db, t, tenant_no)
item["protocolNo"] = (proto.protocol_no if proto else "") or ""
item["motherName"] = (party.mother_name if party else "") or ""
out.append(item)
return out, int(total)