From d7f2d09b88d677468233a03638076e3161730dfb Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 10 Oct 2026 19:52:33 +0800 Subject: [PATCH] fix(procurement): align correction actions with shipment state --- .../components/ProcurementNodeActions.test.ts | 35 +++++++++++++- .../components/ProcurementNodeActions.vue | 21 +++++++-- .../components/ProcurementPhysicalFields.vue | 8 ++-- .../pages/OverseasBusinessViews.test.ts | 13 ++++++ .../pages/PurchaseOrdersView.vue | 12 ++--- .../procurementPhysicalForm.test.ts | 45 +++++++++++++++++- .../procurementPhysicalForm.ts | 46 ++++++++++++++++++- 7 files changed, 162 insertions(+), 18 deletions(-) diff --git a/src/modules/overseas-business/components/ProcurementNodeActions.test.ts b/src/modules/overseas-business/components/ProcurementNodeActions.test.ts index 0d7b56f8..4f65da7a 100644 --- a/src/modules/overseas-business/components/ProcurementNodeActions.test.ts +++ b/src/modules/overseas-business/components/ProcurementNodeActions.test.ts @@ -41,6 +41,39 @@ beforeEach(() => { vi.mocked(uploadProcurementFile).mockReset().mockResolvedValue({ id: '11', fileId: '101', category: 'PURCHASE', fileName: 'purchase.pdf', contentType: 'application/pdf', sizeBytes: 3, createdAt: '' }) }) describe('procurement purchase node operations', () => { + it('disables terminal physical changes and keeps the management evidence form open', async () => { + const value = fixture('MANAGE'); value.customerView.order.status = 'SHIPPED_TO_CUSTOMER' + value.purchaseFacts = { overseas_shipped_at: '2020-01-02', customer_shipped_at: '2020-01-03', actual_warehouse_id: '20' } + value.physical.receipts = [{ id: '11', receiptNo: 'PR-11', leg: 'DOMESTIC', sourceCartonId: '31', goodQuantity: 2, damagedQuantity: 0, extraQuantity: 0, receivedAt: '2020-01-02', active: true }] + const wrapper = render(value); const dialog = await open(wrapper) + const selector = dialog.findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === '实物更正操作')!.findComponent({ name: 'ElSelect' }) + expect(selector.props('modelValue')).toBe('control-notes') + const options = selector.findAllComponents({ name: 'ElOption' }) + expect(options.find(option => option.props('value') === 'receipts/correct')!.props()).toMatchObject({ disabled: true, label: expect.stringContaining('订单已结束') }) + expect(options.find(option => option.props('value') === 'control-notes')!.props('disabled')).toBe(false) + expect(options.find(option => option.props('value') === 'actual-warehouse/correct')!.props('disabled')).toBe(false) + selector.vm.$emit('update:modelValue', 'receipts/correct'); await flushPromises() + expect(dialog.props('confirmDisabled')).toBe(true) + await confirm(wrapper) + expect(submitProcurementCommand).not.toHaveBeenCalled() + expect(sessionStorage.getItem(storageKey)).toBeNull() + }) + it('disables overseas receipt candidates after international shipment without blocking domestic correction', async () => { + const value = fixture('MANAGE'); value.customerView.order.status = 'DOMESTIC_RECEIVING' + value.purchaseFacts.overseas_shipped_at = '2020-01-02' + const receipt = { id: '11', receiptNo: 'PR-11', leg: 'OVERSEAS' as const, sourceCartonId: null, goodQuantity: 2, damagedQuantity: 0, extraQuantity: 0, receivedAt: '2020-01-01', active: true } + value.physical.receipts = [receipt, { ...receipt, id: '12', receiptNo: 'PR-12', leg: 'DOMESTIC', sourceCartonId: '31' }] + const wrapper = render(value); const dialog = await open(wrapper) + dialog.findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === '实物更正操作')!.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'receipts/correct') + await flushPromises() + const selector = dialog.findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === '待更正实收')!.findComponent({ name: 'ElSelect' }) + const options = selector.findAllComponents({ name: 'ElOption' }) + expect(options.find(option => option.props('value') === '11')!.props()).toMatchObject({ disabled: true, label: expect.stringContaining('本段已实际交寄') }) + expect(options.find(option => option.props('value') === '12')!.props('disabled')).toBe(false) + selector.vm.$emit('update:modelValue', '12'); await flushPromises() + expect(dialog.props('confirmDisabled')).toBe(false) + expect(submitProcurementCommand).not.toHaveBeenCalled() + }) it('saves and restores a separate correction while the immutable original request is pending, scoped to operator/customer/order', async () => { const value = fixture('FINANCE_ADJUST', false) value.fees = [{ id: '11', feeNo: 'PF-11', purpose: 'SAMPLE_PAYMENT', confirmationOrigin: 'BUSINESS', feeItemCode: 'SAMPLE_PAYMENT', feeItemName: '样品货款', feeVersion: 1, active: true, determination: 'POSITIVE', amount: '70.00', currency: 'CNY', originalAmount: '10.00', originalCurrency: 'USD', exchangeRate: '7', exchangeRateMonth: '2020-01', businessDate: '2020-01-01', parentFeeId: null, sourceFeeId: null, voucherFileId: '501', voucherFileName: 'invoice.pdf', walletStatus: 'PENDING', walletLedgerId: null, walletErrorCode: null, walletErrorMessage: null, remark: null }] @@ -181,7 +214,7 @@ describe('procurement purchase node operations', () => { expect(submitProcurementCommand).toHaveBeenCalledTimes(1) }) it('uses exception evidence and source quantities for damaged goods disposition', async () => { - const value = fixture('MANAGE'); value.purchaseFacts.purchased_quantity = 2 + const value = fixture('MANAGE'); value.purchaseFacts.purchased_quantity = 2; value.purchaseFacts.purchased_at = '2020-01-01T10:00:00' value.physical.receipts = [{ id: '11', receiptNo: 'PR-11', leg: 'OVERSEAS', sourceCartonId: null, goodQuantity: 1, damagedQuantity: 1, extraQuantity: 0, receivedAt: '2020-01-02T10:00:00', active: true }] vi.mocked(uploadProcurementFile).mockResolvedValue({ id: '12', fileId: '102', category: 'EXCEPTION', fileName: 'damage.png', contentType: 'image/png', sizeBytes: 3, createdAt: '' }) const wrapper = render(value); const dialog = await open(wrapper); await explain(wrapper, '破损样品已经处理,客户已确认') diff --git a/src/modules/overseas-business/components/ProcurementNodeActions.vue b/src/modules/overseas-business/components/ProcurementNodeActions.vue index d86ec268..1785e460 100644 --- a/src/modules/overseas-business/components/ProcurementNodeActions.vue +++ b/src/modules/overseas-business/components/ProcurementNodeActions.vue @@ -21,7 +21,7 @@ import { financeCommands, financeForm, buildFinance, feeRetryPermission } from ' import ProcurementTransportFields from './ProcurementTransportFields.vue' import { transportCommands, transportForm, buildTransport } from '../procurementTransportForm' import ProcurementPhysicalFields from './ProcurementPhysicalFields.vue' -import { physicalCommands, physicalForm, buildPhysical } from '../procurementPhysicalForm' +import { physicalCommands, physicalForm, buildPhysical, managementOptions, managementReason, receiptCorrectionReason, dispositionReason } from '../procurementPhysicalForm' const props = defineProps<{ detail: PlatformProcurement; customerId: string }>() const emit = defineEmits<{ changed: [] }>() const auth = useAuthStore(); const access = useAccessPreviewStore() @@ -49,6 +49,17 @@ const draftKey = computed(() => key.value ? `${key.value}:correction-draft` : '' const canDraft = computed(() => !!key.value && access.can('platform:overseas-business:purchase:finance-adjust') && props.detail.fees.some(fee => !['INTERNATIONAL_ESTIMATE', 'ADJUSTMENT', 'REFUND'].includes(fee.purpose))) const commandPermissions: Record = { review: 'review', purchase: 'buy', 'purchase/confirm': 'buy', 'budget-authorizations': 'review', 'planned-warehouse': 'review', cancel: 'cancel', 'overseas-receipts': 'receive-overseas', 'domestic-receipts': 'receive-domestic', 'overseas-receipts/complete': 'receive-overseas', 'domestic-receipts/complete': 'receive-domestic', packing: 'pack', 'packing/confirm': 'pack', 'packing/withdraw': 'manage', dispositions: 'manage', 'receipts/correct': 'manage', 'dispositions/revoke': 'manage', labels: 'label', 'international-estimate': 'estimate', 'labels/apply': 'apply-label', 'overseas-shipment': 'ship-overseas', 'customer-shipment': 'ship-customer', 'domestic-repack': 'receive-domestic', 'charges/confirm': 'charge', 'fees/retry': 'charge', 'fees/terminate': 'finance-adjust', 'fees/release-estimate': 'finance-adjust', 'fees/correct-target': 'finance-adjust', 'fees/additional': 'finance-adjust', 'fees/replace-failed': 'finance-adjust', 'fees/refund': 'finance-adjust', 'control-notes': 'manage', 'control-notes/resolve': 'manage', 'actual-warehouse/correct': 'manage', 'exception-close': 'manage' } const command = computed(() => editingDraft.value ? 'fees/correct-target' : pending.value?.command ?? (active.value?.key === 'MANAGE' ? manageCommand.value : active.value?.key === 'FINANCE_ADJUST' ? financialCommand.value : active.value ? routes[active.value.key] : undefined)) +const manageOptions = computed(() => managementOptions.map(option => ({ ...option, reason: managementReason(option.value, props.detail) }))) +const managementBlock = computed(() => { + if (active.value?.key !== 'MANAGE' || !command.value || pending.value) return null + const reason = managementReason(command.value, props.detail) + if (reason) return reason + if (command.value === 'dispositions') return dispositionReason(props.detail, physical.value.leg) + const row = command.value === 'receipts/correct' + ? props.detail.physical.receipts.find(item => item.id === physical.value.receiptId) + : command.value === 'dispositions/revoke' ? props.detail.physical.dispositions.find(item => item.id === physical.value.dispositionId) : null + return row ? receiptCorrectionReason(props.detail, row.leg) : null +}) const isControl = computed(() => !!command.value && controlCommands.has(command.value)) const isFinance = computed(() => !!command.value && financeCommands.has(command.value)) const isTransport = computed(() => !!command.value && transportCommands.has(command.value)) @@ -77,7 +88,7 @@ async function loadWarehouses() { } function open(action: ProcurementAction) { if (!action.allowed || pending.value || storageError.value || saving.value) return - editingDraft.value = false; active.value = action; error.value = null; manageCommand.value = 'dispositions'; physical.value = physicalForm(props.detail) + editingDraft.value = false; active.value = action; error.value = null; manageCommand.value = manageOptions.value.find(option => !option.reason)?.value || 'control-notes'; physical.value = physicalForm(props.detail) control.value = controlForm(props.detail) finance.value = financeForm(props.detail); financialCommand.value = 'fees/correct-target' transport.value = transportForm(props.detail, ['SHIP_CUSTOMER', 'REPACK'].includes(action.key) ? 'DOMESTIC' : action.key === 'LABELS' ? undefined : 'OVERSEAS') @@ -150,6 +161,7 @@ async function submit() { if (!pending.value) { if (!active.value?.allowed || !access.can(active.value.permission) || !command.value) return try { + if (managementBlock.value) throw new Error(managementBlock.value) const request = { command: command.value, payload: build() } const serialized = JSON.stringify(request); readPendingProcurement(serialized) sessionStorage.setItem(scope, serialized); pending.value = request @@ -201,10 +213,11 @@ async function upload(event: Event) {

原操作:{{ pending.command === 'review' ? (pending.payload.decision === 'APPROVE' ? '审核通过' : '审核拒绝') : actions.find(action => routes[action.key] === pending?.command)?.label || '采购节点操作' }} · {{ pending.payload.remark || pending.payload.reason }}

原提交金额:{{ pending.payload.amount }} {{ pending.payload.currency || (['international-estimate', 'fees/refund'].includes(pending.command) ? 'CNY' : '') }}

确认上次提交结果
- +