"""临床应用缴费 CRUD""" import uuid from datetime import datetime from decimal import Decimal from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import and_, func from sqlalchemy.orm import Session from app.models.clinical_payment import BizPilotPayment from app.models.models import ( BizPilotApplication, BizPilotPatient, BizPilotInfusionPlan, BizPilotInfusionSchedule, ) # ─── helpers ────────────────────────────────────────────── def _row_to_dict(row: BizPilotPayment, *, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: d: Dict[str, Any] = { "id": row.id, "pilotApplicationId": row.pilot_application_id or "", "infusionScheduleId": row.infusion_schedule_id or "", "bizType": row.biz_type or "clinical_application", "amount": float(row.amount) if row.amount else 0, "transferType": row.transfer_type or "", "transferAmount": float(row.transfer_amount) if row.transfer_amount else None, "transferTime": row.transfer_time.strftime("%Y-%m-%d %H:%M:%S") if row.transfer_time else "", "transferAttachments": row.transfer_attachments or "", "payStatus": row.pay_status or "dfj", "financeConfirmAttachments": row.finance_confirm_attachments or "", "financeRemark": row.finance_remark or "", "financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "", "refundAmount": float(row.refund_amount) if row.refund_amount else None, "refundRemark": row.refund_remark or "", "refundAttachments": row.refund_attachments or "", "refundApplyBy": row.refund_apply_by or "", "refundApplyTime": row.refund_apply_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_apply_time else "", "refundFinanceConfirmAttachments": row.refund_finance_confirm_attachments or "", "refundFinanceRemark": row.refund_finance_remark or "", "refundFinanceConfirmTime": row.refund_finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_finance_confirm_time else "", "tenantNo": row.tenant_no or "", "remark": row.remark or "", "createBy": row.create_by or "", "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "", "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "", } if extra: d.update(extra) return d def _get_or_raise(db: Session, *, payment_id: str, tenant_no: str) -> BizPilotPayment: row = ( db.query(BizPilotPayment) .filter( and_( BizPilotPayment.id == payment_id, BizPilotPayment.tenant_no == tenant_no, BizPilotPayment.del_flag == 0, ) ) .first() ) if not row: raise ValueError("缴费记录不存在") return row def _enrich_payment(db: Session, row: BizPilotPayment) -> Dict[str, Any]: """关联查询申请单、患者、输注计划信息,附加到 dict""" app_row = ( db.query(BizPilotApplication) .filter(BizPilotApplication.id == row.pilot_application_id) .first() ) patient_row = None schedule_row = ( db.query(BizPilotInfusionSchedule) .filter(BizPilotInfusionSchedule.id == row.infusion_schedule_id) .first() ) if app_row: patient_row = ( db.query(BizPilotPatient) .filter( BizPilotPatient.pilot_application_id == app_row.id, BizPilotPatient.del_flag == 0, ) .first() ) cell_type = schedule_row.cell_type if schedule_row else "" spec_text = schedule_row.spec_text if schedule_row else "" cell_type_spec = " / ".join(x for x in (cell_type, spec_text) if x) app_biz = (app_row.biz_type or "") if app_row else "" extra = { "applicationNo": app_row.application_no if app_row else "", "applicationName": app_row.application_name if app_row else "", "channelName": app_row.channel_name if app_row else "", "patientName": "" if app_biz == "basic_experiment" else (patient_row.patient_name if patient_row else ""), "cellType": cell_type, "specText": spec_text, "cellTypeSpec": cell_type_spec, "paymentAmount": float(row.amount) if row.amount else 0, "infusionTime": schedule_row.infusion_time.strftime("%Y-%m-%d %H:%M:%S") if schedule_row and schedule_row.infusion_time else "", } if app_biz == "basic_experiment" and app_row: extra["projectTitle"] = app_row.project_title or "" extra["customerName"] = app_row.customer_name or "" extra["customerContact"] = app_row.customer_contact or "" extra["customerPhone"] = app_row.customer_phone or "" return _row_to_dict(row, extra=extra) def map_payments_by_schedule_id( db: Session, *, application_id: str, tenant_no: str, ) -> Dict[str, Dict[str, Any]]: """按输注计划 ID 索引临床应用缴费记录""" rows = ( db.query(BizPilotPayment) .filter( BizPilotPayment.pilot_application_id == application_id, BizPilotPayment.tenant_no == tenant_no, BizPilotPayment.del_flag == 0, ) .all() ) return {row.infusion_schedule_id: _enrich_payment(db, row) for row in rows} def map_payments_by_schedule_ids( db: Session, *, schedule_ids: List[str], tenant_no: str, ) -> Dict[str, Dict[str, Any]]: """按输注计划 ID 批量索引缴费记录(列表页展示用,不做关联 enrich)""" unique_ids = [sid for sid in {sid for sid in schedule_ids if sid}] if not unique_ids: return {} rows = ( db.query(BizPilotPayment) .filter( BizPilotPayment.infusion_schedule_id.in_(unique_ids), BizPilotPayment.tenant_no == tenant_no, BizPilotPayment.del_flag == 0, ) .all() ) return {row.infusion_schedule_id: _row_to_dict(row) for row in rows} # ─── create (auto) ──────────────────────────────────────── def auto_create_payment( db: Session, *, application_id: str, schedule_id: str, amount: Any, tenant_no: str, user_id: str, dept_id: str, biz_type: str = "clinical_application", ) -> Optional[BizPilotPayment]: """输注计划创建时自动调用,生成 dfj 缴费记录""" if amount is None or Decimal(str(amount)) <= 0: return None now = datetime.now() row = BizPilotPayment( id=str(uuid.uuid4()), pilot_application_id=application_id, infusion_schedule_id=schedule_id, biz_type=biz_type or "clinical_application", amount=Decimal(str(amount)), pay_status="dfj", tenant_no=tenant_no, order_num=0, create_by=user_id, create_dept_id=dept_id or "", create_time=now, update_by=user_id, update_time=now, del_flag=0, ) db.add(row) return row # ─── list ───────────────────────────────────────────────── def list_payments( db: Session, *, page: int = 1, page_size: int = 10, pay_status: Optional[str] = None, keyword: Optional[str] = None, biz_type: Optional[str] = None, tenant_no: str = "", user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: q = ( db.query(BizPilotPayment) .filter( BizPilotPayment.del_flag == 0, BizPilotPayment.tenant_no == tenant_no, ) ) if biz_type and biz_type.strip(): q = q.filter(BizPilotPayment.biz_type == biz_type.strip()) if pay_status and pay_status.strip(): q = q.filter(BizPilotPayment.pay_status == pay_status.strip()) if keyword and keyword.strip(): kw = f"%{keyword.strip()}%" sub = ( db.query(BizPilotApplication.id) .filter( BizPilotApplication.del_flag == 0, BizPilotApplication.application_no.like(kw), ) .subquery() ) q = q.filter(BizPilotPayment.pilot_application_id.in_(sub)) if user_info: from app.core.data_scope import apply_data_scope q = apply_data_scope(q, BizPilotPayment, user_info, db, tenant_no) total = q.order_by(None).with_entities(func.count(BizPilotPayment.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) rows = ( q.order_by(BizPilotPayment.create_time.desc()) .offset((page - 1) * page_size) .limit(page_size) .all() ) out = [_enrich_payment(db, r) for r in rows] return out, int(total) # ─── submit receipt (dfj → jfqrz) ──────────────────────── def _apply_submit_receipt( db: Session, *, tenant_no: str, row: BizPilotPayment, user_id: Optional[str], remark: Optional[str] = None, transfer_attachments: Optional[str] = None, transfer_amount: Optional[Any] = None, transfer_type: Optional[str] = None, ) -> None: from app.crud.dict_data import normalize_transfer_type if row.pay_status != "dfj": raise ValueError("仅待缴费状态可提交缴费确认") plan_amount = Decimal(str(row.amount or 0)) if transfer_amount is not None and Decimal(str(transfer_amount)) != plan_amount: raise ValueError("实缴金额须与计划金额一致") if transfer_type is not None: row.transfer_type = normalize_transfer_type( db, value=transfer_type, tenant_no=tenant_no ) row.pay_status = "jfqrz" row.transfer_amount = plan_amount if remark is not None: row.remark = remark.strip() or None if transfer_attachments is not None: row.transfer_attachments = transfer_attachments.strip() or None row.update_by = user_id row.update_time = datetime.now() def submit_receipt( db: Session, *, payment_id: str, tenant_no: str, user_id: Optional[str], remark: Optional[str] = None, transfer_attachments: Optional[str] = None, transfer_amount: Optional[Any] = None, transfer_type: Optional[str] = None, ) -> BizPilotPayment: row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no) _apply_submit_receipt( db, tenant_no=tenant_no, row=row, user_id=user_id, remark=remark, transfer_attachments=transfer_attachments, transfer_amount=transfer_amount, transfer_type=transfer_type, ) db.add(row) db.commit() db.refresh(row) return row def batch_submit_receipt( db: Session, *, payment_ids: List[str], tenant_no: str, user_id: Optional[str], remark: Optional[str] = None, transfer_attachments: Optional[str] = None, ) -> int: ids = [str(i).strip() for i in (payment_ids or []) if str(i).strip()] if not ids: raise ValueError("请选择缴费记录") count = 0 for pid in ids: row = _get_or_raise(db, payment_id=pid, tenant_no=tenant_no) if row.pay_status != "dfj": continue _apply_submit_receipt( db, tenant_no=tenant_no, row=row, user_id=user_id, remark=remark, transfer_attachments=transfer_attachments, ) db.add(row) count += 1 db.commit() return count # ─── confirm receipt (jfqrz → yjf) ────────────────────── def confirm_receipt( db: Session, *, payment_id: str, tenant_no: str, user_id: Optional[str], finance_confirm_attachments: Optional[str] = None, finance_remark: Optional[str] = None, ) -> BizPilotPayment: row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no) if row.pay_status != "jfqrz": if row.pay_status == "yjf": raise ValueError("该记录已确认收款") raise ValueError("当前状态不可确认收款") row.pay_status = "yjf" if finance_confirm_attachments is not None: row.finance_confirm_attachments = finance_confirm_attachments.strip() or None if finance_remark is not None: row.finance_remark = finance_remark.strip() or None row.finance_confirm_time = datetime.now() row.update_by = user_id row.update_time = datetime.now() db.add(row) db.commit() db.refresh(row) return row def reject_receipt( db: Session, *, payment_id: str, tenant_no: str, user_id: Optional[str], ) -> BizPilotPayment: row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no) if row.pay_status != "jfqrz": raise ValueError("当前状态不可退回") row.pay_status = "dfj" row.update_by = user_id row.update_time = datetime.now() db.add(row) db.commit() db.refresh(row) return row # ─── submit refund (yjf → tfqrz) ───────────────────────── def submit_refund( db: Session, *, payment_id: str, tenant_no: str, user_id: Optional[str], refund_amount: Any, refund_remark: Optional[str] = None, refund_attachments: Optional[str] = None, ) -> BizPilotPayment: row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no) if row.pay_status != "yjf": raise ValueError("仅付款完成状态可发起退费") if refund_amount is None or Decimal(str(refund_amount)) <= 0: raise ValueError("请填写有效的退费金额") row.pay_status = "tfqrz" row.refund_amount = Decimal(str(refund_amount)) row.refund_remark = (refund_remark or "").strip() or None row.refund_attachments = (refund_attachments or "").strip() or None row.refund_apply_by = user_id row.refund_apply_time = datetime.now() row.update_by = user_id row.update_time = datetime.now() db.add(row) db.commit() db.refresh(row) return row def batch_submit_refund( db: Session, *, payment_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 (payment_ids or []) if str(i).strip()] if not ids: raise ValueError("请选择缴费记录") for pid in ids: row = _get_or_raise(db, payment_id=pid, tenant_no=tenant_no) if row.pay_status != "yjf": continue if refund_amount is None or Decimal(str(refund_amount)) <= 0: continue row.pay_status = "tfqrz" row.refund_amount = Decimal(str(refund_amount)) row.refund_remark = (refund_remark or "").strip() or None row.refund_attachments = (refund_attachments or "").strip() or None row.refund_apply_by = user_id row.refund_apply_time = datetime.now() row.update_by = user_id row.update_time = datetime.now() db.add(row) db.commit() return len(ids) # ─── confirm refund (tfqrz → ytf) ─────────────────────── def confirm_refund( db: Session, *, payment_id: str, tenant_no: str, user_id: Optional[str], refund_finance_confirm_attachments: Optional[str] = None, refund_finance_remark: Optional[str] = None, ) -> BizPilotPayment: row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no) if row.pay_status != "tfqrz": if row.pay_status == "ytf": raise ValueError("该记录已确认退费") raise ValueError("当前状态不可确认退费") row.pay_status = "ytf" if refund_finance_confirm_attachments is not None: row.refund_finance_confirm_attachments = refund_finance_confirm_attachments.strip() or None if refund_finance_remark is not None: row.refund_finance_remark = refund_finance_remark.strip() or None row.refund_finance_confirm_time = datetime.now() row.update_by = user_id row.update_time = datetime.now() db.add(row) db.commit() db.refresh(row) return row def reject_refund( db: Session, *, payment_id: str, tenant_no: str, user_id: Optional[str], ) -> BizPilotPayment: row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no) if row.pay_status != "tfqrz": raise ValueError("当前状态不可退回") row.pay_status = "yjf" row.update_by = user_id row.update_time = datetime.now() db.add(row) db.commit() db.refresh(row) return row # ─── detail ─────────────────────────────────────────────── def get_payment_detail(db: Session, *, payment_id: str, tenant_no: str) -> Optional[Dict[str, Any]]: row = _get_or_raise(db, payment_id=payment_id, tenant_no=tenant_no) return _enrich_payment(db, row) def get_payment_by_schedule(db: Session, *, schedule_id: str, tenant_no: str) -> Optional[Dict[str, Any]]: row = ( db.query(BizPilotPayment) .filter( BizPilotPayment.infusion_schedule_id == schedule_id, BizPilotPayment.tenant_no == tenant_no, BizPilotPayment.del_flag == 0, ) .first() ) if not row: return None return _enrich_payment(db, row)