From c93eea89c8c916fbfa1013862fec1754f9171577 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Thu, 20 Aug 2026 16:13:55 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pages/FinanceCustomerArapView.test.ts | 20 +--
.../finance/pages/FinanceCustomerArapView.vue | 54 +++---
.../FinanceCustomerStatementsView.test.ts | 43 ++++-
.../pages/FinanceCustomerStatementsView.vue | 155 +++++++++++++++++-
4 files changed, 226 insertions(+), 46 deletions(-)
diff --git a/src/modules/finance/pages/FinanceCustomerArapView.test.ts b/src/modules/finance/pages/FinanceCustomerArapView.test.ts
index 1c188656..e41b5bf1 100644
--- a/src/modules/finance/pages/FinanceCustomerArapView.test.ts
+++ b/src/modules/finance/pages/FinanceCustomerArapView.test.ts
@@ -11,7 +11,7 @@ import { createPlatformExportTask } from '@/modules/downloads'
import { ApiResponseError } from '@/shared/api/http'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
-import { adjustFinanceFeeSource, createReconciliationStatement, editFinanceFeeSource, getFinanceFeeSource, listFinanceFeeSources, reviewFinanceFeeSource, submitFinanceFeeSource, voidFinanceFeeSource } from '../api/finance'
+import { adjustFinanceFeeSource, editFinanceFeeSource, getFinanceFeeSource, listFinanceFeeSources, reviewFinanceFeeSource, submitFinanceFeeSource, voidFinanceFeeSource } from '../api/finance'
import type { FinanceFeeSourceDetail, FinanceFeeSourceSummary } from '../types'
import FinanceCustomerArapView from './FinanceCustomerArapView.vue'
@@ -23,7 +23,6 @@ vi.mock('../api/finance', () => ({
getFinanceFeeSource: vi.fn(),
reviewFinanceFeeSource: vi.fn(),
editFinanceFeeSource: vi.fn(), submitFinanceFeeSource: vi.fn(), adjustFinanceFeeSource: vi.fn(), voidFinanceFeeSource: vi.fn(),
- createReconciliationStatement: vi.fn(),
}))
class ResizeObserverStub implements ResizeObserver {
@@ -62,6 +61,7 @@ async function mountView(permissions: string[], initialPath = '/finance/customer
const router = createRouter({ history: createMemoryHistory(), routes: [
{ path: '/finance/customer-arap', name: 'finance-customer-arap', component: FinanceCustomerArapView },
{ path: '/finance/customer-arap/:feeSourceId', name: 'finance-customer-arap-detail', component: FinanceCustomerArapView },
+ { path: '/finance/customer-statements', name: 'finance-customer-statements', component: { template: '
客户对账单
' } },
] })
await router.push(initialPath); await router.isReady()
const wrapper = mount(FinanceCustomerArapView, {
@@ -96,7 +96,6 @@ describe('FinanceCustomerArapView G2 operations', () => {
vi.mocked(submitFinanceFeeSource).mockReset().mockResolvedValue({ ...detail, reviewStatus: 'PENDING', operationVersion: 4 })
vi.mocked(adjustFinanceFeeSource).mockReset().mockResolvedValue({ ...detail, reviewStatus: 'DRAFT', operationVersion: 4 })
vi.mocked(voidFinanceFeeSource).mockReset().mockResolvedValue({ ...detail, sourceStatus: 'VOIDED', status: 'VOIDED', operationVersion: 4 })
- vi.mocked(createReconciliationStatement).mockReset().mockResolvedValue({} as never)
vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ id: 'exp-3', taskNo: 'EXP-3', taskName: '客户应收应付费用来源', status: 'PENDING' } as never)
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
})
@@ -254,18 +253,19 @@ describe('FinanceCustomerArapView G2 operations', () => {
expect(wrapper.text()).not.toContain(customerName)
})
- it('creates a reconciliation draft from selected eligible receivables', async () => {
+ it('continues selected eligible receivables in the customer statement generation center', async () => {
const wrapper = await mountView(['platform:finance:customer-arap:list', 'platform:finance:reconciliation:create'])
await loadRows(wrapper)
const vm = wrapper.vm as unknown as {
selectedRows: Array>
- generationForm: { mode: 'BY_CUTOFF' | 'SELECTED'; customerId: string; period: string[]; remark: string }
- submitGeneration(): Promise
+ continueSelectedGeneration(): Promise
}
- vm.selectedRows = [{ id: 'RECEIVABLE:42', feeSourceId: '42', customerId: '10001', direction: 'RECEIVABLE', reviewStatus: 'APPROVED', status: 'AVAILABLE', statementGenerated: false, amount: 100 }]
- Object.assign(vm.generationForm, { mode: 'SELECTED', customerId: '10001', period: ['2026-08-01', '2026-08-31'], remark: '八月费用' })
- await vm.submitGeneration()
- expect(createReconciliationStatement).toHaveBeenCalledWith({ customerId: '10001', currency: 'CNY', generationMode: 'SELECTED', feeSourceIds: ['42'], periodStart: '2026-08-01', periodEnd: '2026-08-31', remark: '八月费用' })
+ vm.selectedRows = [{ id: 'RECEIVABLE:42', feeSourceId: '42', customerId: '10001', direction: 'RECEIVABLE', reviewStatus: 'APPROVED', status: 'AVAILABLE', statementGenerated: false, amount: 100, businessDate: '2026-08-11' }]
+ await vm.continueSelectedGeneration()
+ expect(wrapper.router.currentRoute.value).toMatchObject({
+ name: 'finance-customer-statements',
+ query: { createMode: 'SELECTED', customerId: '10001', feeSourceIds: '42', periodStart: '2026-08-11', periodEnd: '2026-08-11' },
+ })
})
it('creates a fee-source export task with the complete active filters and typed booleans', async () => {
diff --git a/src/modules/finance/pages/FinanceCustomerArapView.vue b/src/modules/finance/pages/FinanceCustomerArapView.vue
index bcd32203..d3d7b75c 100644
--- a/src/modules/finance/pages/FinanceCustomerArapView.vue
+++ b/src/modules/finance/pages/FinanceCustomerArapView.vue
@@ -18,7 +18,7 @@ import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
-import { adjustFinanceFeeSource, createReconciliationStatement, editFinanceFeeSource, getFinanceFeeSource, listFinanceFeeSources, reviewFinanceFeeSource, submitFinanceFeeSource, voidFinanceFeeSource } from '../api/finance'
+import { adjustFinanceFeeSource, editFinanceFeeSource, getFinanceFeeSource, listFinanceFeeSources, reviewFinanceFeeSource, submitFinanceFeeSource, voidFinanceFeeSource } from '../api/finance'
import { financeArapStatusOptions, financeDirectionLabel, financeOperationActionLabel, financeOperationResultLabel, financeSourceTypeLabel, financeStatusLabel } from '../status'
import type { FinanceFeeSourceDetail } from '../types'
@@ -72,10 +72,6 @@ const mutationError = ref(null)
const mutationMode = ref<'EDIT' | 'ADJUST' | 'SUBMIT' | 'VOID'>('EDIT')
const mutationForm = reactive({ amount: 0, reason: '' })
const mutationTitle = computed(() => ({ EDIT: '编辑费用', ADJUST: '调整费用', SUBMIT: '提交审核', VOID: '作废费用' } as const)[mutationMode.value])
-const generationOpen = ref(false)
-const generationSaving = ref(false)
-const generationError = ref(null)
-const generationForm = reactive<{ mode: 'BY_CUTOFF' | 'SELECTED'; customerId: string; period: string[]; cutoffDate: string; remark: string }>({ mode: 'SELECTED', customerId: '', period: [], cutoffDate: '', remark: '' })
const selectedRows = ref([])
const page = ref(1)
const pageSize = ref(20)
@@ -101,7 +97,7 @@ const queryFields = computed(() => [
{ key: 'keyword', type: 'input', label: '关键词', width: 240, placeholder: '业务单号/费用/客户/供应商', props: { 'aria-label': '关键词' } },
])
const columns: DataTableColumn[] = [
- { key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: (row: ArapRow) => row.direction === 'RECEIVABLE' && Boolean(row.feeSourceId) }, { key: 'direction', label: '收付方向', width: 100, slotName: 'direction' },
+ { 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' },
{ key: 'feeName', label: '费用项目', minWidth: 150 }, { key: 'amount', label: '金额(CNY)', width: 130 }, { key: 'supplierName', label: '供应商', minWidth: 170, slotName: 'supplierName' },
{ key: 'status', label: '来源状态', width: 130, slotName: 'sourceStatus' },
@@ -247,22 +243,31 @@ async function submitMutation() {
function statusLabel(status: string) { return financeStatusLabel(status) }
function money(value: unknown) { return Number(value || 0).toFixed(2) }
function onSelectionChange(rows: ArapRow[]) { selectedRows.value = rows }
-function openGeneration(mode: 'BY_CUTOFF' | 'SELECTED') {
- generationForm.mode = mode
- generationForm.customerId = ''
- generationForm.period = Array.isArray(filters.value.businessRange) ? filters.value.businessRange.map(String) : []
- generationForm.cutoffDate = generationForm.period[1] || ''
- generationForm.remark = ''
- generationError.value = null
- generationOpen.value = true
+function isSelectableReceivable(row: ArapRow) {
+ return Boolean(row.feeSourceId)
+ && row.direction === 'RECEIVABLE'
+ && row.reviewStatus === 'APPROVED'
+ && row.status === 'AVAILABLE'
+ && row.amount > 0
+ && row.statementGenerated === false
}
-async function submitGeneration() {
- const eligible = selectedRows.value.filter((row) => row.feeSourceId && row.direction === 'RECEIVABLE' && row.reviewStatus === 'APPROVED' && row.status === 'AVAILABLE' && row.amount > 0 && row.statementGenerated === false && row.customerId === generationForm.customerId)
- const period = generationForm.period
- if (!generationForm.customerId || !period[0] || !period[1]) { ElMessage.warning('请选择客户和业务期间'); return }
- if (generationForm.mode === 'SELECTED' && !eligible.length) { ElMessage.warning('请选择审核通过、有效且未出账的应收费用'); return }
- generationSaving.value = true; generationError.value = null
- try { await createReconciliationStatement({ customerId: generationForm.customerId, currency: 'CNY', generationMode: generationForm.mode, feeSourceIds: generationForm.mode === 'SELECTED' ? eligible.map((row) => row.feeSourceId as string) : undefined, periodStart: period[0], periodEnd: period[1], remark: generationForm.remark.trim() }); generationOpen.value = false; selectedRows.value = []; ElMessage.success('对账单草稿已生成'); await load() } catch (error) { generationError.value = getApiErrorDetails(error, '对账单生成失败,请重试') } finally { generationSaving.value = false }
+async function continueSelectedGeneration() {
+ if (!selectedRows.value.length) return ElMessage.warning('请先勾选需要生成对账单的应收费用')
+ if (selectedRows.value.some((row) => !isSelectableReceivable(row))) return ElMessage.warning('所选费用中存在不可生成对账单的记录,请刷新后重新选择')
+ const customerIds = [...new Set(selectedRows.value.map((row) => row.customerId))]
+ if (customerIds.length !== 1) return ElMessage.warning('一次只能选择同一客户的费用生成对账单')
+ const businessDates = selectedRows.value.map((row) => row.businessDate).filter((value) => /^\d{4}-\d{2}-\d{2}$/.test(value)).sort()
+ if (businessDates.length !== selectedRows.value.length) return ElMessage.warning('所选费用缺少有效业务日期,暂不能生成对账单')
+ await router.push({
+ name: 'finance-customer-statements',
+ query: {
+ createMode: 'SELECTED',
+ customerId: customerIds[0],
+ feeSourceIds: selectedRows.value.map((row) => row.feeSourceId as string).join(','),
+ periodStart: businessDates[0],
+ periodEnd: businessDates.at(-1),
+ },
+ })
}
async function exportFeeSources() {
const range = Array.isArray(filters.value.businessRange) ? filters.value.businessRange.map(String) : []
@@ -315,14 +320,14 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
-
+
- 导出Excel按截止日期生成按勾选生成
+ 导出Excel生成所选对账单
{{ customerName(row.customerId, row.customerName) }}{{ financeDirectionLabel(row.direction) }}
{{ businessReference(row as unknown as ArapRow) }}{{ sourceTypeDisplay(row.sourceType) }}{{ supplierDisplay(row as unknown as ArapRow) }}{{ sourceStatusDisplay(row.status) }}
@@ -347,9 +352,6 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
-
- {{ selectedRows.length }} 笔,合计 {{ money(selectedAmount) }} CNY
-
diff --git a/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts b/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts
index 16bf1994..cba0f508 100644
--- a/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts
+++ b/src/modules/finance/pages/FinanceCustomerStatementsView.test.ts
@@ -10,8 +10,8 @@ import { useAuthStore } from '@/core/auth/auth'
import { createPlatformExportTask } from '@/modules/downloads'
import { listPlatformTenants } from '@/modules/iam'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
-import { getReconciliationStatement, listFinanceStatements } from '../api/finance'
-import type { FinanceReconciliationDetail, FinanceStatementReadModel } from '../types'
+import { createReconciliationStatement, getReconciliationStatement, listFinanceFeeSources, listFinanceStatements } from '../api/finance'
+import type { FinanceFeeSourceSummary, FinanceReconciliationDetail, FinanceStatementReadModel } from '../types'
import FinanceCustomerStatementsView from './FinanceCustomerStatementsView.vue'
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
@@ -19,7 +19,7 @@ vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
vi.mock('../api/finance', () => ({
actionReconciliationStatement: vi.fn(), createReconciliationStatement: vi.fn(),
- getReconciliationStatement: vi.fn(), listFinanceStatements: vi.fn(),
+ getReconciliationStatement: vi.fn(), listFinanceFeeSources: vi.fn(), listFinanceStatements: vi.fn(),
}))
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
@@ -44,6 +44,13 @@ const detail: FinanceReconciliationDetail = {
paymentHistory: [{ transactionId: '701', transactionType: 'RECEIPT', transactionDate: '2026-08-20', amount: 100, method: 'BANK_TRANSFER', referenceNo: 'PAY-701', voucherFileIds: ['9001'], createdBy: '9', createdByName: '财务员', occurredAt: '2026-08-20 10:00:00' }],
revisions: [{ revisionNo: 1, action: 'ADJUST', beforeAmount: 320, afterAmount: 300, reason: '优惠调整', operatorId: '10', operatorName: '调整人', occurredAt: '2026-08-20 09:00:00' }],
}
+const eligibleFeeSource: FinanceFeeSourceSummary = {
+ poolItemId: '42', feeSourceId: '42', sourceSystem: 'FIRSTLEG', sourceType: 'INBOUND_FEE', sourceBusinessId: 'IN-21',
+ sourceId: '81', sourceVersion: 2, customerId: '10001', customerName: '客户A', direction: 'RECEIVABLE', supplierId: null,
+ supplierName: '', orderId: '11', inboundId: '21', inboundCode: 'IN-21', containerId: null, containerNo: '', businessDate: '2026-08-15',
+ feeItemName: '运费', quantity: 10, unitPrice: 30, amount: 320, currency: 'CNY', reviewStatus: 'APPROVED',
+ sourceStatus: 'AVAILABLE', statementGenerated: false, status: 'AVAILABLE', createdAt: '2026-08-15',
+}
async function mountPage(permissions = ['platform:finance:customer-statement:list'], initialPath = '/finance/customer-statements') {
const pinia = createPinia(); setActivePinia(pinia)
@@ -73,6 +80,8 @@ describe('FinanceCustomerStatementsView', () => {
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [{ id: '10001', tenantName: '客户A', status: 'ACTIVE' } as never], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(listFinanceStatements).mockResolvedValue([statement])
vi.mocked(getReconciliationStatement).mockResolvedValue(detail)
+ vi.mocked(listFinanceFeeSources).mockReset().mockResolvedValue({ records: [eligibleFeeSource], total: 1, pages: 1 })
+ vi.mocked(createReconciliationStatement).mockReset().mockResolvedValue({ ...detail, id: '502', billNo: 'REC-502', status: 'DRAFT' })
vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ taskName: '客户对账单', taskNo: 'EXP-STMT-1' } as never)
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
})
@@ -255,6 +264,34 @@ describe('FinanceCustomerStatementsView', () => {
expect(listPlatformTenants).not.toHaveBeenCalled()
})
+ it('accepts selected fee sources from customer arap and creates them in the statement page', async () => {
+ const permissions = [
+ 'platform:finance:customer-statement:list',
+ 'platform:finance:customer-arap:list',
+ 'platform:finance:reconciliation:create',
+ 'platform:customer:list',
+ ]
+ const { wrapper, router } = await mountPage(permissions, '/finance/customer-statements?createMode=SELECTED&customerId=10001&feeSourceIds=42&periodStart=2026-08-15&periodEnd=2026-08-15')
+ const vm = wrapper.vm as unknown as {
+ createOpen: boolean
+ createForm: { generationMode: string; customerId: string; feeSourceIds: string[]; periodStart: string; periodEnd: string }
+ createStatement(): Promise
+ }
+ expect(vm.createOpen).toBe(true)
+ expect(vm.createForm).toMatchObject({ generationMode: 'SELECTED', customerId: '10001', feeSourceIds: ['42'], periodStart: '2026-08-15', periodEnd: '2026-08-15' })
+ expect(listFinanceFeeSources).toHaveBeenCalledWith(expect.objectContaining({
+ customerId: '10001', direction: 'RECEIVABLE', status: 'AVAILABLE', reviewStatus: 'APPROVED', statementGenerated: false,
+ businessDateFrom: '2026-08-15', businessDateTo: '2026-08-15',
+ }))
+
+ await vm.createStatement()
+ expect(createReconciliationStatement).toHaveBeenCalledWith({
+ customerId: '10001', currency: 'CNY', generationMode: 'SELECTED', feeSourceIds: ['42'],
+ periodStart: '2026-08-15', periodEnd: '2026-08-15', remark: undefined,
+ })
+ expect(router.currentRoute.value).toMatchObject({ name: 'finance-customer-statement-detail', params: { statementId: '502' } })
+ })
+
it('uses statement detail snapshots without a second fee-source request or technical trace fields', async () => {
const { wrapper } = await mountPage(['platform:finance:customer-statement:list'])
await (wrapper.vm as unknown as { openDetail(row: FinanceStatementReadModel): Promise }).openDetail(statement)
diff --git a/src/modules/finance/pages/FinanceCustomerStatementsView.vue b/src/modules/finance/pages/FinanceCustomerStatementsView.vue
index 6cc8fff2..709829df 100644
--- a/src/modules/finance/pages/FinanceCustomerStatementsView.vue
+++ b/src/modules/finance/pages/FinanceCustomerStatementsView.vue
@@ -17,9 +17,9 @@ import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
-import { actionReconciliationStatement, createReconciliationStatement, getReconciliationStatement, listFinanceStatements } from '../api/finance'
+import { actionReconciliationStatement, createReconciliationStatement, getReconciliationStatement, listFinanceFeeSources, listFinanceStatements } from '../api/finance'
import { financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
-import type { FinanceReconciliationDetail, FinanceStatementReadModel, ReconciliationActionPayload } from '../types'
+import type { FinanceFeeSourceSummary, FinanceReconciliationDetail, FinanceStatementReadModel, ReconciliationActionPayload } from '../types'
interface AdjustmentRow {
selected: boolean
@@ -61,14 +61,20 @@ const activeDetailTab = ref<'lines' | 'payments'>('lines')
const detailCache = new Map()
const createOpen = ref(false)
const createBusy = ref(false)
+const createSourcesLoading = ref(false)
+const createSourcesError = ref(null)
+const createSourceOptions = ref([])
const exportBusy = ref(false)
const actionBusy = ref(false)
const createForm = ref({
+ generationMode: 'BY_CUTOFF' as 'BY_CUTOFF' | 'SELECTED',
customerId: '',
periodStart: new Date().toISOString().slice(0, 8) + '01',
periodEnd: new Date().toISOString().slice(0, 10),
+ feeSourceIds: [] as string[],
remark: '',
})
+let createSourceLoadSequence = 0
const adjustOpen = ref(false)
const adjustBusy = ref(false)
const adjustStatement = ref(null)
@@ -78,6 +84,7 @@ const adjustLines = ref([])
const canView = computed(() => access.can('platform:finance:customer-statement:list'))
const canListCustomers = computed(() => access.can('platform:customer:list'))
const canViewFeeSource = computed(() => access.can('platform:finance:customer-statement:list'))
+const canSelectSources = computed(() => access.can('platform:finance:customer-arap:list'))
const canCreate = computed(() => access.can('platform:finance:reconciliation:create'))
const activeCustomers = computed(() => customers.value.filter((item) => item.status === 'ACTIVE'))
const canReview = computed(() => access.can('platform:finance:reconciliation:review'))
@@ -86,6 +93,8 @@ const canAdjust = computed(() => access.can('platform:finance:reconciliation:adj
const canVoid = computed(() => access.can('platform:finance:reconciliation:void'))
const canExport = computed(() => access.can('platform:finance:customer-statement:export') && access.can('platform:download:list'))
const canGenerate = computed(() => canCreate.value && canListCustomers.value)
+const selectedCreateSources = computed(() => createSourceOptions.value.filter((item) => createForm.value.feeSourceIds.includes(item.feeSourceId)))
+const selectedCreateAmount = computed(() => selectedCreateSources.value.reduce((total, item) => total + Number(item.amount || 0), 0))
const detailRouteId = computed(() => route.name === 'finance-customer-statement-detail' ? String(route.params.statementId || '') : '')
const isDetailRoute = computed(() => Boolean(detailRouteId.value))
const fields = computed(() => [
@@ -366,22 +375,120 @@ async function exportStatements() {
}
}
+function defaultCreateForm() {
+ return {
+ generationMode: 'BY_CUTOFF' as 'BY_CUTOFF' | 'SELECTED',
+ customerId: '',
+ periodStart: new Date().toISOString().slice(0, 8) + '01',
+ periodEnd: new Date().toISOString().slice(0, 10),
+ feeSourceIds: [] as string[],
+ remark: '',
+ }
+}
+
function openCreate() {
if (!canListCustomers.value) return ElMessage.warning('缺少客户列表权限,无法选择客户生成对账单')
- createForm.value = { ...createForm.value, customerId: '' }
+ createForm.value = defaultCreateForm()
+ createSourceOptions.value = []
+ createSourcesError.value = null
createOpen.value = true
}
+async function loadEligibleCreateSources(preferredIds: string[] = createForm.value.feeSourceIds) {
+ const sequence = ++createSourceLoadSequence
+ createSourcesError.value = null
+ createSourceOptions.value = []
+ if (createForm.value.generationMode !== 'SELECTED' || !createForm.value.customerId
+ || !createForm.value.periodStart || !createForm.value.periodEnd || !canSelectSources.value) {
+ createForm.value.feeSourceIds = []
+ return
+ }
+ createSourcesLoading.value = true
+ try {
+ const records: FinanceFeeSourceSummary[] = []
+ let currentPage = 1
+ let pages = 1
+ do {
+ const result = await listFinanceFeeSources({
+ page: currentPage,
+ size: 200,
+ customerId: createForm.value.customerId,
+ direction: 'RECEIVABLE',
+ status: 'AVAILABLE',
+ reviewStatus: 'APPROVED',
+ statementGenerated: false,
+ businessDateFrom: createForm.value.periodStart,
+ businessDateTo: createForm.value.periodEnd,
+ })
+ records.push(...result.records)
+ pages = Math.max(1, result.pages)
+ currentPage += 1
+ } while (currentPage <= pages)
+ if (sequence !== createSourceLoadSequence) return
+ createSourceOptions.value = records.filter((item) => item.direction === 'RECEIVABLE'
+ && item.reviewStatus === 'APPROVED' && item.status === 'AVAILABLE'
+ && !item.statementGenerated && item.amount > 0)
+ const availableIds = new Set(createSourceOptions.value.map((item) => item.feeSourceId))
+ createForm.value.feeSourceIds = preferredIds.filter((id) => availableIds.has(id))
+ if (preferredIds.length && createForm.value.feeSourceIds.length !== preferredIds.length) {
+ ElMessage.warning('部分带入费用已被占用或不再符合生成条件,请重新核对')
+ }
+ } catch (cause) {
+ if (sequence !== createSourceLoadSequence) return
+ createSourcesError.value = getApiErrorDetails(cause, '可生成费用加载失败')
+ createForm.value.feeSourceIds = []
+ } finally {
+ if (sequence === createSourceLoadSequence) createSourcesLoading.value = false
+ }
+}
+
+function queryText(value: unknown) {
+ return Array.isArray(value) ? String(value[0] || '') : String(value || '')
+}
+
+async function openSelectedCreateFromRoute() {
+ if (route.name !== 'finance-customer-statements' || queryText(route.query.createMode) !== 'SELECTED') return
+ if (!canGenerate.value || !canSelectSources.value) {
+ ElMessage.warning('缺少客户或费用来源权限,无法生成所选对账单')
+ return
+ }
+ const feeSourceIds = queryText(route.query.feeSourceIds).split(',').map((value) => value.trim()).filter((value) => /^[1-9]\d*$/.test(value))
+ const customerId = queryText(route.query.customerId)
+ const periodStart = queryText(route.query.periodStart)
+ const periodEnd = queryText(route.query.periodEnd)
+ if (!customerId || !feeSourceIds.length || !/^\d{4}-\d{2}-\d{2}$/.test(periodStart) || !/^\d{4}-\d{2}-\d{2}$/.test(periodEnd)) {
+ ElMessage.warning('带入的对账单生成参数不完整,请重新选择费用')
+ return
+ }
+ if (!activeCustomers.value.some((item) => String(item.id) === customerId)) {
+ ElMessage.warning('带入费用所属客户当前不可用,请重新选择费用')
+ return
+ }
+ createForm.value = { generationMode: 'SELECTED', customerId, periodStart, periodEnd, feeSourceIds: [], remark: '' }
+ await loadEligibleCreateSources(feeSourceIds)
+ createOpen.value = true
+ await router.replace({ name: 'finance-customer-statements' })
+}
+
async function createStatement() {
if (!createForm.value.customerId) return ElMessage.warning('请先选择客户')
if (!createForm.value.periodStart || !createForm.value.periodEnd) return ElMessage.warning('请选择完整账期')
if (createForm.value.periodStart > createForm.value.periodEnd) return ElMessage.warning('账期开始日期不能晚于结束日期')
+ if (createForm.value.generationMode === 'SELECTED' && !createForm.value.feeSourceIds.length) return ElMessage.warning('请选择至少一笔应收费用')
createBusy.value = true
try {
- await createReconciliationStatement({ customerId: String(createForm.value.customerId), currency: 'CNY', generationMode: 'BY_CUTOFF', periodStart: createForm.value.periodStart, periodEnd: createForm.value.periodEnd, remark: createForm.value.remark || undefined })
+ const created = await createReconciliationStatement({
+ customerId: String(createForm.value.customerId),
+ currency: 'CNY',
+ generationMode: createForm.value.generationMode,
+ feeSourceIds: createForm.value.generationMode === 'SELECTED' ? createForm.value.feeSourceIds : undefined,
+ periodStart: createForm.value.periodStart,
+ periodEnd: createForm.value.periodEnd,
+ remark: createForm.value.remark || undefined,
+ })
createOpen.value = false
ElMessage.success('对账单已生成')
- await load(filters.value)
+ await router.push({ name: 'finance-customer-statement-detail', params: { statementId: created.id } })
} catch (cause) { error.value = getApiErrorDetails(cause, '创建对账单失败') }
finally { createBusy.value = false }
}
@@ -431,7 +538,17 @@ async function submitAdjustment() {
finally { adjustBusy.value = false }
}
-onMounted(() => { if (canListCustomers.value) void loadCustomers() })
+onMounted(async () => {
+ if (canListCustomers.value) await loadCustomers()
+ await openSelectedCreateFromRoute()
+})
+watch(
+ () => [createOpen.value, createForm.value.generationMode, createForm.value.customerId, createForm.value.periodStart, createForm.value.periodEnd],
+ ([open, mode]) => {
+ if (!open || mode !== 'SELECTED') return
+ void loadEligibleCreateSources()
+ },
+)
watch(detailRouteId, (statementId) => {
if (statementId) void loadDetail(statementId)
else {
@@ -568,7 +685,29 @@ watch(detailRouteId, (statementId) => {
-
+
+
+
+
+ 按账期自动汇总
+ 指定费用生成
+
+
+
+
+
+
+ 可生成费用加载失败{{ createSourcesError.message }}
重试
+
+
+
+
+ 已选 {{ createForm.feeSourceIds.length }} 笔,合计 {{ selectedCreateAmount.toFixed(2) }} CNY
+
+
+
+
+
@@ -580,6 +719,8 @@ watch(detailRouteId, (statementId) => {