| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646 |
- """按样本缴费方案 / 转账笔财务确认 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]
- psp_svc.sync_schedules_and_transfers(
- db,
- sample_payment_id=sp.id,
- tenant_no=tenant_no,
- user_id=user_id,
- schedules=schedule_payloads,
- )
- 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)
- locked_transfer = psp_svc._get_transfer_with_schedule_lock(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_info=user,
- )
- obj = psp_svc.update_transfer_draft(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_id=user_id,
- fields=fields,
- locked_transfer=locked_transfer,
- )
- 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
- locked_transfer = psp_svc._get_transfer_with_schedule_lock(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_info=user,
- )
- obj = psp_svc.submit_receipt_finance(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_id=user_id,
- draft_fields=draft,
- locked_transfer=locked_transfer,
- )
- 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)
- locked_transfer = psp_svc._get_transfer_with_schedule_lock(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_info=user,
- )
- obj = psp_svc.withdraw_receipt(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_id=user_id,
- locked_transfer=locked_transfer,
- )
- 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.delete("/payment-transfers/{transfer_id}")
- async def delete_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._get_transfer_or_raise(
- db, transfer_id=transfer_id, tenant_no=tenant_no
- )
- if not _protocol_accessible(db, obj.collection_protocol_id, tenant_no, user):
- return error_response(403, "无权操作该协议")
- psp_svc.soft_delete_payment_transfer(
- db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
- )
- return success_response(None, 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)
- locked_transfer = psp_svc._get_transfer_with_schedule_lock(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_info=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,
- locked_transfer=locked_transfer,
- )
- 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)
- locked_transfer = psp_svc._get_transfer_with_schedule_lock(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_info=user,
- )
- obj = psp_svc.reject_receipt(
- db,
- transfer_id=transfer_id,
- tenant_no=tenant_no,
- user_id=user_id,
- locked_transfer=locked_transfer,
- )
- 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))
|