| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220 |
- """细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API"""
- from datetime import datetime, timedelta
- 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 sqlalchemy import and_ as sa_and, func as sa_func
- 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.core.utils import parse_datetime
- from app.crud import collection_protocol as cp_svc
- from app.crud import customer as customer_crud
- from app.models.models import (
- BizCollectionProtocol,
- BizCustomer,
- BizProtocolPartyCustomer,
- BizProtocolSample,
- BizProtocolStorageCert,
- 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
- _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
- def _ensure_customer_for_mother(db: Session, party_customer: dict, tenant_no: str, user_id: str) -> dict:
- """If mother info exists but no customerId, create a customer record and link it."""
- if not party_customer:
- return party_customer
- customer_id = party_customer.get("customer_id") or party_customer.get("customerId")
- mother_name = (party_customer.get("mother_name") or party_customer.get("motherName") or "").strip()
- if customer_id or not mother_name:
- return party_customer
- # Build customer fields from mother info
- mother_phone = (party_customer.get("mother_phone") or party_customer.get("motherPhone") or "").strip() or None
- mother_id_card = (party_customer.get("mother_id_card") or party_customer.get("motherIdCard") or "").strip() or None
- # Check if customer with this id_card already exists
- if mother_id_card and customer_crud.id_card_taken(db, mother_id_card, tenant_no):
- existing = db.query(BizCustomer).filter(
- sa_and(
- BizCustomer.id_card_no == mother_id_card,
- BizCustomer.tenant_no == tenant_no,
- BizCustomer.del_flag == 0,
- )
- ).first()
- if existing:
- party_customer["customer_id"] = existing.id
- return party_customer
- # Create new customer
- try:
- obj = customer_crud.create_customer(
- db,
- customer_name=mother_name,
- sex="1", # female (mother)
- id_card_no=mother_id_card,
- phone_no=mother_phone,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- party_customer["customer_id"] = obj.id
- except ValueError:
- # If creation fails (e.g. duplicate), try to find existing
- pass
- return party_customer
- # ---------------------------------------------------------------------------
- # 采集协议单
- # ---------------------------------------------------------------------------
- @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")
- 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")
- 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")
- 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")
- 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 _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],
- storage_cert: Optional[dict],
- ) -> None:
- """持久化协议子资源。
- 缴费(含明细校验)须在样本之前:明细校验失败时不得已占用样本编码。
- 各子表 CRUD 内部仍可能分段 commit,创建接口须在失败时做补偿软删。
- """
- if party_customer:
- _parse_datetime_fields(party_customer, ["newbornDeliveryTime"])
- party_customer = _ensure_customer_for_mother(db, party_customer, tenant_no, 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)
- # 缴费先于样本:避免「应缴合计不符」等校验失败后样本编码已被占用
- pay = None
- if payment:
- pay = cp_svc.upsert_payment(db, protocol_id, tenant_no, user_id, **payment)
- if payment_details is not None:
- if not pay and payment:
- pay = cp_svc.upsert_payment(db, protocol_id, tenant_no, user_id, **payment)
- if not pay:
- pay = cp_svc.get_payment_by_protocol_id(db, protocol_id, tenant_no)
- if pay:
- aligned_details = cp_svc.align_fq_payment_details_payload(pay, payment_details)
- cp_svc.save_payment_details(db, pay.id, tenant_no, user_id, aligned_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),
- ):
- 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,
- 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,
- 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,
- storage_cert=body.storage_cert,
- )
- 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),
- ):
- 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", "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},
- )
- _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,
- storage_cert=body.storage_cert,
- )
- 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)
- 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("/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 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="已完成开票")
|