| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374 |
- """细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API"""
- from datetime import datetime, timedelta
- from typing import Any, Dict, List, Optional
- from fastapi import APIRouter, Depends, HTTPException, Query
- from pydantic import BaseModel, ConfigDict, Field
- from sqlalchemy.orm import Session
- from sqlalchemy import and_ as sa_and, func as sa_func
- from app.core.database import get_db
- from app.core.data_scope import get_user_dept_id
- from app.core.menu_guard import require_admin_or_super_admin
- from app.core.response import error_response, success_response
- from app.core.utils import parse_datetime
- from app.crud import collection_protocol as cp_svc
- from app.models.models import (
- BizCollectionProtocol,
- BizProtocolPartyCustomer,
- BizProtocolSample,
- BizProtocolStorageCert,
- SysUser,
- )
- from app.services.party_customer_sync import sync_party_customers
- router = APIRouter(prefix="/web/admin", tags=["细胞存储"])
- _PAYMENT_DETAIL_SENSITIVE_FIELDS = {
- "pay_status",
- "payStatus",
- "finance_confirm_attachments",
- "financeConfirmAttachments",
- "finance_confirm_attachment_files",
- "financeConfirmAttachmentFiles",
- "finance_remark",
- "financeRemark",
- "refund_amount",
- "refundAmount",
- "refund_remark",
- "refundRemark",
- "refund_attachments",
- "refundAttachments",
- "refund_attachment_files",
- "refundAttachmentFiles",
- "refund_finance_confirm_attachments",
- "refundFinanceConfirmAttachments",
- "refund_finance_confirm_attachment_files",
- "refundFinanceConfirmAttachmentFiles",
- "refund_finance_remark",
- "refundFinanceRemark",
- }
- 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
- _CAMEL_TO_SNAKE_DATETIME = {
- "newbornDeliveryTime": "newborn_delivery_time",
- "sampleCollectTime": "sample_collect_time",
- "maternalBloodCollectTime": "maternal_blood_collect_time",
- }
- def _parse_datetime_fields(d: dict, camel_keys: list) -> None:
- for camel in camel_keys:
- val = d.get(camel)
- if val is not None and isinstance(val, str):
- snake = _CAMEL_TO_SNAKE_DATETIME.get(camel, camel)
- parsed = parse_datetime(val)
- if parsed:
- d.pop(camel, None)
- d[snake] = parsed
- else:
- d.pop(camel, None)
- d[snake] = None
- # ---------------------------------------------------------------------------
- # 采集协议单
- # ---------------------------------------------------------------------------
- @router.get("/collection-protocols")
- async def list_collection_protocols(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- protocolStatus: Optional[str] = Query(None, alias="protocolStatus"),
- createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
- createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
- sampleCollectTimeStart: Optional[str] = Query(None, alias="sampleCollectTimeStart"),
- sampleCollectTimeEnd: Optional[str] = Query(None, alias="sampleCollectTimeEnd"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = cp_svc.list_collection_protocols(
- db,
- page=page,
- page_size=pageSize,
- keyword=keyword,
- protocol_status=protocolStatus,
- create_time_start=createTimeStart,
- create_time_end=createTimeEnd,
- sample_collect_time_start=sampleCollectTimeStart,
- sample_collect_time_end=sampleCollectTimeEnd,
- tenant_no=tenant_no,
- user_info=user,
- )
- tp = cp_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/collection-protocols/{protocol_id}")
- async def get_collection_protocol_detail(
- protocol_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- if not _protocol_accessible(db, protocol_id, tenant_no, user):
- return error_response(404, "协议单不存在或无权查看")
- bundle = cp_svc.get_protocol_detail_bundle(db, protocol_id, tenant_no)
- if not bundle:
- return error_response(404, "协议单不存在或无权查看")
- return success_response(bundle)
- class ProtocolCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- protocol_no: Optional[str] = Field(None, alias="protocolNo")
- protocol_no_id: Optional[str] = Field(None, alias="protocolNoId")
- protocol_status: str = Field("dsx", alias="protocolStatus")
- protocol_attachments: Optional[str] = Field(None, alias="protocolAttachments")
- consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
- health_survey_form_urls: Optional[str] = Field(None, alias="healthSurveyFormUrls")
- register_date: Optional[str] = Field(None, alias="registerDate")
- collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
- protocol_total_amount: Optional[float] = Field(None, alias="protocolTotalAmount")
- source_presign_order_id: Optional[str] = Field(None, alias="sourcePresignOrderId")
- payment_model_version: Optional[int] = Field(2, alias="paymentModelVersion")
- remark: Optional[str] = None
- party_customer: Optional[dict] = Field(None, alias="partyCustomer")
- collection_info: Optional[dict] = Field(None, alias="collectionInfo")
- samples: Optional[list] = Field(None, alias="samples")
- payment: Optional[dict] = Field(None, alias="payment")
- payment_details: Optional[list] = Field(None, alias="paymentDetails")
- sample_payments: Optional[list] = Field(None, alias="samplePayments")
- attachments: Optional[list] = Field(None, alias="attachments")
- consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
- health_survey_attachments: Optional[list] = Field(None, alias="healthSurveyAttachments")
- storage_cert: Optional[dict] = Field(None, alias="storageCert")
- class ProtocolUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- protocol_no: Optional[str] = Field(None, alias="protocolNo")
- protocol_status: Optional[str] = Field(None, alias="protocolStatus")
- protocol_attachments: Optional[str] = Field(None, alias="protocolAttachments")
- consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
- health_survey_form_urls: Optional[str] = Field(None, alias="healthSurveyFormUrls")
- register_date: Optional[str] = Field(None, alias="registerDate")
- effective_time: Optional[str] = Field(None, alias="effectiveTime")
- effective_desc: Optional[str] = Field(None, alias="effectiveDesc")
- expire_time: Optional[str] = Field(None, alias="expireTime")
- expire_desc: Optional[str] = Field(None, alias="expireDesc")
- void_time: Optional[str] = Field(None, alias="voidTime")
- void_desc: Optional[str] = Field(None, alias="voidDesc")
- void_type: Optional[str] = Field(None, alias="voidType")
- to_research_time: Optional[str] = Field(None, alias="toResearchTime")
- to_research_desc: Optional[str] = Field(None, alias="toResearchDesc")
- collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
- protocol_total_amount: Optional[float] = Field(None, alias="protocolTotalAmount")
- source_presign_order_id: Optional[str] = Field(None, alias="sourcePresignOrderId")
- remark: Optional[str] = None
- party_customer: Optional[dict] = Field(None, alias="partyCustomer")
- collection_info: Optional[dict] = Field(None, alias="collectionInfo")
- samples: Optional[list] = Field(None, alias="samples")
- payment: Optional[dict] = Field(None, alias="payment")
- payment_details: Optional[list] = Field(None, alias="paymentDetails")
- sample_payments: Optional[list] = Field(None, alias="samplePayments")
- attachments: Optional[list] = Field(None, alias="attachments")
- consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
- health_survey_attachments: Optional[list] = Field(None, alias="healthSurveyAttachments")
- storage_cert: Optional[dict] = Field(None, alias="storageCert")
- def assert_protocol_sensitive_fields(
- db: Session,
- user: dict,
- body: BaseModel,
- *,
- creating: bool,
- ) -> None:
- """防止通过普通协议保存接口注入财务确认或退费状态字段(不做菜单权限校验)。"""
- for detail in getattr(body, "payment_details", None) or []:
- normalized_detail = (
- cp_svc._normalize_fields(detail) if isinstance(detail, dict) else {}
- )
- if _PAYMENT_DETAIL_SENSITIVE_FIELDS.intersection(normalized_detail.keys()):
- raise HTTPException(
- status_code=400,
- detail="普通协议保存不允许提交财务确认或退费状态字段",
- )
- def _resolve_protocol_attachments(
- protocol_attachments: Optional[str],
- attachments: Optional[list],
- ) -> Optional[str]:
- from_urls = cp_svc.attachments_to_urls(attachments)
- if from_urls:
- return from_urls
- return protocol_attachments
- def _resolve_consent_form_urls(
- consent_form_urls: Optional[str],
- consent_attachments: Optional[list],
- ) -> Optional[str]:
- from_urls = cp_svc.attachments_to_urls(consent_attachments)
- if from_urls:
- return from_urls
- return consent_form_urls
- def _resolve_health_survey_form_urls(
- health_survey_form_urls: Optional[str],
- health_survey_attachments: Optional[list],
- ) -> Optional[str]:
- from_urls = cp_svc.attachments_to_urls(health_survey_attachments)
- if from_urls:
- return from_urls
- return health_survey_form_urls
- def _persist_protocol_sub_resources(
- db: Session,
- protocol_id: str,
- tenant_no: str,
- user_id: str,
- *,
- party_customer: Optional[dict],
- collection_info: Optional[dict],
- samples: Optional[list],
- payment: Optional[dict],
- payment_details: Optional[list],
- sample_payments: Optional[list] = None,
- storage_cert: Optional[dict],
- payment_model_version: int = 2,
- ) -> None:
- """持久化协议子资源。
- version=1:缴费(含明细校验)须在样本之前,避免校验失败后已占用样本编码。
- version=2:样本先落库占码,再按 sample_code 挂样本缴费方案。
- """
- if party_customer:
- _parse_datetime_fields(party_customer, ["newbornDeliveryTime"])
- party_customer = sync_party_customers(
- db, party_customer, tenant_no=tenant_no, user_id=user_id
- )
- cp_svc.upsert_party_customer(db, protocol_id, tenant_no, user_id, **party_customer)
- if collection_info:
- _parse_datetime_fields(collection_info, ["sampleCollectTime", "maternalBloodCollectTime"])
- cp_svc.upsert_collection_info(db, protocol_id, tenant_no, user_id, **collection_info)
- version = int(payment_model_version or 2)
- if version >= 2:
- if samples is not None:
- cp_svc.save_samples(db, protocol_id, tenant_no, user_id, samples)
- if sample_payments is not None:
- from app.crud import protocol_sample_payment as sp_pay_svc
- sp_pay_svc.save_sample_payments_for_protocol(
- db,
- collection_protocol_id=protocol_id,
- tenant_no=tenant_no,
- user_id=user_id,
- sample_payments=sample_payments,
- )
- else:
- # 旧模型:缴费先于样本
- if payment or payment_details is not None:
- cp_svc.save_payment_with_details(
- db,
- protocol_id,
- tenant_no,
- user_id,
- payment=payment,
- payment_details=payment_details,
- )
- if samples is not None:
- cp_svc.save_samples(db, protocol_id, tenant_no, user_id, samples)
- if storage_cert:
- cp_svc.upsert_storage_cert(db, protocol_id, tenant_no, user_id, **storage_cert)
- @router.post("/collection-protocols")
- async def create_collection_protocol(
- body: ProtocolCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- assert_protocol_sensitive_fields(db, user, body, creating=True)
- tenant_no = _get_tenant_no(db, user)
- user_id = _get_current_user_id(user)
- protocol_id: Optional[str] = None
- try:
- protocol_attachments = _resolve_protocol_attachments(
- body.protocol_attachments,
- body.attachments,
- )
- consent_form_urls = _resolve_consent_form_urls(
- body.consent_form_urls,
- body.consent_attachments,
- )
- health_survey_form_urls = _resolve_health_survey_form_urls(
- body.health_survey_form_urls,
- body.health_survey_attachments,
- )
- collector = (body.collector_user_id or "").strip()
- if not collector:
- collector = user_id
- obj = cp_svc.create_collection_protocol(
- db,
- create_dept_id=get_user_dept_id(db, user_id) or "",
- protocol_no=body.protocol_no or "",
- protocol_no_id=body.protocol_no_id,
- protocol_status=body.protocol_status,
- protocol_attachments=protocol_attachments,
- consent_form_urls=consent_form_urls,
- health_survey_form_urls=health_survey_form_urls,
- register_date=body.register_date,
- collector_user_id=collector,
- protocol_total_amount=body.protocol_total_amount,
- source_presign_order_id=body.source_presign_order_id,
- payment_model_version=body.payment_model_version
- if body.payment_model_version is not None
- else 2,
- remark=body.remark,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- protocol_id = obj.id
- _persist_protocol_sub_resources(
- db,
- protocol_id,
- tenant_no,
- user_id,
- party_customer=body.party_customer,
- collection_info=body.collection_info,
- samples=body.samples,
- payment=body.payment,
- payment_details=body.payment_details,
- sample_payments=body.sample_payments,
- storage_cert=body.storage_cert,
- payment_model_version=int(
- getattr(obj, "payment_model_version", None) or 2
- ),
- )
- return success_response(cp_svc._protocol_to_dict(obj), message="创建成功")
- except ValueError as e:
- # 子表分段 commit 后校验失败时,补偿软删主单及样本,释放样本编码占用
- if protocol_id:
- try:
- cp_svc.soft_delete_collection_protocol(
- db,
- protocol_id,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- except Exception:
- pass
- return error_response(400, str(e))
- @router.put("/collection-protocols/{protocol_id}")
- async def update_collection_protocol(
- protocol_id: str,
- body: ProtocolUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- assert_protocol_sensitive_fields(db, user, body, creating=False)
- 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, "协议单不存在或无权操作")
- try:
- payload = body.model_dump(exclude_unset=True, by_alias=False)
- sub_keys = {
- "party_customer", "collection_info", "samples",
- "payment", "payment_details", "sample_payments", "attachments",
- "consent_attachments", "health_survey_attachments", "storage_cert",
- }
- main_payload = {k: v for k, v in payload.items() if k not in sub_keys}
- if body.attachments is not None:
- resolved = _resolve_protocol_attachments(
- main_payload.get("protocol_attachments"),
- body.attachments,
- )
- if resolved is not None:
- main_payload["protocol_attachments"] = resolved
- if body.consent_attachments is not None:
- resolved = _resolve_consent_form_urls(
- main_payload.get("consent_form_urls"),
- body.consent_attachments,
- )
- if resolved is not None:
- main_payload["consent_form_urls"] = resolved
- if body.health_survey_attachments is not None:
- resolved = _resolve_health_survey_form_urls(
- main_payload.get("health_survey_form_urls"),
- body.health_survey_attachments,
- )
- if resolved is not None:
- main_payload["health_survey_form_urls"] = resolved
- obj = cp_svc.update_collection_protocol(
- db,
- protocol_id,
- tenant_no=tenant_no,
- user_id=user_id,
- **{
- k: v
- for k, v in main_payload.items()
- if v is not None or k == "protocol_total_amount"
- },
- )
- _persist_protocol_sub_resources(
- db,
- protocol_id,
- tenant_no,
- user_id,
- party_customer=body.party_customer,
- collection_info=body.collection_info,
- samples=body.samples,
- payment=body.payment,
- payment_details=body.payment_details,
- sample_payments=body.sample_payments,
- storage_cert=body.storage_cert,
- payment_model_version=int(
- getattr(obj, "payment_model_version", None) or 1
- ),
- )
- return success_response(cp_svc._protocol_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/collection-protocols/{protocol_id}")
- async def delete_collection_protocol(
- protocol_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- 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, "协议单不存在或无权操作")
- try:
- cp_svc.soft_delete_collection_protocol(
- db,
- protocol_id,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 审核
- # ---------------------------------------------------------------------------
- @router.get("/collection-protocol-audits")
- async def list_collection_protocol_audits(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- auditStatus: Optional[str] = Query(None, alias="auditStatus"),
- collectionProtocolId: Optional[str] = Query(None, alias="collectionProtocolId"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = cp_svc.list_audits(
- db,
- page=page,
- page_size=pageSize,
- keyword=keyword,
- audit_status=auditStatus,
- collection_protocol_id=collectionProtocolId,
- tenant_no=tenant_no,
- user_info=user,
- )
- tp = cp_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- class AuditCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- audit_status: str = Field(..., alias="auditStatus")
- audit_remark: Optional[str] = Field(None, alias="auditRemark")
- @router.post("/collection-protocols/{protocol_id}/audit")
- async def submit_collection_protocol_audit(
- protocol_id: str,
- body: AuditCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- if not _protocol_accessible(db, protocol_id, tenant_no, user):
- return error_response(403, "无权访问该协议或协议不存在")
- try:
- cp_svc.create_audit(
- db,
- collection_protocol_id=protocol_id,
- audit_status=body.audit_status,
- audit_remark=body.audit_remark,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- )
- p = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.id == protocol_id).first()
- pn = (p.protocol_no if p else "") or ""
- return success_response({"protocolNo": pn}, message="审核已提交")
- except ValueError as e:
- return error_response(400, str(e))
- @router.get("/collection-protocols/{protocol_id}/status-changes")
- async def list_collection_protocol_status_changes(
- protocol_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- if not _protocol_accessible(db, protocol_id, tenant_no, user):
- return error_response(403, "无权访问该协议或协议不存在")
- items = cp_svc.list_protocol_status_changes(
- db,
- collection_protocol_id=protocol_id,
- tenant_no=tenant_no,
- )
- return success_response({"list": items})
- # ---------------------------------------------------------------------------
- # 收款确认
- # ---------------------------------------------------------------------------
- @router.get("/protocol-payment-details")
- async def list_payment_details(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- payStatus: str = Query("dfj", alias="payStatus"),
- keyword: str = Query("", alias="keyword"),
- transferType: str = Query("", alias="transferType"),
- 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),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = cp_svc.list_payment_details_page(
- db,
- page=page,
- page_size=pageSize,
- pay_status=payStatus,
- keyword=keyword or None,
- transfer_type=transferType or None,
- installment_due_date_start=installmentDueDateStart,
- installment_due_date_end=installmentDueDateEnd,
- tenant_no=tenant_no,
- user_info=user,
- )
- tp = cp_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- class BatchDetailIdsBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- detail_ids: List[str] = Field(..., alias="detailIds", min_length=1)
- class PaymentDetailDraftBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- transfer_type: Optional[str] = Field(None, alias="transferType")
- transfer_amount: Optional[float] = Field(None, alias="transferAmount")
- transfer_time: Optional[str] = Field(None, alias="transferTime")
- transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
- remark: Optional[str] = None
- def _draft_fields_from_body(body: PaymentDetailDraftBody) -> dict:
- return body.model_dump(exclude_unset=True)
- @router.put("/protocol-payment-details/{detail_id}")
- async def put_payment_detail_draft(
- detail_id: str,
- body: PaymentDetailDraftBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- fields = _draft_fields_from_body(body)
- if not fields:
- return error_response(400, "请提供需要保存的字段")
- try:
- cp_svc.update_payment_detail_draft(
- db,
- detail_id=detail_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- draft_fields=fields,
- )
- return success_response(None, message="保存成功")
- except ValueError as e:
- return error_response(400, str(e))
- class RefundFinanceSubmitBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- refund_amount: float = Field(..., alias="refundAmount", gt=0)
- refund_remark: Optional[str] = Field(None, alias="refundRemark")
- refund_attachments: Optional[str] = Field(None, alias="refundAttachments")
- class BatchRefundFinanceSubmitBody(RefundFinanceSubmitBody):
- model_config = ConfigDict(populate_by_name=True)
- detail_ids: List[str] = Field(..., alias="detailIds", min_length=1)
- @router.post("/protocol-payment-details/{detail_id}/submit-receipt-finance")
- async def post_submit_receipt_finance(
- detail_id: str,
- body: PaymentDetailDraftBody = PaymentDetailDraftBody(),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- fields = _draft_fields_from_body(body)
- try:
- cp_svc.submit_receipt_finance(
- db,
- detail_id=detail_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- draft_fields=fields or None,
- )
- return success_response(None, message="提交成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/protocol-payment-details/submit-receipt-finance-batch")
- async def post_submit_receipt_finance_batch(
- body: BatchDetailIdsBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- count = cp_svc.batch_submit_receipt_finance(
- db,
- detail_ids=body.detail_ids,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- )
- return success_response({"count": count}, message=f"已成功提交 {count} 条")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/protocol-payment-details/{detail_id}/submit-refund-finance")
- async def post_submit_refund_finance(
- detail_id: str,
- body: RefundFinanceSubmitBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- cp_svc.submit_refund_finance(
- db,
- detail_id=detail_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- refund_amount=body.refund_amount,
- refund_remark=body.refund_remark,
- refund_attachments=body.refund_attachments,
- )
- return success_response(None, message="提交成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/protocol-payment-details/submit-refund-finance-batch")
- async def post_submit_refund_finance_batch(
- body: BatchRefundFinanceSubmitBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- count = cp_svc.batch_submit_refund_finance(
- db,
- detail_ids=body.detail_ids,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- refund_amount=body.refund_amount,
- refund_remark=body.refund_remark,
- refund_attachments=body.refund_attachments,
- )
- return success_response({"count": count}, message=f"已成功提交 {count} 条")
- except ValueError as e:
- return error_response(400, str(e))
- @router.get("/protocol-payment-details/receipt-confirm")
- async def list_receipt_confirm(
- page: int = Query(1, ge=1),
- pageSize: 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),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = cp_svc.list_receipt_confirm_details(
- db,
- page=page,
- page_size=pageSize,
- keyword=keyword or None,
- installment_due_date_start=installmentDueDateStart,
- installment_due_date_end=installmentDueDateEnd,
- tenant_no=tenant_no,
- user_info=user,
- )
- tp = cp_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/protocol-payment-details/receipt-confirmed")
- async def list_receipt_confirmed(
- page: int = Query(1, ge=1),
- pageSize: 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),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = cp_svc.list_receipt_confirmed_details(
- db,
- page=page,
- page_size=pageSize,
- keyword=keyword or None,
- installment_due_date_start=installmentDueDateStart,
- installment_due_date_end=installmentDueDateEnd,
- tenant_no=tenant_no,
- user_info=user,
- )
- tp = cp_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- class FinanceConfirmBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- finance_confirm_attachments: Optional[str] = Field(None, alias="financeConfirmAttachments")
- finance_remark: Optional[str] = Field(None, alias="financeRemark")
- class RefundFinanceConfirmBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- refund_finance_confirm_attachments: Optional[str] = Field(
- None, alias="refundFinanceConfirmAttachments"
- )
- refund_finance_remark: Optional[str] = Field(None, alias="refundFinanceRemark")
- @router.post("/protocol-payment-details/{detail_id}/receipt-confirm")
- async def post_receipt_confirm(
- detail_id: str,
- body: FinanceConfirmBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- cp_svc.confirm_receipt(
- db,
- detail_id=detail_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- finance_confirm_attachments=body.finance_confirm_attachments,
- finance_remark=body.finance_remark,
- )
- return success_response(None, message="确认成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/protocol-payment-details/{detail_id}/receipt-reject")
- async def post_receipt_reject(
- detail_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- cp_svc.reject_receipt(
- db,
- detail_id=detail_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- )
- return success_response(None, message="退回成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 退费确认
- # ---------------------------------------------------------------------------
- @router.get("/protocol-payment-details/refund-confirm")
- async def list_refund_confirm(
- page: int = Query(1, ge=1),
- pageSize: 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),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = cp_svc.list_refund_confirm_details(
- db,
- page=page,
- page_size=pageSize,
- keyword=keyword or None,
- installment_due_date_start=installmentDueDateStart,
- installment_due_date_end=installmentDueDateEnd,
- tenant_no=tenant_no,
- user_info=user,
- )
- tp = cp_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/protocol-payment-details/refund-confirmed")
- async def list_refund_confirmed(
- page: int = Query(1, ge=1),
- pageSize: 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),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = cp_svc.list_refund_confirmed_details(
- db,
- page=page,
- page_size=pageSize,
- keyword=keyword or None,
- installment_due_date_start=installmentDueDateStart,
- installment_due_date_end=installmentDueDateEnd,
- tenant_no=tenant_no,
- user_info=user,
- )
- tp = cp_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.post("/protocol-payment-details/{detail_id}/refund-confirm")
- async def post_refund_confirm(
- detail_id: str,
- body: RefundFinanceConfirmBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- cp_svc.confirm_refund(
- db,
- detail_id=detail_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- refund_finance_confirm_attachments=body.refund_finance_confirm_attachments,
- refund_finance_remark=body.refund_finance_remark,
- )
- return success_response(None, message="确认成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/protocol-payment-details/{detail_id}/refund-reject")
- async def post_refund_reject(
- detail_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- cp_svc.reject_refund(
- db,
- detail_id=detail_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- )
- return success_response(None, message="退回成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 储存证明管理 / 满月待办
- # ---------------------------------------------------------------------------
- @router.get("/storage-certs")
- async def list_storage_certs(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- makeStatus: Optional[str] = Query(None, alias="makeStatus"),
- storageInDateStart: Optional[str] = Query(None, alias="storageInDateStart"),
- storageInDateEnd: Optional[str] = Query(None, alias="storageInDateEnd"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- page_items, total = cp_svc.list_storage_certs(
- db,
- tenant_no=tenant_no,
- keyword=keyword,
- make_status=makeStatus,
- storage_in_date_start=storageInDateStart,
- storage_in_date_end=storageInDateEnd,
- page=page,
- page_size=pageSize,
- user_info=user,
- )
- page_safe = max(1, page)
- page_size_safe = min(max(1, pageSize), 100)
- tp = cp_svc.total_pages(total, page_size_safe)
- return success_response(
- {"list": page_items, "total": total, "page": page_safe, "pageSize": page_size_safe, "totalPages": tp}
- )
- @router.get("/fullmoon-todos")
- async def list_fullmoon_todos(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- todoStatus: Optional[str] = Query(None, alias="todoStatus"),
- fullmoonDateStart: Optional[str] = Query(None, alias="fullmoonDateStart"),
- fullmoonDateEnd: Optional[str] = Query(None, alias="fullmoonDateEnd"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- party_join = sa_and(
- BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
- BizProtocolPartyCustomer.del_flag == 0,
- BizProtocolPartyCustomer.tenant_no == tenant_no,
- )
- q = (
- db.query(BizCollectionProtocol, BizProtocolPartyCustomer)
- .outerjoin(BizProtocolPartyCustomer, party_join)
- .filter(
- BizCollectionProtocol.del_flag == 0,
- BizCollectionProtocol.tenant_no == tenant_no,
- BizCollectionProtocol.protocol_status == "sxz",
- )
- )
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q = q.filter(BizProtocolPartyCustomer.mother_name.like(like))
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no)
- rows = q.order_by(BizCollectionProtocol.create_time.desc()).all()
- protocol_ids = [r.id for r, _ in rows]
- ft_map: Dict[str, Any] = {}
- if protocol_ids:
- from app.models.models import BizProtocolFullmoonTodo
- ft_rows = (
- db.query(BizProtocolFullmoonTodo)
- .filter(
- BizProtocolFullmoonTodo.collection_protocol_id.in_(protocol_ids),
- BizProtocolFullmoonTodo.tenant_no == tenant_no,
- BizProtocolFullmoonTodo.del_flag == 0,
- )
- .all()
- )
- for ft in ft_rows:
- ft_map[ft.collection_protocol_id] = ft
- now = datetime.now()
- _empty_ft: Dict[str, Any] = {
- "todoStatus": "0",
- "handleDate": "",
- "handleRemark": "",
- "insuranceAttachments": "",
- "insuranceCompanyId": "",
- "insuranceCompanyName": "",
- "insuranceType": "",
- "insurancePolicyNo": "",
- "insuranceEffectiveDate": "",
- "insuranceExpiryDate": "",
- "insuranceSubscribed": "0",
- "insuranceFee": None,
- }
- items: List[Dict[str, Any]] = []
- for proto, pc in rows:
- dt = pc.newborn_delivery_time if pc else None
- fullmoon_date_str = ""
- is_fullmoon_passed = False
- if dt:
- fullmoon_date = dt + timedelta(days=30)
- fullmoon_date_str = fullmoon_date.strftime("%Y-%m-%d")
- is_fullmoon_passed = fullmoon_date <= now
- ft = ft_map.get(proto.id)
- ts = (ft.todo_status or "0") if ft else "0"
- if todoStatus and todoStatus.strip() and ts != todoStatus.strip():
- continue
- fm_start = (fullmoonDateStart or "").strip()[:10]
- fm_end = (fullmoonDateEnd or "").strip()[:10]
- if fm_start or fm_end:
- if not fullmoon_date_str:
- continue
- if fm_start and fullmoon_date_str < fm_start:
- continue
- if fm_end and fullmoon_date_str > fm_end:
- continue
- ft_dict = cp_svc._fullmoon_todo_to_dict(ft) if ft else dict(_empty_ft)
- items.append(
- {
- "protocolId": proto.id,
- "id": proto.id,
- "protocolNo": proto.protocol_no or "",
- "protocolStatus": proto.protocol_status or "",
- "motherName": (pc.mother_name if pc else "") or "",
- "motherPhone": (pc.mother_phone if pc else "") or "",
- "newbornName": (pc.newborn_name if pc else "") or "",
- "newbornDeliveryTime": cp_svc._fmt_dt(dt) if dt else "",
- "fullmoonDate": fullmoon_date_str,
- "isFullmoonPassed": is_fullmoon_passed,
- **ft_dict,
- }
- )
- total = len(items)
- page_safe = max(1, page)
- page_size_safe = min(max(1, pageSize), 100)
- start = (page_safe - 1) * page_size_safe
- page_items = items[start : start + page_size_safe]
- tp = cp_svc.total_pages(total, page_size_safe)
- return success_response(
- {"list": page_items, "total": total, "page": page_safe, "pageSize": page_size_safe, "totalPages": tp}
- )
- class FullmoonTodoInsuranceBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- insurance_company_id: Optional[str] = Field(None, alias="insuranceCompanyId")
- insurance_company_name: Optional[str] = Field(None, alias="insuranceCompanyName")
- insurance_type: Optional[str] = Field(None, alias="insuranceType")
- insurance_policy_no: Optional[str] = Field(None, alias="insurancePolicyNo")
- insurance_effective_date: Optional[str] = Field(None, alias="insuranceEffectiveDate")
- insurance_expiry_date: Optional[str] = Field(None, alias="insuranceExpiryDate")
- insurance_subscribed: Optional[str] = Field(None, alias="insuranceSubscribed")
- insurance_fee: Optional[float] = Field(None, alias="insuranceFee")
- insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
- todo_status: Optional[str] = Field(None, alias="todoStatus")
- handle_date: Optional[str] = Field(None, alias="handleDate")
- handle_remark: Optional[str] = Field(None, alias="handleRemark")
- @router.put("/fullmoon-todos/{protocol_id}/insurance")
- async def update_fullmoon_todo_insurance(
- protocol_id: str,
- body: FullmoonTodoInsuranceBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- user_id = _get_current_user_id(user)
- try:
- payload = body.model_dump(exclude_unset=True, by_alias=False)
- ft = cp_svc.upsert_fullmoon_todo(
- db,
- protocol_id,
- tenant_no,
- user_id,
- **payload,
- )
- return success_response(cp_svc._fullmoon_todo_to_dict(ft), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- class StorageCertHandleBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- handle_time: Optional[str] = Field(None, alias="handleTime")
- handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
- cert_image_urls: Optional[str] = Field(None, alias="certImageUrls")
- remark: Optional[str] = None
- class FullmoonHandleBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- handle_time: Optional[str] = Field(None, alias="handleTime")
- handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
- insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
- handle_remark: Optional[str] = Field(None, alias="handleRemark")
- def _attach_handler_name(db: Session, payload: Dict[str, Any]) -> Dict[str, Any]:
- uid = (payload.get("handlerUserId") or "").strip()
- name = ""
- if uid:
- u = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
- name = (u.nick_name or "") if u else ""
- payload["handlerUserName"] = name
- return payload
- @router.put("/storage-certs/{protocol_id}")
- async def update_storage_cert_handle(
- protocol_id: str,
- body: StorageCertHandleBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- 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, "协议单不存在")
- try:
- ht = parse_datetime(body.handle_time) if body.handle_time else None
- sc = cp_svc.upsert_storage_cert_handle(
- db,
- protocol_id,
- tenant_no,
- user_id,
- handle_time=ht,
- handler_user_id=body.handler_user_id,
- cert_image_urls=body.cert_image_urls,
- remark=body.remark,
- )
- return success_response(
- _attach_handler_name(db, cp_svc._storage_cert_to_dict(sc)), message="更新成功"
- )
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/fullmoon-todos/{protocol_id}/handle")
- async def update_fullmoon_todo_handle(
- protocol_id: str,
- body: FullmoonHandleBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- 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, "协议单不存在")
- try:
- ht = parse_datetime(body.handle_time) if body.handle_time else None
- ft = cp_svc.upsert_fullmoon_todo_handle(
- db,
- protocol_id,
- tenant_no,
- user_id,
- handle_time=ht,
- handler_user_id=body.handler_user_id,
- insurance_attachments=body.insurance_attachments,
- handle_remark=body.handle_remark,
- )
- return success_response(
- _attach_handler_name(db, cp_svc._fullmoon_todo_to_dict(ft)), message="更新成功"
- )
- except ValueError as e:
- return error_response(400, str(e))
- class InvoiceRequestCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- payment_detail_ids: List[str] = Field(..., alias="paymentDetailIds", min_length=1)
- invoice_title: Optional[str] = Field(None, alias="invoiceTitle")
- tax_no: Optional[str] = Field(None, alias="taxNo")
- company_address: Optional[str] = Field(None, alias="companyAddress")
- company_phone: Optional[str] = Field(None, alias="companyPhone")
- bank_name: Optional[str] = Field(None, alias="bankName")
- bank_account: Optional[str] = Field(None, alias="bankAccount")
- apply_remark: Optional[str] = Field(None, alias="applyRemark")
- class InvoiceRequestCompleteBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- invoice_attachments: str = Field(..., alias="invoiceAttachments")
- finance_remark: Optional[str] = Field(None, alias="financeRemark")
- @router.get("/collection-protocols/{protocol_id}/invoice-eligible-payments")
- async def get_invoice_eligible_payments(
- protocol_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items = cp_svc.list_invoice_eligible_payment_details(
- db, collection_protocol_id=protocol_id, tenant_no=tenant_no
- )
- return success_response({"list": items, "total": len(items)})
- @router.get("/collection-protocols/{protocol_id}/invoice-requests")
- async def get_protocol_invoice_requests(
- protocol_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items = cp_svc.list_invoice_requests_by_protocol(
- db, collection_protocol_id=protocol_id, tenant_no=tenant_no
- )
- return success_response({"list": items, "total": len(items)})
- @router.post("/collection-protocols/{protocol_id}/invoice-requests")
- async def post_protocol_invoice_request(
- protocol_id: str,
- body: InvoiceRequestCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- row = cp_svc.create_invoice_request(
- db,
- collection_protocol_id=protocol_id,
- payment_detail_ids=body.payment_detail_ids,
- tenant_no=tenant_no,
- user_info=user,
- invoice_title=body.invoice_title,
- tax_no=body.tax_no,
- company_address=body.company_address,
- company_phone=body.company_phone,
- bank_name=body.bank_name,
- bank_account=body.bank_account,
- apply_remark=body.apply_remark,
- )
- except ValueError as e:
- return error_response(400, str(e))
- return success_response(row, message="提交成功")
- @router.get("/protocol-invoice-requests")
- async def list_protocol_invoice_requests_api(
- status: str = Query(..., description="dkp|ykp"),
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100),
- keyword: Optional[str] = Query(None),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = cp_svc.list_protocol_invoice_requests(
- db,
- status=status,
- page=page,
- page_size=pageSize,
- keyword=keyword,
- tenant_no=tenant_no,
- user_info=user,
- )
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize})
- @router.get("/protocol-invoice-requests/pending-count")
- async def protocol_invoice_pending_count(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- count = cp_svc.count_pending_protocol_invoice_requests(
- db, tenant_no=tenant_no, user_info=user
- )
- return success_response({"count": count})
- @router.put("/protocol-invoice-requests/{request_id}/complete")
- async def complete_protocol_invoice_request(
- request_id: str,
- body: InvoiceRequestCompleteBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- try:
- row = cp_svc.complete_invoice_request(
- db,
- request_id=request_id,
- tenant_no=tenant_no,
- invoice_attachments=body.invoice_attachments,
- finance_remark=body.finance_remark,
- user_info=user,
- )
- except ValueError as e:
- return error_response(400, str(e))
- return success_response(row, message="已完成开票")
|