This commit is contained in:
zlwu committed 2026-05-17 12:31:11 +08:00
1 parent e5ba8f097a
commit 62cfb8fbbf
24 files changed
+3323 -249

No files matched your search

+1 -1
View File
@@ -1 +1 @@
25041
68508
+171 -26
View File
@@ -409,6 +409,133 @@ async def submit_collection_protocol_audit(
# ---------------------------------------------------------------------------
@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"),
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,
tenant_no=tenant_no,
)
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),
@@ -422,10 +549,35 @@ async def list_receipt_confirm(
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"),
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, tenant_no=tenant_no
)
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")
@@ -443,6 +595,7 @@ async def post_receipt_confirm(
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:
@@ -467,10 +620,25 @@ async def list_refund_confirm(
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"),
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, tenant_no=tenant_no
)
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: FinanceConfirmBody,
body: RefundFinanceConfirmBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
@@ -481,33 +649,10 @@ async def post_refund_confirm(
detail_id=detail_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
finance_confirm_attachments=body.finance_confirm_attachments,
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("/protocol-payment-details/installment-warning")
async def list_installment_warning(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
warningType: str = Query("all", alias="warningType"),
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_installment_warning(
db,
page=page,
page_size=pageSize,
warning_type=warningType,
tenant_no=tenant_no,
)
tp = cp_svc.total_pages(total, pageSize)
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
+9 -9
View File
@@ -166,6 +166,13 @@ STATIC_MENU_TREE = [
"component": "/cell-storage/protocol/index",
"meta": {"title": "采集协议单", "keepAlive": True},
},
{
"id": "50000000000000000000000000000007",
"name": "CellStoragePaymentInfo",
"path": "payment-info",
"component": "/cell-storage/payment-info/index",
"meta": {"title": "缴费信息", "keepAlive": True},
},
{
"id": "50000000000000000000000000000003",
"name": "CellStorageProtocolAudit",
@@ -178,21 +185,14 @@ STATIC_MENU_TREE = [
"name": "CellStoragePaymentConfirm",
"path": "payment-confirm",
"component": "/cell-storage/payment-confirm/index",
"meta": {"title": "采集协议单收款确认(财务确认)", "keepAlive": True},
"meta": {"title": "缴费确认(财务)", "keepAlive": True},
},
{
"id": "50000000000000000000000000000005",
"name": "CellStorageRefundConfirm",
"path": "refund-confirm",
"component": "/cell-storage/refund-confirm/index",
"meta": {"title": "采集协议单退费确认(财务确认)", "keepAlive": True},
},
{
"id": "50000000000000000000000000000006",
"name": "CellStorageInstallmentWarning",
"path": "installment-warning",
"component": "/cell-storage/installment-warning/index",
"meta": {"title": "分期缴费用户预警模块", "keepAlive": True},
"meta": {"title": "退费确认(财务)", "keepAlive": True},
},
],
},
+408 -25
View File
@@ -178,6 +178,14 @@ def urls_to_attachment_list(
return result
def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
ids = list({x for x in user_ids if x})
if not ids:
return {}
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
return {u.id: u.nick_name or "" for u in rows}
def _protocol_to_dict(p: BizCollectionProtocol) -> Dict[str, Any]:
return {
"id": p.id,
@@ -226,21 +234,39 @@ def _detail_to_dict(
*,
protocol_no: str = "",
collection_protocol_id: str = "",
protocol_status: str = "",
pay_mode: str = "",
mother_name: str = "",
mother_id_card: str = "",
newborn_name: str = "",
newborn_id_card: str = "",
) -> Dict[str, Any]:
return {
"id": d.id,
"paymentId": d.payment_id,
"collectionProtocolId": collection_protocol_id,
"protocolNo": protocol_no,
"protocolStatus": protocol_status,
"payMode": pay_mode,
"motherName": mother_name,
"motherIdCard": mother_id_card,
"newbornName": newborn_name,
"newbornIdCard": newborn_id_card,
"transferType": d.transfer_type or "",
"dueAmount": _dec(d.due_amount),
"transferAmount": _dec(d.transfer_amount),
"transferTime": _fmt_dt(d.transfer_time),
"transferAttachments": d.transfer_attachments or "",
"financeConfirmAttachments": d.finance_confirm_attachments or "",
"financeRemark": d.finance_remark or "",
"refundFinanceConfirmAttachments": d.refund_finance_confirm_attachments or "",
"refundFinanceRemark": d.refund_finance_remark or "",
"payStatus": d.pay_status or "",
"installmentNo": d.installment_no,
"installmentDueDate": (d.installment_due_date or "").strip() if d.installment_due_date else "",
"refundAmount": _dec(d.refund_amount),
"refundRemark": d.refund_remark or "",
"refundAttachments": d.refund_attachments or "",
"tenantNo": d.tenant_no,
"orderNum": d.order_num if d.order_num is not None else 0,
"remark": d.remark or "",
@@ -567,10 +593,39 @@ def get_protocol_detail_bundle(
ci = get_collection_info(db, protocol_id, tenant_no)
s_rows = list_samples(db, protocol_id, tenant_no)
sc = get_storage_cert(db, protocol_id, tenant_no)
out: Dict[str, Any] = {"protocol": _protocol_to_dict(p)}
protocol_dict = _protocol_to_dict(p)
ci_dict = _collection_info_to_dict(ci) if ci else None
user_ids: List[str] = []
if protocol_dict.get("collectorUserId"):
user_ids.append(str(protocol_dict["collectorUserId"]))
if ci_dict and ci_dict.get("salesUserId"):
user_ids.append(str(ci_dict["salesUserId"]))
user_name_map = _user_nick_name_map(db, user_ids)
protocol_dict["collectorUserName"] = user_name_map.get(
str(protocol_dict.get("collectorUserId") or ""), ""
)
if ci_dict:
ci_dict["salesUserName"] = user_name_map.get(str(ci_dict.get("salesUserId") or ""), "")
region_id = str(ci_dict.get("regionId") or "")
if region_id:
r = (
db.query(BizCollectionRegion.region_name)
.filter(BizCollectionRegion.id == region_id)
.first()
)
ci_dict["regionName"] = r.region_name if r else ""
hospital_id = str(ci_dict.get("hospitalId") or "")
if hospital_id:
h = (
db.query(BizCollectionHospital.hospital_name)
.filter(BizCollectionHospital.id == hospital_id)
.first()
)
ci_dict["hospitalName"] = h.hospital_name if h else ""
out: Dict[str, Any] = {"protocol": protocol_dict}
out["payment"] = _payment_to_dict(pay) if pay else None
out["partyCustomer"] = _party_customer_to_dict(pc) if pc else None
out["collectionInfo"] = _collection_info_to_dict(ci) if ci else None
out["collectionInfo"] = ci_dict
transport_rows = list_sample_transports(db, protocol_id, tenant_no)
transport_by_sample = {t.sample_id: _transport_to_dict(t) for t in transport_rows}
samples_out: List[Dict[str, Any]] = []
@@ -591,6 +646,15 @@ def get_protocol_detail_bundle(
collection_protocol_id=protocol_id,
)
item["attachments"] = urls_to_attachment_list(d.transfer_attachments, db, tenant_no)
item["financeConfirmAttachmentFiles"] = urls_to_attachment_list(
d.finance_confirm_attachments, db, tenant_no
)
item["refundFinanceConfirmAttachmentFiles"] = urls_to_attachment_list(
d.refund_finance_confirm_attachments, db, tenant_no
)
item["refundAttachmentFiles"] = urls_to_attachment_list(
d.refund_attachments, db, tenant_no
)
details_out.append(item)
out["paymentDetails"] = details_out
else:
@@ -697,7 +761,7 @@ def _detail_query_with_protocol(
pay_status_filter: str,
):
return (
db.query(BizProtocolPaymentDetail, BizProtocolPayment, BizCollectionProtocol.protocol_no)
db.query(BizProtocolPaymentDetail, BizProtocolPayment, BizCollectionProtocol)
.join(BizProtocolPayment, BizProtocolPaymentDetail.payment_id == BizProtocolPayment.id)
.join(BizCollectionProtocol, BizProtocolPayment.collection_protocol_id == BizCollectionProtocol.id)
.filter(
@@ -710,6 +774,40 @@ def _detail_query_with_protocol(
)
def _rows_to_payment_detail_dicts(
db: Session,
rows: List[Any],
tenant_no: str,
*,
post_process: Optional[Any] = None,
) -> List[Dict[str, Any]]:
"""缴费明细行 → 前端字典(含协议、客户、转账附件)。"""
protocol_ids = [
pay.collection_protocol_id for _, pay, _ in rows if pay and pay.collection_protocol_id
]
pc_map = _party_customer_map_by_protocol_ids(db, protocol_ids)
out: List[Dict[str, Any]] = []
for d, pay, prot in rows:
cpid = pay.collection_protocol_id if pay else ""
pc = pc_map.get(cpid)
item = _detail_to_dict(
d,
protocol_no=(prot.protocol_no if prot else "") or "",
collection_protocol_id=cpid,
protocol_status=(prot.protocol_status if prot else "") or "",
pay_mode=(pay.pay_mode if pay else "") or "",
mother_name=(pc.mother_name if pc else "") or "",
mother_id_card=(pc.mother_id_card if pc else "") or "",
newborn_name=(pc.newborn_name if pc else "") or "",
newborn_id_card=(pc.newborn_id_card if pc else "") or "",
)
item["attachments"] = urls_to_attachment_list(d.transfer_attachments, db, tenant_no)
if post_process:
post_process(item, d)
out.append(item)
return out
def list_receipt_confirm_details(
db: Session,
*,
@@ -729,15 +827,39 @@ def list_receipt_confirm_details(
.limit(page_size)
.all()
)
out = [
_detail_to_dict(
d,
protocol_no=pn or "",
collection_protocol_id=pay.collection_protocol_id if pay else "",
return _rows_to_payment_detail_dicts(db, rows, tenant_no), int(total)
def list_receipt_confirmed_details(
db: Session,
*,
page: int = 1,
page_size: int = 10,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
"""财务已确认收款(yjf),按确认时间倒序。"""
if not tenant_no:
return [], 0
q_base = _detail_query_with_protocol(db, tenant_no, "yjf")
total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q_base.order_by(BizProtocolPaymentDetail.update_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
def _add_finance_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
item["financeConfirmAttachmentFiles"] = urls_to_attachment_list(
d.finance_confirm_attachments, db, tenant_no
)
for d, pay, pn in rows
]
return out, int(total)
return (
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_finance_files),
int(total),
)
def confirm_receipt(
@@ -747,6 +869,7 @@ def confirm_receipt(
tenant_no: str,
user_id: Optional[str],
finance_confirm_attachments: Optional[str] = None,
finance_remark: Optional[str] = None,
) -> BizProtocolPaymentDetail:
d = (
db.query(BizProtocolPaymentDetail)
@@ -768,6 +891,8 @@ def confirm_receipt(
d.pay_status = "yjf"
if finance_confirm_attachments is not None:
d.finance_confirm_attachments = finance_confirm_attachments.strip() or None
if finance_remark is not None:
d.finance_remark = finance_remark.strip() or None
d.update_by = user_id
d.update_time = datetime.now()
db.add(d)
@@ -795,15 +920,49 @@ def list_refund_confirm_details(
.limit(page_size)
.all()
)
out = [
_detail_to_dict(
d,
protocol_no=pn or "",
collection_protocol_id=pay.collection_protocol_id if pay else "",
def _add_refund_apply_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
item["refundAttachmentFiles"] = urls_to_attachment_list(
d.refund_attachments, db, tenant_no
)
for d, pay, pn in rows
]
return out, int(total)
return (
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_refund_apply_files),
int(total),
)
def list_refund_confirmed_details(
db: Session,
*,
page: int = 1,
page_size: int = 10,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
"""财务已确认退费(ytf),按确认时间倒序。"""
if not tenant_no:
return [], 0
q_base = _detail_query_with_protocol(db, tenant_no, "ytf")
total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q_base.order_by(BizProtocolPaymentDetail.update_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
def _add_refund_confirmed_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
item["refundAttachmentFiles"] = urls_to_attachment_list(
d.refund_attachments, db, tenant_no
)
item["refundFinanceConfirmAttachmentFiles"] = urls_to_attachment_list(
d.refund_finance_confirm_attachments, db, tenant_no
)
return (
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_refund_confirmed_files),
int(total),
)
def confirm_refund(
@@ -812,7 +971,8 @@ def confirm_refund(
detail_id: str,
tenant_no: str,
user_id: Optional[str],
finance_confirm_attachments: Optional[str] = None,
refund_finance_confirm_attachments: Optional[str] = None,
refund_finance_remark: Optional[str] = None,
) -> BizProtocolPaymentDetail:
d = (
db.query(BizProtocolPaymentDetail)
@@ -832,8 +992,10 @@ def confirm_refund(
raise ValueError("该明细已确认退费")
raise ValueError("当前状态不可确认退费")
d.pay_status = "ytf"
if finance_confirm_attachments is not None:
d.finance_confirm_attachments = finance_confirm_attachments.strip() or None
if refund_finance_confirm_attachments is not None:
d.refund_finance_confirm_attachments = refund_finance_confirm_attachments.strip() or None
if refund_finance_remark is not None:
d.refund_finance_remark = refund_finance_remark.strip() or None
d.update_by = user_id
d.update_time = datetime.now()
db.add(d)
@@ -842,6 +1004,224 @@ def confirm_refund(
return d
def _get_payment_detail_or_raise(
db: Session,
*,
detail_id: str,
tenant_no: str,
) -> BizProtocolPaymentDetail:
d = (
db.query(BizProtocolPaymentDetail)
.filter(
and_(
BizProtocolPaymentDetail.id == detail_id,
BizProtocolPaymentDetail.tenant_no == tenant_no,
BizProtocolPaymentDetail.del_flag == 0,
)
)
.first()
)
if not d:
raise ValueError("缴费明细不存在")
return d
def _apply_submit_receipt_finance(
d: BizProtocolPaymentDetail,
*,
user_id: Optional[str],
) -> None:
if d.pay_status != "dfj":
raise ValueError("仅待缴费状态可提交财务确认")
d.pay_status = "jfqrz"
d.update_by = user_id
d.update_time = datetime.now()
def submit_receipt_finance(
db: Session,
*,
detail_id: str,
tenant_no: str,
user_id: Optional[str],
) -> BizProtocolPaymentDetail:
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
_apply_submit_receipt_finance(d, user_id=user_id)
db.add(d)
db.commit()
db.refresh(d)
return d
def batch_submit_receipt_finance(
db: Session,
*,
detail_ids: List[str],
tenant_no: str,
user_id: Optional[str],
) -> int:
ids = [str(i).strip() for i in (detail_ids or []) if str(i).strip()]
if not ids:
raise ValueError("请选择缴费明细")
for detail_id in ids:
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
_apply_submit_receipt_finance(d, user_id=user_id)
db.add(d)
db.commit()
return len(ids)
def _apply_submit_refund_finance(
d: BizProtocolPaymentDetail,
*,
user_id: Optional[str],
refund_amount: Any,
refund_remark: Optional[str] = None,
refund_attachments: Optional[str] = None,
) -> None:
if d.pay_status != "yjf":
raise ValueError("仅已缴费状态可提交退费财务确认")
if refund_amount is None or Decimal(str(refund_amount)) <= 0:
raise ValueError("请填写有效的退费金额")
d.pay_status = "tfqrz"
d.refund_amount = Decimal(str(refund_amount))
d.refund_remark = (refund_remark or "").strip() or None
d.refund_attachments = (refund_attachments or "").strip() or None
d.update_by = user_id
d.update_time = datetime.now()
def submit_refund_finance(
db: Session,
*,
detail_id: str,
tenant_no: str,
user_id: Optional[str],
refund_amount: Any,
refund_remark: Optional[str] = None,
refund_attachments: Optional[str] = None,
) -> BizProtocolPaymentDetail:
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
_apply_submit_refund_finance(
d,
user_id=user_id,
refund_amount=refund_amount,
refund_remark=refund_remark,
refund_attachments=refund_attachments,
)
db.add(d)
db.commit()
db.refresh(d)
return d
def batch_submit_refund_finance(
db: Session,
*,
detail_ids: List[str],
tenant_no: str,
user_id: Optional[str],
refund_amount: Any,
refund_remark: Optional[str] = None,
refund_attachments: Optional[str] = None,
) -> int:
ids = [str(i).strip() for i in (detail_ids or []) if str(i).strip()]
if not ids:
raise ValueError("请选择缴费明细")
for detail_id in ids:
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
_apply_submit_refund_finance(
d,
user_id=user_id,
refund_amount=refund_amount,
refund_remark=refund_remark,
refund_attachments=refund_attachments,
)
db.add(d)
db.commit()
return len(ids)
def _party_customer_map_by_protocol_ids(
db: Session,
protocol_ids: List[str],
) -> Dict[str, BizProtocolPartyCustomer]:
if not protocol_ids:
return {}
rows = (
db.query(BizProtocolPartyCustomer)
.filter(
BizProtocolPartyCustomer.collection_protocol_id.in_(protocol_ids),
BizProtocolPartyCustomer.del_flag == 0,
)
.all()
)
return {r.collection_protocol_id: r for r in rows}
def list_payment_details_page(
db: Session,
*,
page: int = 1,
page_size: int = 10,
pay_status: Optional[str] = None,
keyword: Optional[str] = None,
transfer_type: Optional[str] = None,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
q = (
db.query(BizProtocolPaymentDetail, BizProtocolPayment, BizCollectionProtocol)
.join(BizProtocolPayment, BizProtocolPaymentDetail.payment_id == BizProtocolPayment.id)
.join(BizCollectionProtocol, BizProtocolPayment.collection_protocol_id == BizCollectionProtocol.id)
.filter(
BizProtocolPaymentDetail.del_flag == 0,
BizProtocolPayment.del_flag == 0,
BizCollectionProtocol.del_flag == 0,
BizProtocolPaymentDetail.tenant_no == tenant_no,
)
)
if pay_status:
q = q.filter(BizProtocolPaymentDetail.pay_status == pay_status)
if keyword and keyword.strip():
like = f"%{keyword.strip()}%"
q = q.filter(BizCollectionProtocol.protocol_no.like(like))
if transfer_type and transfer_type.strip():
q = q.filter(BizProtocolPaymentDetail.transfer_type == transfer_type.strip())
total = q.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q.order_by(BizProtocolPaymentDetail.installment_due_date.asc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
protocol_ids = [
pay.collection_protocol_id for _, pay, _ in rows if pay and pay.collection_protocol_id
]
pc_map = _party_customer_map_by_protocol_ids(db, protocol_ids)
out = []
for d, pay, prot in rows:
cpid = pay.collection_protocol_id if pay else ""
pc = pc_map.get(cpid)
out.append(
_detail_to_dict(
d,
protocol_no=(prot.protocol_no if prot else "") or "",
collection_protocol_id=cpid,
protocol_status=(prot.protocol_status if prot else "") or "",
pay_mode=(pay.pay_mode if pay else "") or "",
mother_name=(pc.mother_name if pc else "") or "",
mother_id_card=(pc.mother_id_card if pc else "") or "",
newborn_name=(pc.newborn_name if pc else "") or "",
newborn_id_card=(pc.newborn_id_card if pc else "") or "",
)
)
return out, int(total)
def _classify_installment_warning(due: str, today: str) -> str:
"""overdue | warn | upcoming — due 为 YYYYMMDD"""
if not due or len(due) != 8 or not due.isdigit():
@@ -872,13 +1252,16 @@ def list_installment_warning(
# 拉取当前租户下候选(分期号非空);日期过滤在内存中完成以保证与 classify 一致
rows_all = q0.order_by(BizProtocolPaymentDetail.installment_due_date.asc()).all()
flattened: List[Dict[str, Any]] = []
for d, pay, pn in rows_all:
for d, pay, prot in rows_all:
due_raw = (d.installment_due_date or "").strip() if d.installment_due_date else ""
wt = _classify_installment_warning(due_raw, today)
cpid = pay.collection_protocol_id if pay else ""
item = _detail_to_dict(
d,
protocol_no=pn or "",
collection_protocol_id=pay.collection_protocol_id if pay else "",
protocol_no=(prot.protocol_no if prot else "") or "",
collection_protocol_id=cpid,
protocol_status=(prot.protocol_status if prot else "") or "",
pay_mode=(pay.pay_mode if pay else "") or "",
)
item["warningType"] = wt
wt_filter = (warning_type or "all").strip().lower()
+8 -2
View File
@@ -796,11 +796,17 @@ class BizProtocolPaymentDetail(Base):
due_amount = Column(Numeric(18, 2), nullable=True, comment="应缴费金额")
transfer_amount = Column(Numeric(18, 2), nullable=True, comment="转账金额")
transfer_time = Column(DateTime, nullable=True, comment="转账时间")
transfer_attachments = Column(String(4000), nullable=True, comment="转账附件")
finance_confirm_attachments = Column(String(4000), nullable=True, comment="财务确认附件")
transfer_attachments = Column(Text, nullable=True, comment="转账附件")
finance_confirm_attachments = Column(Text, nullable=True, comment="缴费财务确认附件")
finance_remark = Column(String(512), nullable=True, comment="缴费财务确认备注")
refund_finance_confirm_attachments = Column(Text, nullable=True, comment="财务退费确认附件")
refund_finance_remark = Column(String(512), nullable=True, comment="财务退费确认备注")
pay_status = Column(String(32), nullable=False, default="dfj", comment="缴费状态")
installment_no = Column(Integer, nullable=True, comment="分期期序号")
installment_due_date = Column(CHAR(8), nullable=True, comment="分期应缴日期YYYYMMDD")
refund_amount = Column(Numeric(18, 2), nullable=True, comment="退费金额")
refund_remark = Column(String(512), nullable=True, comment="退费备注")
refund_attachments = Column(Text, nullable=True, comment="退费附件逗号分隔")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
@@ -195,6 +195,60 @@ class TestListReceiptConfirmSortOrder(unittest.TestCase):
)
class TestListReceiptConfirmedSortOrder(unittest.TestCase):
"""list_receipt_confirmed_details should sort by update_time DESC."""
@patch("app.crud.collection_protocol.BizProtocolPaymentDetail")
@patch("app.crud.collection_protocol.BizProtocolPayment")
@patch("app.crud.collection_protocol.BizCollectionProtocol")
def test_receipt_confirmed_sorts_by_update_time_desc(self, MockProtocol, MockPayment, MockDetail):
from app.crud.collection_protocol import list_receipt_confirmed_details
mock_db = MagicMock()
mock_query = MagicMock()
mock_query.join.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.with_entities.return_value = mock_query
mock_query.scalar.return_value = 0
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.all.return_value = []
mock_db.query.return_value = mock_query
list_receipt_confirmed_details(mock_db, tenant_no="t1")
order_by_calls = mock_query.order_by.call_args_list
self.assertGreaterEqual(len(order_by_calls), 1)
class TestListRefundConfirmedSortOrder(unittest.TestCase):
"""list_refund_confirmed_details should sort by update_time DESC."""
@patch("app.crud.collection_protocol.BizProtocolPaymentDetail")
@patch("app.crud.collection_protocol.BizProtocolPayment")
@patch("app.crud.collection_protocol.BizCollectionProtocol")
def test_refund_confirmed_sorts_by_update_time_desc(self, MockProtocol, MockPayment, MockDetail):
from app.crud.collection_protocol import list_refund_confirmed_details
mock_db = MagicMock()
mock_query = MagicMock()
mock_query.join.return_value = mock_query
mock_query.filter.return_value = mock_query
mock_query.order_by.return_value = mock_query
mock_query.with_entities.return_value = mock_query
mock_query.scalar.return_value = 0
mock_query.offset.return_value = mock_query
mock_query.limit.return_value = mock_query
mock_query.all.return_value = []
mock_db.query.return_value = mock_query
list_refund_confirmed_details(mock_db, tenant_no="t1")
order_by_calls = mock_query.order_by.call_args_list
self.assertGreaterEqual(len(order_by_calls), 1)
# ---------------------------------------------------------------------------
# M1: Sort order — list_refund_confirm_details should include order_num ASC
# ---------------------------------------------------------------------------