922 lines
34 KiB
Python
922 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"),
|
|
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 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))
|
|
|