完善柜级费用分摊与财务来源展示
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
|
||||
## 费用付款与凭证快速入口
|
||||
|
||||
- 入库费用的应付/直接已付表单在 `../unified-orders/components/InboundFeeDetailManager.vue`,柜次费用在 `../loading-lists/components/ContainerFeesPanel.vue`;对应 API 分别为 `../unified-orders/api/inboundFeeDetails.ts`、`../loading-lists/api/containerFees.ts`。
|
||||
- 入库费用的应付/直接已付表单在 `../unified-orders/components/InboundFeeDetailManager.vue`,柜次费用在 `../loading-lists/components/ContainerFeesPanel.vue`;柜级公共费用在同一面板按真实客户与订单分摊,入仓码按事实选填;对应 API 分别为 `../unified-orders/api/inboundFeeDetails.ts`、`../loading-lists/api/containerFees.ts`。
|
||||
- `DIRECT_PAID` 必须提交已付时间、付款方式,以及付款流水号或至少一个付款凭证;凭证由费用 API 调用 `/platform/finance/v1/vouchers` 上传为 `FINANCE_VOUCHER`,并绑定当前客户。
|
||||
- 后端事实入口为 Firstleg `FeePaymentSnapshotValidator`、入库/柜次费用服务,文件元数据归 File;Finance 只消费已发布的费用来源快照,不在前端或 Finance 中重建付款事实。
|
||||
|
||||
|
||||
@@ -246,9 +246,10 @@ describe('FinanceCustomerArapView G2 operations', () => {
|
||||
expect(page).toContain('<el-button v-if="canViewSource" size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />详情</el-button>')
|
||||
expect(page).toContain("@click=\"openMutation(row, 'SUBMIT')\"")
|
||||
expect(page).toContain("@click=\"openMutation(row, 'ADJUST')\"")
|
||||
expect(page).toContain('!isFirstlegSource(row)')
|
||||
expect(page).toContain('!isFirstlegSource(detail)')
|
||||
expect(page).toContain('!isUpstreamSource(row)')
|
||||
expect(page).toContain('!isUpstreamSource(detail)')
|
||||
expect(page).toContain("['INBOUND_FEE', 'CONTAINER_FEE'].includes(type)")
|
||||
expect(page).toContain("=== 'LASTLEG'")
|
||||
expect(page).not.toMatch(/<template #actions[\s\S]*?<el-button[^>]*\blink\b/)
|
||||
expect(page).toContain('white-space: nowrap')
|
||||
})
|
||||
|
||||
@@ -126,6 +126,9 @@ function closeCustomerSelectOnScroll() {
|
||||
const active = document.activeElement
|
||||
if (active instanceof HTMLElement) active.blur()
|
||||
}
|
||||
function isUpstreamSource(source: { sourceSystem?: unknown; sourceType?: unknown }) {
|
||||
return isFirstlegSource(source) || String(source.sourceSystem || '').trim().toUpperCase() === 'LASTLEG'
|
||||
}
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: isSelectableReceivable }, { key: 'direction', label: '收付方向', width: 100, slotName: 'direction' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' }, { key: 'businessReference', label: '关联业务', minWidth: 220, slotName: 'businessReference' }, { key: 'sourceType', label: '来源类型', width: 120, slotName: 'sourceType' },
|
||||
@@ -144,6 +147,9 @@ function sourceTypeDisplay(value: unknown) {
|
||||
const labels: Record<string, string> = {
|
||||
INBOUND_FEE: '入库费用', ORDER_FEE: '订单费用', CONTAINER_COST: '整柜成本', CONTAINER_FEE: '柜次费用',
|
||||
MANUAL_COST: '人工成本', STORAGE_FEE: '仓储费用',
|
||||
OVERSEAS_INBOUND_FEE: '海外入仓费用', OVERSEAS_PROCUREMENT_FEE: '海外采购费用',
|
||||
OVERSEAS_TRUCK_FEE: '本土卡车费用', OVERSEAS_LOOSEBOARD_FEE: '本土散板费用',
|
||||
OVERSEAS_WAREHOUSE_FEE: '海外仓服务费',
|
||||
}
|
||||
return labels[code] || (code ? '其他费用来源' : '来源类型暂不可用')
|
||||
}
|
||||
@@ -251,8 +257,10 @@ async function submitReview() {
|
||||
}
|
||||
async function openMutation(row: Record<string, unknown>, mode: 'EDIT' | 'ADJUST' | 'SUBMIT' | 'VOID') {
|
||||
const target = row as unknown as ArapRow; selected.value = target
|
||||
if ((mode === 'EDIT' || mode === 'ADJUST') && isFirstlegSource(target)) {
|
||||
ElMessage.info('请到订单入库费用或柜次费用明细中调整,审核通过后会自动同步到财务')
|
||||
if ((mode === 'EDIT' || mode === 'ADJUST' || mode === 'VOID') && isUpstreamSource(target)) {
|
||||
ElMessage.info(String(target.sourceSystem || '').toUpperCase() === 'LASTLEG'
|
||||
? '请到对应海外业务费用中调整或作废,审核后会通过跨区事件同步到财务'
|
||||
: '请到订单入库费用或柜次费用明细中调整或作废,审核后会自动同步到财务')
|
||||
return
|
||||
}
|
||||
const loaded = await loadDetail(target)
|
||||
@@ -364,7 +372,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
<section v-if="!customersError && !loadError" class="surface-panel table-panel">
|
||||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" max-height="calc(100vh - 300px)" preference-key="finance-customer-arap-table" aria-label="客户应收应付列表" row-key="id" empty-text="暂无客户应收应付数据" @selection-change="onSelectionChange" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-actions><div class="generation-actions"><el-button v-if="canExport" :loading="exportBusy" :disabled="loading || exportBusy" @click="exportFeeSources"><Download :size="16" />导出Excel</el-button><el-button v-if="canCreateStatement" type="primary" :disabled="!selectedRows.length" @click="continueSelectedGeneration"><FilePlus2 :size="16" />生成所选对账单</el-button></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><template v-if="row.feeSourceId"><el-button v-if="canViewSource" size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />详情</el-button><el-button v-if="canEditSource && !isFirstlegSource(row) && ['DRAFT','REJECTED'].includes(row.reviewStatus) && row.status === 'AVAILABLE'" size="small" plain @click="openMutation(row, 'EDIT')"><Pencil :size="15" />编辑</el-button><el-button v-if="canEditSource && ['DRAFT','REJECTED'].includes(row.reviewStatus) && row.status === 'AVAILABLE'" size="small" plain type="primary" @click="openMutation(row, 'SUBMIT')"><Send :size="15" />提交审核</el-button><el-button v-if="canReviewSource && row.reviewStatus === 'PENDING' && row.status === 'AVAILABLE'" size="small" plain type="success" @click="openReview(row)"><FileCheck2 :size="15" />审核</el-button><el-button v-if="canEditSource && !isFirstlegSource(row) && row.reviewStatus === 'APPROVED' && row.status === 'AVAILABLE' && !row.statementGenerated" size="small" plain @click="openMutation(row, 'ADJUST')"><Pencil :size="15" />调整</el-button><el-button v-if="canEditSource && row.status === 'AVAILABLE' && !row.statementGenerated" size="small" plain type="danger" @click="openMutation(row, 'VOID')"><Ban :size="15" />作废</el-button></template><span v-else class="muted-action">手工成本</span></div></template>
|
||||
<template #actions="{ row }"><div class="row-actions"><template v-if="row.feeSourceId"><el-button v-if="canViewSource" size="small" plain type="primary" @click="openDetail(row)"><Eye :size="15" />详情</el-button><el-button v-if="canEditSource && !isUpstreamSource(row) && ['DRAFT','REJECTED'].includes(row.reviewStatus) && row.status === 'AVAILABLE'" size="small" plain @click="openMutation(row, 'EDIT')"><Pencil :size="15" />编辑</el-button><el-button v-if="canEditSource && ['DRAFT','REJECTED'].includes(row.reviewStatus) && row.status === 'AVAILABLE'" size="small" plain type="primary" @click="openMutation(row, 'SUBMIT')"><Send :size="15" />提交审核</el-button><el-button v-if="canReviewSource && row.reviewStatus === 'PENDING' && row.status === 'AVAILABLE'" size="small" plain type="success" @click="openReview(row)"><FileCheck2 :size="15" />审核</el-button><el-button v-if="canEditSource && !isUpstreamSource(row) && row.reviewStatus === 'APPROVED' && row.status === 'AVAILABLE' && !row.statementGenerated" size="small" plain @click="openMutation(row, 'ADJUST')"><Pencil :size="15" />调整</el-button><el-button v-if="canEditSource && !isUpstreamSource(row) && row.status === 'AVAILABLE' && !row.statementGenerated" size="small" plain type="danger" @click="openMutation(row, 'VOID')"><Ban :size="15" />作废</el-button></template><span v-else class="muted-action">手工成本</span></div></template>
|
||||
<template #customerName="{ row }">{{ customerName(row.customerId, row.customerName) }}</template><template #direction="{ row }"><el-tag :type="row.direction === 'RECEIVABLE' ? 'success' : row.direction === 'PAYABLE' ? 'warning' : 'info'">{{ financeDirectionLabel(row.direction) }}</el-tag></template>
|
||||
<template #businessReference="{ row }">{{ businessReference(row as unknown as ArapRow) }}</template><template #sourceType="{ row }">{{ sourceTypeDisplay(row.sourceType) }}</template><template #supplierName="{ row }">{{ supplierDisplay(row as unknown as ArapRow) }}</template><template #sourceStatus="{ row }">{{ sourceStatusDisplay(row.status) }}</template>
|
||||
<template #status="{ row }"><el-tag :type="row.reviewStatus === 'APPROVED' ? 'success' : row.reviewStatus === 'REJECTED' ? 'danger' : 'warning'">{{ statusLabel(row.reviewStatus) }}</el-tag></template>
|
||||
@@ -375,7 +383,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
<AdminPage v-else as="div" class="finance-arap-detail" spacing="normal">
|
||||
<section class="surface-panel finance-arap-detail__surface">
|
||||
<DetailPageLayout class="finance-arap-detail__layout" aria-label="费用来源详情">
|
||||
<template #header><div class="detail-heading"><el-button @click="closeDetail"><ArrowLeft :size="16" />返回客户应收应付</el-button><div class="detail-title"><h2>费用来源详情</h2><p>{{ selected?.documentNo || '费用来源' }} · {{ selected ? customerName(selected.customerId, selected.customerName) : '客户名称暂不可用' }}</p></div><div class="detail-actions"><el-button v-if="canEditSource && detail && !isFirstlegSource(detail) && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected as unknown as Record<string, unknown>, 'EDIT')"><Pencil :size="16" />编辑</el-button><el-button v-if="canEditSource && detail && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected as unknown as Record<string, unknown>, 'SUBMIT')"><Send :size="16" />提交审核</el-button><el-button v-if="canReviewSource && detail?.reviewStatus === 'PENDING' && selected" @click="openReview(selected as unknown as Record<string, unknown>)"><FileCheck2 :size="16" />审核</el-button></div></div></template>
|
||||
<template #header><div class="detail-heading"><el-button @click="closeDetail"><ArrowLeft :size="16" />返回客户应收应付</el-button><div class="detail-title"><h2>费用来源详情</h2><p>{{ selected?.documentNo || '费用来源' }} · {{ selected ? customerName(selected.customerId, selected.customerName) : '客户名称暂不可用' }}</p></div><div class="detail-actions"><el-button v-if="canEditSource && detail && !isUpstreamSource(detail) && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected as unknown as Record<string, unknown>, 'EDIT')"><Pencil :size="16" />编辑</el-button><el-button v-if="canEditSource && detail && ['DRAFT','REJECTED'].includes(detail.reviewStatus)" @click="openMutation(selected as unknown as Record<string, unknown>, 'SUBMIT')"><Send :size="16" />提交审核</el-button><el-button v-if="canReviewSource && detail?.reviewStatus === 'PENDING' && selected" @click="openReview(selected as unknown as Record<string, unknown>)"><FileCheck2 :size="16" />审核</el-button></div></div></template>
|
||||
<template v-if="detail" #summary><section class="arap-metrics" aria-label="费用来源概览"><div class="arap-metric is-amount"><span>费用金额</span><strong>{{ money(detail.amount) }}</strong><small>CNY</small></div><div class="arap-metric"><span>收付方向</span><strong>{{ financeDirectionLabel(detail.direction) }}</strong></div><div class="arap-metric"><span>来源状态</span><strong>{{ sourceStatusDisplay(detail.sourceStatus) }}</strong></div><div class="arap-metric"><span>审核状态</span><strong>{{ statusLabel(detail.reviewStatus) }}</strong></div></section></template>
|
||||
<LoadErrorState v-if="detailError" title="费用来源详情加载失败" :message="detailError.message" :loading="detailLoading" @retry="selected && loadDetail(selected)"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<template v-else-if="detail"><div class="arap-detail-grid"><FormSection class="arap-information" title="单据信息" :bordered="false"><el-descriptions :column="2" class="arap-detail"><el-descriptions-item label="客户">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ displayFeeName(detail.feeItemName) }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ sourceTypeDisplay(detail.sourceType) }}</el-descriptions-item><el-descriptions-item label="关联业务">{{ detail.inboundCode || detail.containerNo || detail.sourceBusinessId || '业务关联待补充' }}</el-descriptions-item><el-descriptions-item v-if="detail.direction === 'PAYABLE'" label="供应商">{{ detail.supplierName || '供应商待补充' }}</el-descriptions-item><el-descriptions-item label="业务日期">{{ formatBusinessDate(detail.businessDate) }}</el-descriptions-item></el-descriptions></FormSection><FormSection class="arap-settlement" title="审核与结算" :bordered="false"><el-descriptions :column="1" class="arap-detail"><el-descriptions-item label="费用状态">{{ sourceStatusDisplay(detail.sourceStatus) }}</el-descriptions-item><el-descriptions-item label="已生成对账单">{{ detail.statementGenerated ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="审核原因">{{ detail.reviewReason || '未填写' }}</el-descriptions-item></el-descriptions></FormSection></div><FormSection class="operation-log" title="操作记录" :bordered="false"><el-timeline v-if="detail.operationLogs?.length"><el-timeline-item v-for="(log, index) in detail.operationLogs" :key="`${log.occurredAt}-${index}`" :timestamp="formatDateTime(log.occurredAt)">{{ financeOperationActionLabel(log.action) }} · {{ displayOperator(log.operatorName) }}<small>{{ log.remark || financeOperationResultLabel(log.result) }}</small></el-timeline-item></el-timeline><el-empty v-else description="暂无操作记录" /></FormSection></template>
|
||||
|
||||
Reference in New Issue
Block a user