432 lines
16 KiB
Python
432 lines
16 KiB
Python
"""按样本缴费方案 / 转账笔财务确认 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:
|
||
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,
|
||
)
|
||
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("/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.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),
|
||
keyword: Optional[str] = None,
|
||
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,
|
||
)
|
||
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),
|
||
keyword: Optional[str] = None,
|
||
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,
|
||
)
|
||
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))
|