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 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) => { - + + + + + 按账期自动汇总 + 指定费用生成 + + + +
+ + +
+
@@ -580,6 +719,8 @@ watch(detailRouteId, (statementId) => {