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

608 lines
23 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.
"""按样本缴费方案 / 转账笔财务确认 API(payment_model_version=2)"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.core.response import error_response, success_response
from app.crud import protocol_sample_payment as psp_svc
from app.models.models import BizCollectionProtocol, SysUser
router = APIRouter(prefix="/web/admin", tags=["细胞存储-按样本缴费"])
def _get_tenant_no(db: Session, user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失,无法确定租户")
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
if not sys_user or not sys_user.tenant_no:
raise PermissionError("用户租户信息缺失")
return sys_user.tenant_no
def _get_current_user_id(user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失")
return str(user_id)
def _protocol_accessible(db: Session, protocol_id: str, tenant_no: str, user: dict) -> bool:
from app.core.data_scope import apply_data_scope
q = db.query(BizCollectionProtocol).filter(
BizCollectionProtocol.id == protocol_id,
BizCollectionProtocol.del_flag == 0,
BizCollectionProtocol.tenant_no == tenant_no,
)
q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
return q.first() is not None
class ScheduleBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
id: Optional[str] = None
schedule_no: Optional[int] = Field(None, alias="scheduleNo")
due_amount: Optional[float] = Field(None, alias="dueAmount")
due_date: Optional[str] = Field(None, alias="dueDate")
schedule_status: Optional[str] = Field(None, alias="scheduleStatus")
remark: Optional[str] = None
transfers: Optional[List[Dict[str, Any]]] = None
class SamplePaymentBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
id: Optional[str] = None
sample_code: str = Field(..., alias="sampleCode")
protocol_sample_id: Optional[str] = Field(None, alias="protocolSampleId")
pay_mode: str = Field(..., alias="payMode")
contract_amount: Optional[float] = Field(None, alias="contractAmount")
installment_count: Optional[int] = Field(None, alias="installmentCount")
installment_interval_months: Optional[float] = Field(
None, alias="installmentIntervalMonths"
)
installment_start_date: Optional[str] = Field(None, alias="installmentStartDate")
installment_advance_days: Optional[int] = Field(None, alias="installmentAdvanceDays")
installment_ratios: Optional[Any] = Field(None, alias="installmentRatios")
lump_sum_deadline: Optional[str] = Field(None, alias="lumpSumDeadline")
remark: Optional[str] = None
schedules: Optional[List[ScheduleBody]] = None
class SamplePaymentsPutBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
sample_payments: List[SamplePaymentBody] = Field(
default_factory=list, alias="samplePayments"
)
class TransferCreateBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
schedule_id: str = Field(..., alias="scheduleId")
transfer_amount: Optional[float] = Field(None, alias="transferAmount")
transfer_time: Optional[str] = Field(None, alias="transferTime")
transfer_type: Optional[str] = Field(None, alias="transferType")
transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
remark: Optional[str] = None
class TransferDraftBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
transfer_amount: Optional[float] = Field(None, alias="transferAmount")
transfer_time: Optional[str] = Field(None, alias="transferTime")
transfer_type: Optional[str] = Field(None, alias="transferType")
transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
remark: Optional[str] = None
class FinanceConfirmBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
finance_confirm_attachments: Optional[str] = Field(
None, alias="financeConfirmAttachments"
)
finance_remark: Optional[str] = Field(None, alias="financeRemark")
def _ratios_to_str(ratios: Any) -> Optional[str]:
if ratios is None:
return None
if isinstance(ratios, str):
return ratios.strip() or None
import json
try:
return json.dumps(ratios, ensure_ascii=False)
except Exception:
return str(ratios)
def _save_sample_payments(
db: Session,
*,
protocol_id: str,
tenant_no: str,
user_id: str,
payments: List[SamplePaymentBody],
) -> List[Dict[str, Any]]:
for item in payments:
prev = psp_svc.find_active_sample_payment(
db,
tenant_no=tenant_no,
sample_payment_id=item.id,
collection_protocol_id=protocol_id,
sample_code=item.sample_code,
)
prev_mode = (prev.pay_mode or "").strip() if prev else ""
sp = psp_svc.upsert_sample_payment(
db,
collection_protocol_id=protocol_id,
tenant_no=tenant_no,
user_id=user_id,
sample_code=item.sample_code,
pay_mode=item.pay_mode,
contract_amount=item.contract_amount,
protocol_sample_id=item.protocol_sample_id,
installment_count=item.installment_count,
installment_interval_months=item.installment_interval_months,
installment_start_date=item.installment_start_date,
installment_advance_days=item.installment_advance_days,
installment_ratios=_ratios_to_str(item.installment_ratios),
lump_sum_deadline=item.lump_sum_deadline,
remark=item.remark,
sample_payment_id=item.id,
)
next_mode = (item.pay_mode or "").strip()
if (
prev_mode == "fq"
and next_mode in ("fgd", "dj")
and psp_svc.sample_payment_has_yjf(
db, sample_payment_id=sp.id, tenant_no=tenant_no
)
):
psp_svc.apply_fq_to_fgd_merge(
db,
sample_payment_id=sp.id,
tenant_no=tenant_no,
user_id=user_id,
target_pay_mode=next_mode,
)
continue
if item.schedules is not None:
schedule_payloads = [s.model_dump(by_alias=False) for s in item.schedules]
created = psp_svc.replace_schedules(
db,
sample_payment_id=sp.id,
tenant_no=tenant_no,
user_id=user_id,
schedules=schedule_payloads,
)
# 可选:随 PUT 带入的草稿转账(仅新建 dfj)
for sch_body, sch_row in zip(item.schedules, created):
for tr in sch_body.transfers or []:
psp_svc.add_transfer(
db,
schedule_id=sch_row.id,
tenant_no=tenant_no,
user_id=user_id,
transfer_amount=tr.get("transferAmount") or tr.get("transfer_amount"),
transfer_time=tr.get("transferTime") or tr.get("transfer_time"),
transfer_type=tr.get("transferType") or tr.get("transfer_type"),
transfer_attachments=tr.get("transferAttachments")
or tr.get("transfer_attachments"),
remark=tr.get("remark"),
commit=False,
)
db.commit()
return psp_svc.get_sample_payments_bundle(db, protocol_id, tenant_no)
@router.get("/payment-chase")
async def list_payment_chase(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
chaseType: Optional[str] = Query(None, alias="chaseType"),
payMode: Optional[str] = Query(None, alias="payMode"),
salesUserId: Optional[str] = Query(None, alias="salesUserId"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
"""催缴信息:逾期未清 ∪ 未来 30 天内应缴未清(一行一期)。"""
try:
tenant_no = _get_tenant_no(db, user)
items, total = psp_svc.list_payment_chase_page(
db,
tenant_no=tenant_no,
keyword=keyword,
chase_type=chaseType,
pay_mode=payMode,
sales_user_id=salesUserId,
page=page,
page_size=page_size,
user_info=user,
)
return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
except Exception as e:
return error_response(500, str(e))
@router.get("/sample-payments")
async def list_sample_payments(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
payMode: Optional[str] = Query(None, alias="payMode"),
progressStatus: Optional[str] = Query(None, alias="progressStatus"),
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
"""按样本缴费方案分页列表(独立缴费信息菜单)。"""
try:
tenant_no = _get_tenant_no(db, user)
items, total = psp_svc.list_sample_payments_page(
db,
tenant_no=tenant_no,
keyword=keyword,
pay_mode=payMode,
progress_status=progressStatus,
due_date_start=installmentDueDateStart,
due_date_end=installmentDueDateEnd,
page=page,
page_size=page_size,
user_info=user,
)
return success_response(
{"list": items, "total": total, "page": page, "pageSize": page_size}
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.get("/sample-payments/{sample_payment_id}")
async def get_sample_payment(
sample_payment_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = psp_svc.get_sample_payment_detail(
db,
sample_payment_id=sample_payment_id,
tenant_no=tenant_no,
user_info=user,
)
if not data:
return error_response(403, "无权查看或记录不存在")
return success_response(data)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.get("/collection-protocols/{protocol_id}/sample-payments")
async def get_sample_payments(
protocol_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
if not _protocol_accessible(db, protocol_id, tenant_no, user):
return error_response(404, "协议单不存在或无权查看")
data = psp_svc.get_sample_payments_bundle(db, protocol_id, tenant_no)
return success_response(data)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.put("/collection-protocols/{protocol_id}/sample-payments")
async def put_sample_payments(
protocol_id: str,
body: SamplePaymentsPutBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
if not _protocol_accessible(db, protocol_id, tenant_no, user):
return error_response(404, "协议单不存在或无权操作")
# pydantic v2: may need model_validate with populate
payments = body.sample_payments
if hasattr(body, "model_dump"):
raw = body.model_dump(by_alias=True)
payments = [
SamplePaymentBody.model_validate(x)
for x in (raw.get("samplePayments") or raw.get("sample_payments") or [])
]
data = _save_sample_payments(
db,
protocol_id=protocol_id,
tenant_no=tenant_no,
user_id=user_id,
payments=payments or body.sample_payments,
)
return success_response(data, message="保存成功")
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.post("/payment-transfers")
async def create_payment_transfer(
body: TransferCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
sch = psp_svc._get_schedule_or_raise(
db, schedule_id=body.schedule_id, tenant_no=tenant_no
)
if not _protocol_accessible(db, sch.collection_protocol_id, tenant_no, user):
return error_response(403, "无权操作该协议")
obj = psp_svc.add_transfer(
db,
schedule_id=body.schedule_id,
tenant_no=tenant_no,
user_id=user_id,
transfer_amount=body.transfer_amount,
transfer_time=body.transfer_time,
transfer_type=body.transfer_type,
transfer_attachments=body.transfer_attachments,
remark=body.remark,
)
from app.crud.protocol_sample_payment import _transfer_to_dict
return success_response(
_transfer_to_dict(db, obj, tenant_no), message="创建成功"
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.put("/payment-transfers/{transfer_id}")
async def update_payment_transfer(
transfer_id: str,
body: TransferDraftBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
fields = body.model_dump(by_alias=False, exclude_unset=True)
obj = psp_svc.update_transfer_draft(
db,
transfer_id=transfer_id,
tenant_no=tenant_no,
user_id=user_id,
fields=fields,
)
if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
return error_response(403, "无权操作该协议")
from app.crud.protocol_sample_payment import _transfer_to_dict
return success_response(_transfer_to_dict(db, obj, tenant_no), message="保存成功")
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.post("/payment-transfers/{transfer_id}/submit-receipt-finance")
async def submit_payment_transfer_finance(
transfer_id: str,
body: Optional[TransferDraftBody] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
draft = body.model_dump(by_alias=True, exclude_unset=True) if body else None
obj = psp_svc.submit_receipt_finance(
db,
transfer_id=transfer_id,
tenant_no=tenant_no,
user_id=user_id,
draft_fields=draft,
)
if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
return error_response(403, "无权操作该协议")
from app.crud.protocol_sample_payment import _transfer_to_dict
return success_response(_transfer_to_dict(db, obj, tenant_no), message="已提交财务确认")
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.post("/payment-transfers/{transfer_id}/withdraw")
async def withdraw_payment_transfer(
transfer_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = psp_svc.withdraw_receipt(
db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
)
if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
return error_response(403, "无权操作该协议")
from app.crud.protocol_sample_payment import _transfer_to_dict
return success_response(_transfer_to_dict(db, obj, tenant_no), message="已撤回")
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.get("/payment-transfers")
async def list_payment_transfers(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
payStatus: str = Query("dfj", alias="payStatus"),
keyword: Optional[str] = None,
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
"""按转账笔状态分页(缴费信息看板 / 财务共用)。"""
try:
tenant_no = _get_tenant_no(db, user)
items, total = psp_svc.list_transfers_for_finance(
db,
pay_status=payStatus,
tenant_no=tenant_no,
keyword=keyword,
page=page,
page_size=page_size,
user_info=user,
due_date_start=installmentDueDateStart,
due_date_end=installmentDueDateEnd,
)
return success_response(
{"list": items, "total": total, "page": page, "pageSize": page_size}
)
except PermissionError as e:
return error_response(403, str(e))
@router.get("/payment-transfers/receipt-confirm")
async def list_receipt_confirm(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items, total = psp_svc.list_transfers_for_finance(
db,
pay_status="jfqrz",
tenant_no=tenant_no,
keyword=keyword,
page=page,
page_size=page_size,
user_info=user,
due_date_start=installmentDueDateStart,
due_date_end=installmentDueDateEnd,
)
return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
except PermissionError as e:
return error_response(403, str(e))
@router.get("/payment-transfers/receipt-confirmed")
async def list_receipt_confirmed(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items, total = psp_svc.list_transfers_for_finance(
db,
pay_status="yjf",
tenant_no=tenant_no,
keyword=keyword,
page=page,
page_size=page_size,
user_info=user,
due_date_start=installmentDueDateStart,
due_date_end=installmentDueDateEnd,
)
return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
except PermissionError as e:
return error_response(403, str(e))
@router.post("/payment-transfers/{transfer_id}/receipt-confirm")
async def confirm_payment_transfer(
transfer_id: str,
body: Optional[FinanceConfirmBody] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = psp_svc.confirm_receipt(
db,
transfer_id=transfer_id,
tenant_no=tenant_no,
user_id=user_id,
finance_confirm_attachments=body.finance_confirm_attachments if body else None,
finance_remark=body.finance_remark if body else None,
)
if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
return error_response(403, "无权操作该协议")
from app.crud.protocol_sample_payment import _transfer_to_dict
return success_response(_transfer_to_dict(db, obj, tenant_no), message="确认收款成功")
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.post("/payment-transfers/{transfer_id}/receipt-reject")
async def reject_payment_transfer(
transfer_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = psp_svc.reject_receipt(
db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
)
if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
return error_response(403, "无权操作该协议")
from app.crud.protocol_sample_payment import _transfer_to_dict
return success_response(_transfer_to_dict(db, obj, tenant_no), message="已退回")
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))