Files
zkxw/backend/app/api/collection_protocol.py
T
2026-06-11 15:42:58 +08:00

920 lines
34 KiB
Python

"""细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 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)
_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)
current_user_id = _get_current_user_id(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,
scope_collector_user_id=current_user_id,
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)
current_user_id = _get_current_user_id(user)
bundle = cp_svc.get_protocol_detail_bundle(
db, protocol_id, tenant_no, scope_collector_user_id=current_user_id
)
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")
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")
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")
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")
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")
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 _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:
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)
if samples is not None:
cp_svc.save_samples(db, protocol_id, tenant_no, user_id, samples)
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 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 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)
try:
protocol_attachments = _resolve_protocol_attachments(
body.protocol_attachments,
body.attachments,
)
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,
register_date=body.register_date,
collector_user_id=user_id,
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:
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)
try:
payload = body.model_dump(exclude_unset=True, by_alias=False)
sub_keys = {
"party_customer", "collection_info", "samples",
"payment", "payment_details", "attachments", "storage_cert",
}
main_payload = {k: v for k, v in payload.items() if k not in sub_keys}
main_payload.pop("collector_user_id", None)
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
obj = cp_svc.update_collection_protocol(
db,
protocol_id,
tenant_no=tenant_no,
user_id=user_id,
scope_collector_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)
try:
cp_svc.soft_delete_collection_protocol(
db,
protocol_id,
tenant_no=tenant_no,
scope_collector_user_id=user_id,
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"),
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,
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 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,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
try:
cp_svc.submit_receipt_finance(
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.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.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.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))