Преглед на файлове

feat(web-admin): finance confirm by transfer and remove refund confirm tab

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu преди 1 месец
родител
ревизия
c404c168fe

+ 3 - 3
backend/app/api/menu.py

@@ -65,7 +65,7 @@ STATIC_MENU_TREE = [
                         "name": "CellStoragePaymentConfirm",
                         "path": "payment-confirm",
                         "component": "/cell-storage/payment-confirm/index",
-                        "meta": {"title": "缴退费财务确认及开票", "keepAlive": True, "menuCode": "biz:cellStoragePaymentConfirm:list"},
+                        "meta": {"title": "缴费财务确认及开票", "keepAlive": True, "menuCode": "biz:cellStoragePaymentConfirm:list"},
                     },
                     # 市场侧入库放行审核(总览)暂下线:改由生产/质检入口审核
                     # {
@@ -149,7 +149,7 @@ STATIC_MENU_TREE = [
                         "name": "BasicInfoInsuranceCompany",
                         "path": "insurance-company",
                         "component": "/basic-info/insurance-company/index",
-                        "meta": {"title": "保险公司及险种价格", "keepAlive": True, "menuCode": "basic-info:insurance-company:list"},
+                        "meta": {"title": "保险公司及险种价格", "keepAlive": True, "isHide": True, "menuCode": "basic-info:insurance-company:list"},
                     },
 
                     {
@@ -193,7 +193,7 @@ STATIC_MENU_TREE = [
                         "name": "CheckupInstitution",
                         "path": "checkup-institution",
                         "component": "/pilot-service/checkup-institution/index",
-                        "meta": {"title": "体检机构及项目价格", "keepAlive": True, "menuCode": "pilot-service:checkup-institution:list"},
+                        "meta": {"title": "体检机构及项目价格", "keepAlive": True, "isHide": True, "menuCode": "pilot-service:checkup-institution:list"},
                     },
 
                     {

+ 10 - 2
backend/app/api/protocol_sample_payment.py

@@ -334,8 +334,10 @@ async def submit_payment_transfer_finance(
 @router.get("/payment-transfers/receipt-confirm")
 async def list_receipt_confirm(
     page: int = Query(1, ge=1),
-    page_size: int = Query(10, ge=1, le=100),
+    page_size: 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),
 ):
@@ -349,6 +351,8 @@ async def list_receipt_confirm(
             page=page,
             page_size=page_size,
             user_info=user,
+            due_date_start=installmentDueDateStart,
+            due_date_end=installmentDueDateEnd,
         )
         return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
     except PermissionError as e:
@@ -358,8 +362,10 @@ async def list_receipt_confirm(
 @router.get("/payment-transfers/receipt-confirmed")
 async def list_receipt_confirmed(
     page: int = Query(1, ge=1),
-    page_size: int = Query(10, ge=1, le=100),
+    page_size: 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),
 ):
@@ -373,6 +379,8 @@ async def list_receipt_confirmed(
             page=page,
             page_size=page_size,
             user_info=user,
+            due_date_start=installmentDueDateStart,
+            due_date_end=installmentDueDateEnd,
         )
         return success_response({"list": items, "total": total, "page": page, "pageSize": page_size})
     except PermissionError as e:

+ 44 - 2
backend/app/crud/protocol_sample_payment.py

@@ -829,12 +829,20 @@ def list_transfers_for_finance(
     page: int = 1,
     page_size: int = 10,
     user_info=None,
+    due_date_start: Optional[str] = None,
+    due_date_end: Optional[str] = None,
 ) -> Tuple[List[Dict[str, Any]], int]:
     if not tenant_no:
         return [], 0
     st = (pay_status or "").strip()
     q = (
-        db.query(BizProtocolPaymentTransfer, BizCollectionProtocol, BizProtocolPartyCustomer)
+        db.query(
+            BizProtocolPaymentTransfer,
+            BizCollectionProtocol,
+            BizProtocolPartyCustomer,
+            BizProtocolPaymentSchedule,
+            BizProtocolSamplePayment,
+        )
         .join(
             BizCollectionProtocol,
             and_(
@@ -852,6 +860,21 @@ def list_transfers_for_finance(
                 BizProtocolPartyCustomer.del_flag == 0,
             ),
         )
+        .outerjoin(
+            BizProtocolPaymentSchedule,
+            and_(
+                BizProtocolPaymentSchedule.id == BizProtocolPaymentTransfer.schedule_id,
+                BizProtocolPaymentSchedule.del_flag == 0,
+            ),
+        )
+        .outerjoin(
+            BizProtocolSamplePayment,
+            and_(
+                BizProtocolSamplePayment.id
+                == BizProtocolPaymentTransfer.sample_payment_id,
+                BizProtocolSamplePayment.del_flag == 0,
+            ),
+        )
         .filter(
             BizProtocolPaymentTransfer.tenant_no == tenant_no,
             BizProtocolPaymentTransfer.del_flag == 0,
@@ -867,8 +890,16 @@ def list_transfers_for_finance(
                 BizProtocolPaymentTransfer.sample_code.like(like),
                 BizProtocolPartyCustomer.mother_name.like(like),
                 BizProtocolPartyCustomer.mother_id_card.like(like),
+                BizProtocolPartyCustomer.newborn_name.like(like),
+                BizProtocolPartyCustomer.newborn_id_card.like(like),
             )
         )
+    ds = (due_date_start or "").strip().replace("-", "")[:8]
+    de = (due_date_end or "").strip().replace("-", "")[:8]
+    if ds:
+        q = q.filter(BizProtocolPaymentSchedule.due_date >= ds)
+    if de:
+        q = q.filter(BizProtocolPaymentSchedule.due_date <= de)
     if user_info:
         from app.core.data_scope import apply_data_scope
 
@@ -885,9 +916,20 @@ def list_transfers_for_finance(
         .all()
     )
     out: List[Dict[str, Any]] = []
-    for t, proto, party in rows:
+    for t, proto, party, sch, sp in rows:
         item = _transfer_to_dict(db, t, tenant_no)
         item["protocolNo"] = (proto.protocol_no if proto else "") or ""
+        item["protocolStatus"] = (proto.protocol_status if proto else "") or ""
+        item["voidType"] = (proto.void_type if proto else "") or ""
         item["motherName"] = (party.mother_name if party else "") or ""
+        item["motherIdCard"] = (party.mother_id_card if party else "") or ""
+        item["newbornName"] = (party.newborn_name if party else "") or ""
+        item["newbornIdCard"] = (party.newborn_id_card if party else "") or ""
+        item["installmentNo"] = sch.schedule_no if sch else None
+        item["installmentDueDate"] = (sch.due_date if sch else "") or ""
+        item["dueAmount"] = (
+            float(sch.due_amount) if sch and sch.due_amount is not None else None
+        )
+        item["payMode"] = (sp.pay_mode if sp else "") or ""
         out.append(item)
     return out, int(total)

+ 4 - 0
web-admin/src/api/protocol-sample-payment.ts

@@ -52,6 +52,8 @@ export function fetchPaymentTransferReceiptConfirmList(params?: {
   page?: number
   pageSize?: number
   keyword?: string
+  installmentDueDateStart?: string
+  installmentDueDateEnd?: string
 }) {
   return request.get<PageList<Record<string, unknown>>>({
     url: '/web/admin/payment-transfers/receipt-confirm',
@@ -63,6 +65,8 @@ export function fetchPaymentTransferReceiptConfirmedList(params?: {
   page?: number
   pageSize?: number
   keyword?: string
+  installmentDueDateStart?: string
+  installmentDueDateEnd?: string
 }) {
   return request.get<PageList<Record<string, unknown>>>({
     url: '/web/admin/payment-transfers/receipt-confirmed',

+ 3 - 1
web-admin/src/router/modules/sqxp.ts

@@ -89,7 +89,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
             path: 'payment-confirm',
             component: '/cell-storage/payment-confirm/index',
             meta: {
-              title: '缴退费财务确认及开票',
+              title: '缴费财务确认及开票',
               keepAlive: true,
               menuCode: 'biz:cellStoragePaymentConfirm:list'
             }
@@ -290,6 +290,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
             meta: {
               title: '保险公司及险种价格',
               keepAlive: true,
+              isHide: true,
               menuCode: 'basic-info:insurance-company:list'
             }
           },
@@ -398,6 +399,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
             meta: {
               title: '体检机构及项目价格',
               keepAlive: true,
+              isHide: true,
               menuCode: 'pilot-service:checkup-institution:list'
             }
           },

+ 1 - 1
web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue

@@ -1,4 +1,4 @@
-<!-- 缴退费财务确认 · 退费 Tab -->
+<!-- 已停用:按样本缴费后退费改为协议级登记,财务页不再挂载本 Tab(保留文件供旧版对照) -->
 <!-- eslint-disable vue/no-v-model-argument -->
 <template>
   <div class="refund-confirm-tab">

+ 68 - 72
web-admin/src/views/cell-storage/payment-confirm/index.vue

@@ -1,4 +1,4 @@
-<!-- 缴退费财务确认 -->
+<!-- 缴费财务确认及开票(按转账笔;退费确认 Tab 已下线) -->
 <!-- eslint-disable vue/no-v-model-argument -->
 <template>
   <div class="page-container">
@@ -18,19 +18,7 @@
               formatTabBadgeCount(paymentPendingBadge)
             }}</span>
           </button>
-          <button
-            type="button"
-            role="tab"
-            class="panel-pill-tabs__btn"
-            :class="{ 'is-active': mainTab === 'refund' }"
-            :aria-selected="mainTab === 'refund'"
-            @click="mainTab = 'refund'"
-          >
-            退费确认
-            <span v-if="refundPendingBadge > 0" class="panel-pill-tabs__badge">{{
-              formatTabBadgeCount(refundPendingBadge)
-            }}</span>
-          </button>
+          <!-- TODO(P1): 开票改挂 transfer;当前仍走旧 payment_detail 开票 API -->
           <button
             type="button"
             role="tab"
@@ -68,7 +56,7 @@
               <el-form-item label="关键词">
                 <el-input
                   v-model="pendingSearchForm.keyword"
-                  placeholder="客户姓名/身份证号/新生儿姓名/新生儿身份证号"
+                  placeholder="协议号/样本编码/客户姓名/身份证号"
                   clearable
                   @keyup.enter="handlePendingSearch"
                 />
@@ -105,20 +93,23 @@
                 <div class="pending-card__identity">
                   <div class="pending-card__name">{{ row.motherName || '-' }}</div>
                   <div class="pending-card__sub">
-                    {{ maskIdCard(row.motherIdCard) }}
-                    <template v-if="row.newbornName"> · 新生儿 {{ row.newbornName }} </template>
+                    {{ row.protocolNo || '-' }}
+                    <template v-if="row.sampleCode"> · {{ row.sampleCode }}</template>
+                    <template v-if="row.motherIdCard">
+                      · {{ maskIdCard(row.motherIdCard) }}
+                    </template>
                   </div>
                 </div>
                 <div class="pending-card__amount">
                   <div class="pending-card__amount-val">
                     {{ formatPaymentAmount(row.transferAmount) }}
                   </div>
-                  <div class="pending-card__amount-label">实缴金额</div>
+                  <div class="pending-card__amount-label">转账金额</div>
                 </div>
               </div>
               <div class="pending-card__grid">
                 <div class="pending-card__field">
-                  <label>期数</label>
+                  <label>期次</label>
                   <span>第 {{ row.installmentNo ?? '-' }} 期</span>
                 </div>
                 <div class="pending-card__field">
@@ -126,7 +117,7 @@
                   <span>{{ formatPaymentDueDate(row.installmentDueDate) }}</span>
                 </div>
                 <div class="pending-card__field">
-                  <label>应缴金额</label>
+                  <label>批次应缴</label>
                   <span>{{ formatPaymentAmount(row.dueAmount) }}</span>
                 </div>
                 <div class="pending-card__field">
@@ -141,6 +132,10 @@
                   <label>转账时间</label>
                   <span>{{ formatDisplayOrDash(row.transferTime) }}</span>
                 </div>
+                <div class="pending-card__field">
+                  <label>来源</label>
+                  <span>{{ row.source === 'presign' ? '预签约意向金' : '正式协议' }}</span>
+                </div>
               </div>
               <div class="pending-card__foot">
                 <div class="pending-card__meta">
@@ -195,7 +190,7 @@
               <el-form-item label="关键词">
                 <el-input
                   v-model="confirmedSearchForm.keyword"
-                  placeholder="客户姓名/身份证号/新生儿姓名/新生儿身份证号"
+                  placeholder="协议号/样本编码/客户姓名/身份证号"
                   clearable
                   @keyup.enter="handleConfirmedSearch"
                 />
@@ -244,6 +239,24 @@
                 (index) => (confirmedPagination.page - 1) * confirmedPagination.pageSize + index + 1
               "
             />
+            <el-table-column
+              prop="protocolNo"
+              label="协议号"
+              min-width="140"
+              align="center"
+              header-align="center"
+            >
+              <template #default="{ row }">{{ row.protocolNo || '-' }}</template>
+            </el-table-column>
+            <el-table-column
+              prop="sampleCode"
+              label="样本编码"
+              min-width="120"
+              align="center"
+              header-align="center"
+            >
+              <template #default="{ row }">{{ row.sampleCode || '-' }}</template>
+            </el-table-column>
             <el-table-column
               prop="motherName"
               label="客户姓名"
@@ -263,22 +276,17 @@
               <template #default="{ row }">{{ maskIdCard(row.motherIdCard) }}</template>
             </el-table-column>
             <el-table-column
-              prop="newbornName"
-              label="新生儿姓名"
-              min-width="110"
-              align="center"
-              header-align="center"
-            >
-              <template #default="{ row }">{{ row.newbornName || '-' }}</template>
-            </el-table-column>
-            <el-table-column
-              prop="newbornIdCard"
-              label="新生儿身份证号"
-              min-width="170"
+              prop="source"
+              label="来源"
+              min-width="100"
               align="center"
               header-align="center"
             >
-              <template #default="{ row }">{{ maskIdCard(row.newbornIdCard) }}</template>
+              <template #default="{ row }">
+                <el-tag size="small" :type="row.source === 'presign' ? 'warning' : 'primary'">
+                  {{ row.source === 'presign' ? '预签约' : '正式' }}
+                </el-tag>
+              </template>
             </el-table-column>
             <el-table-column
               prop="protocolStatus"
@@ -291,7 +299,12 @@
                 <el-tag
                   :type="protocolStatusTagType(String(row.protocolStatus || ''))"
                   size="small"
-                  >{{ protocolStatusDisplayLabel(String(row.protocolStatus || ''), String(row.voidType || '')) }}</el-tag
+                  >{{
+                    protocolStatusDisplayLabel(
+                      String(row.protocolStatus || ''),
+                      String(row.voidType || '')
+                    )
+                  }}</el-tag
                 >
               </template>
             </el-table-column>
@@ -378,9 +391,10 @@
               label="确认时间"
               min-width="170"
               align="center"
-              header-align="center">
-            <template #default="{ row }">{{ formatDisplayOrDash(row.updateTime) }}</template>
-          </el-table-column>
+              header-align="center"
+            >
+              <template #default="{ row }">{{ formatDisplayOrDash(row.updateTime) }}</template>
+            </el-table-column>
             <el-table-column
               prop="financeRemark"
               label="财务备注"
@@ -457,11 +471,7 @@
               <el-icon><Close /></el-icon>
               取消
             </el-button>
-            <el-button
-              :loading="rejectLoading"
-              :disabled="submitLoading"
-              @click="submitReject"
-            >
+            <el-button :loading="rejectLoading" :disabled="submitLoading" @click="submitReject">
               <el-icon><RefreshLeft /></el-icon>
               退回
             </el-button>
@@ -503,7 +513,6 @@
         </el-dialog>
       </div>
 
-      <RefundConfirmTab v-if="mainTab === 'refund'" @action-done="loadTabPendingCounts" />
       <InvoiceRequestTab v-if="mainTab === 'invoice'" @action-done="loadTabPendingCounts" />
     </section>
   </div>
@@ -514,14 +523,13 @@
   import { ref, reactive, onMounted } from 'vue'
   import { ElMessage, ElMessageBox } from 'element-plus'
   import { Close, Check, RefreshLeft, Search, View } from '@element-plus/icons-vue'
+  import { fetchProtocolInvoicePendingCount } from '@/api/collection-protocol'
   import {
-    fetchReceiptConfirmList,
-    fetchReceiptConfirmedList,
-    fetchReceiptConfirmSubmit,
-    fetchReceiptConfirmReject,
-    fetchRefundConfirmList,
-    fetchProtocolInvoicePendingCount
-  } from '@/api/collection-protocol'
+    fetchPaymentTransferReceiptConfirmList,
+    fetchPaymentTransferReceiptConfirmedList,
+    fetchConfirmPaymentTransfer,
+    fetchRejectPaymentTransfer
+  } from '@/api/protocol-sample-payment'
   import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
   import ClinicalPaymentConfirmTimeline from '@/components/business/clinical-payment-confirm-timeline/index.vue'
   import {
@@ -533,14 +541,12 @@
   import {
     formatPaymentDueDate,
     payModeLabel,
-    protocolStatusLabel,
     protocolStatusDisplayLabel,
     protocolStatusTagType,
     loadTransferTypeDict,
     transferTypeLabel
   } from '@/utils/protocol-payment-labels'
   import { formatMoneyDisplay } from '@/utils/money-format'
-  import RefundConfirmTab from './RefundConfirmTab.vue'
   import InvoiceRequestTab from './InvoiceRequestTab.vue'
 
   defineOptions({ name: 'CellStoragePaymentConfirm' })
@@ -548,14 +554,12 @@
   const route = useRoute()
   const mainTab = ref('payment')
   const paymentPendingBadge = ref(0)
-  const refundPendingBadge = ref(0)
   const invoicePendingBadge = ref(0)
 
   function formatTabBadgeCount(count: number): string {
     return count > 99 ? '99+' : String(count)
   }
 
-
   function formatPaymentAmount(v: unknown): string {
     if (v == null || v === '') return '-'
     return '¥' + formatMoneyDisplay(v)
@@ -591,12 +595,6 @@
     return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
   }
 
-  function handleAttachmentPreview(file: Record<string, unknown>) {
-    const url = String(file.fileUrl || '')
-    if (!url) return
-    window.open(fullFileUrl(url), '_blank')
-  }
-
   function handleAttachmentDownload(file: Record<string, unknown>) {
     downloadAttachmentFile(file, { fullFileUrl })
   }
@@ -623,13 +621,11 @@
 
   async function loadTabPendingCounts() {
     try {
-      const [paymentRes, refundRes, invRes] = await Promise.all([
-        fetchReceiptConfirmList({ page: 1, pageSize: 1 }),
-        fetchRefundConfirmList({ page: 1, pageSize: 1 }),
+      const [paymentRes, invRes] = await Promise.all([
+        fetchPaymentTransferReceiptConfirmList({ page: 1, pageSize: 1 }),
         fetchProtocolInvoicePendingCount()
       ])
       paymentPendingBadge.value = paymentRes.total ?? 0
-      refundPendingBadge.value = refundRes.total ?? 0
       invoicePendingBadge.value = Number(invRes?.count ?? 0)
     } catch {
       // 角标加载失败时保留上次数值,避免干扰主流程
@@ -639,7 +635,7 @@
   async function loadPending() {
     pendingLoading.value = true
     try {
-      const res = await fetchReceiptConfirmList(buildPendingParams())
+      const res = await fetchPaymentTransferReceiptConfirmList(buildPendingParams())
       pendingData.value = res.list || []
       pendingPagination.total = res.total ?? 0
     } finally {
@@ -650,7 +646,7 @@
   async function loadConfirmed() {
     confirmedLoading.value = true
     try {
-      const res = await fetchReceiptConfirmedList(buildConfirmedParams())
+      const res = await fetchPaymentTransferReceiptConfirmedList(buildConfirmedParams())
       confirmedData.value = res.list || []
       confirmedPagination.total = res.total ?? 0
     } finally {
@@ -707,7 +703,7 @@
     }
     submitLoading.value = true
     try {
-      await fetchReceiptConfirmSubmit(currentDetailId.value, {
+      await fetchConfirmPaymentTransfer(currentDetailId.value, {
         financeConfirmAttachments: financeAttachmentsToUrlString(attachments.value) || undefined,
         financeRemark: financeRemark.value.trim() || undefined
       })
@@ -730,7 +726,7 @@
     }
     rejectLoading.value = true
     try {
-      await fetchReceiptConfirmReject(currentDetailId.value)
+      await fetchRejectPaymentTransfer(currentDetailId.value)
       dialogVisible.value = false
       await loadAll()
       await loadTabPendingCounts()
@@ -760,10 +756,10 @@
   onMounted(async () => {
     await loadTransferTypeDict()
     const tab = route.query.tab
-    if (tab === 'refund') {
-      mainTab.value = 'refund'
-    } else if (tab === 'invoice') {
+    if (tab === 'invoice') {
       mainTab.value = 'invoice'
+    } else {
+      mainTab.value = 'payment'
     }
     loadTabPendingCounts()
     if (mainTab.value === 'payment') {