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