提交
This commit is contained in:
1 parent
e5ba8f097a
commit
62cfb8fbbf
24 files changed
+3323
-249
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
25041
|
||||
68508
|
||||
@@ -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})
|
||||
@@ -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},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user