From a8ff0ef1de204f8396d59d541bb27cc2466ed8ad Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 4 Oct 2026 10:48:38 +0800 Subject: [PATCH] feat(overseas): show return receipt and redispatch history --- .../api/exceptionOrders.test.ts | 13 ++++++- .../overseas-warehouse/api/exceptionOrders.ts | 10 ++++- .../ExceptionProcessingPanel.test.ts | 38 ++++++++++++++++++- .../components/ExceptionProcessingPanel.vue | 34 +++++++++++++++++ .../pages/ExceptionOrdersView.test.ts | 2 +- 5 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/modules/overseas-warehouse/api/exceptionOrders.test.ts b/src/modules/overseas-warehouse/api/exceptionOrders.test.ts index 08a9a5f4..f6c03071 100644 --- a/src/modules/overseas-warehouse/api/exceptionOrders.test.ts +++ b/src/modules/overseas-warehouse/api/exceptionOrders.test.ts @@ -5,15 +5,24 @@ import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation, ge vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal()), http: { get: vi.fn(), post: vi.fn() } })) describe('exception order API', () => { + it('preserves receipt and dispatch business numbers while normalizing all actual history quantities', () => { + const raw = { order: { version: '2' }, returnSources: [], returnReceipts: [{ receiptNo: 'RSR-9007199254740993', receivedPackages: '2', receivedAt: '2026-10-03T23:24:13', + redispatches: [{ dispatchNo: 'DS/1', status: 'DEPARTED', selectedPackages: '2', pickedPackages: '2', deliveries: [{ deliveryNo: 'DL/1', status: 'DELIVERED' }] }] }] } as unknown as ExceptionDetail + const result = normalizeExceptionDetail(raw) + expect(result.returnReceipts[0]).toMatchObject({ receiptNo: 'RSR-9007199254740993', receivedPackages: 2, + redispatches: [{ dispatchNo: 'DS/1', selectedPackages: 2, pickedPackages: 2 }] }) + raw.returnReceipts[0]!.receivedPackages = null as unknown as number + expect(() => normalizeExceptionDetail(raw)).toThrow('历史实收数量无效') + }) it('normalizes actual string versions and quantities without converting business references to numbers', () => { - const raw = { order: { version: '7', evidenceFileIds: ['190000000000000099'] }, returnSources: [{ outboundPalletItemId: '190000000000000098', inventoryPositionId: '190000000000000097', maxReturnPackages: '2' }] } as unknown as ExceptionDetail + const raw = { order: { version: '7', evidenceFileIds: ['190000000000000099'] }, returnSources: [{ outboundPalletItemId: '190000000000000098', inventoryPositionId: '190000000000000097', maxReturnPackages: '2' }], returnReceipts: [] } as unknown as ExceptionDetail const result = normalizeExceptionDetail(raw) expect(result.order.version).toBe(7); expect(result.returnSources[0]?.maxReturnPackages).toBe(2) expect(result.returnSources[0]?.outboundPalletItemId).toBe('190000000000000098') for (const version of ['', '-1', '1.5', '9007199254740993', undefined]) expect(() => normalizeExceptionDetail({ ...raw, order: { ...raw.order, version } } as unknown as ExceptionDetail)).toThrow() }) it('loads authoritative detail and scopes every versioned command and evidence upload to its customer and business number', async () => { - vi.mocked(http.get).mockResolvedValue({ code: '200', data: { order: { version: '7', allowedActions: ['COMPLETE_EXCEPTION'] }, returnSources: [] } }) + vi.mocked(http.get).mockResolvedValue({ code: '200', data: { order: { version: '7', allowedActions: ['COMPLETE_EXCEPTION'] }, returnSources: [], returnReceipts: [] } }) await getExceptionDetail('100', 'EX/1') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders/EX%2F1', { params: { customerId: '100' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { exceptionNo: 'EX/1', version: '8', restoredPackages: '0' } }) diff --git a/src/modules/overseas-warehouse/api/exceptionOrders.ts b/src/modules/overseas-warehouse/api/exceptionOrders.ts index ac7f7b6e..3bf46975 100644 --- a/src/modules/overseas-warehouse/api/exceptionOrders.ts +++ b/src/modules/overseas-warehouse/api/exceptionOrders.ts @@ -14,7 +14,10 @@ export interface ExceptionOrderRow { } export interface ExceptionReturnSource { outboundPalletItemId: string; inventoryPositionId: string; palletNo?: string | null; skuCode?: string | null; maxReturnPackages: number } -export interface ExceptionDetail { order: ExceptionOrderRow; returnSources: ExceptionReturnSource[] } +export interface ExceptionRelatedDelivery { deliveryNo: string; status: string } +export interface ExceptionRedispatch { dispatchNo: string; status: string; selectedPackages: number; pickedPackages: number; deliveries: ExceptionRelatedDelivery[] } +export interface ExceptionReturnReceipt { receiptNo: string; receivedPackages: number; receivedAt: string; redispatches: ExceptionRedispatch[] } +export interface ExceptionDetail { order: ExceptionOrderRow; returnSources: ExceptionReturnSource[]; returnReceipts: ExceptionReturnReceipt[] } export interface ExceptionReceipt { outboundPalletItemId: string; inventoryPositionId: string; receivedPackages: number; targetPalletId: string; targetPalletNo: string; targetLocationId: string; targetLocationCode: string } export interface ExceptionCommand { expectedVersion: number; idempotencyKey: string; handlingAction?: string; reason?: string; handlingResult?: string; evidenceFileIds?: string[]; receipts?: ExceptionReceipt[] } export type ExceptionAction = 'start-processing' | 'complete' | 'cancel' @@ -26,7 +29,10 @@ function safeInteger(value: unknown, label: string) { } export function normalizeExceptionDetail(value: ExceptionDetail): ExceptionDetail { return { ...value, order: { ...value.order, version: safeInteger(value.order.version, '异常版本'), evidenceFileIds: value.order.evidenceFileIds?.map(String) }, - returnSources: value.returnSources.map(row => ({ ...row, outboundPalletItemId: String(row.outboundPalletItemId), inventoryPositionId: String(row.inventoryPositionId), maxReturnPackages: safeInteger(row.maxReturnPackages, '剩余可收数量') })) } + returnSources: value.returnSources.map(row => ({ ...row, outboundPalletItemId: String(row.outboundPalletItemId), inventoryPositionId: String(row.inventoryPositionId), maxReturnPackages: safeInteger(row.maxReturnPackages, '剩余可收数量') })), + returnReceipts: value.returnReceipts.map(receipt => ({ ...receipt, receivedPackages: safeInteger(receipt.receivedPackages, '历史实收数量'), + redispatches: receipt.redispatches.map(dispatch => ({ ...dispatch, selectedPackages: safeInteger(dispatch.selectedPackages, '重派选定数量'), + pickedPackages: safeInteger(dispatch.pickedPackages, '重派实拣数量') })) })) } } export async function getExceptionDetail(customerId: string, exceptionNo: string) { const response = await http.get, ApiEnvelope>(exceptionPath(exceptionNo), { params: { customerId } }) diff --git a/src/modules/overseas-warehouse/components/ExceptionProcessingPanel.test.ts b/src/modules/overseas-warehouse/components/ExceptionProcessingPanel.test.ts index 91bdddee..c22a8ef6 100644 --- a/src/modules/overseas-warehouse/components/ExceptionProcessingPanel.test.ts +++ b/src/modules/overseas-warehouse/components/ExceptionProcessingPanel.test.ts @@ -23,11 +23,11 @@ function snapshot(number = 'EX-1', version = 1, remaining = 4): ExceptionDetail return normalizeExceptionDetail({ order: { id: '1', exceptionNo: number, customerId: 'C', exceptionType: 'RETURN', returnDirection: 'OVERSEAS_WAREHOUSE', overseasWarehouseId: '92', status: remaining ? 'PROCESSING' : 'COMPLETED', version: String(version), allowedActions: remaining ? ['COMPLETE_EXCEPTION'] : [], reason: '拒收', exceptionAt: '', updatedAt: '', evidenceFileIds: ['91'] }, - returnSources: remaining ? [{ outboundPalletItemId: '10', inventoryPositionId: '20', palletNo: 'ORIGINAL', skuCode: 'SKU', maxReturnPackages: String(remaining) }] : [] } as unknown as ExceptionDetail) + returnSources: remaining ? [{ outboundPalletItemId: '10', inventoryPositionId: '20', palletNo: 'ORIGINAL', skuCode: 'SKU', maxReturnPackages: String(remaining) }] : [], returnReceipts: [] } as unknown as ExceptionDetail) } function open() { return mount(ExceptionProcessingPanel, { props: { customerId: 'C', exceptionNo: 'EX-1', active: true }, global: { directives: { loading: () => {} }, stubs: { ElDescriptions: shell, ElDescriptionsItem: shell, ElForm: shell, ElFormItem: shell, - ElInput: field, ElInputNumber: quantity, ElSelect: field, ElOption: true, ElButton: { template: '' } }, + ElInput: field, ElInputNumber: quantity, ElSelect: field, ElOption: true, RouterLink: { template: '' }, ElButton: { template: '' } }, } }) } async function fill(wrapper: ReturnType, amount = 2) { if (wrapper.find('[data-test="history-0"]').exists()) await wrapper.get('[data-test="history-0"]').setValue(true) @@ -35,6 +35,40 @@ async function fill(wrapper: ReturnType, amount = 2) { await wrapper.get('[data-test="quantity-0"]').setValue(amount) await wrapper.get('[data-test="target-0"]').setValue('30'); await flushPromises() } + +it('completed detail retains multiple receipt batches and redispatches as read-only history without commands', async () => { + const completed = snapshot('EX-1', 7, 0) + completed.returnReceipts = [ + { receiptNo: 'RSR-1', receivedPackages: 2, receivedAt: '2026-10-03T23:24:13', redispatches: [ + { dispatchNo: 'DS-1', status: 'DEPARTED', selectedPackages: 1, pickedPackages: 1, deliveries: [{ deliveryNo: 'DL-1', status: 'DELIVERED' }] }, + { dispatchNo: 'DS-2', status: 'PENDING_DEPARTURE', selectedPackages: 1, pickedPackages: 0, deliveries: [] }, + ] }, + { receiptNo: 'RSR-2', receivedPackages: 1, receivedAt: '2026-10-03T23:25:13', redispatches: [] }, + ] + vi.mocked(getExceptionDetail).mockResolvedValue(completed) + const wrapper = open(); await flushPromises() + for (const text of ['RSR-1', 'RSR-2', 'DS-1', 'DS-2', 'DL-1', '尚未关联重派排车', '已发车', '待发车']) expect(wrapper.text()).toContain(text) + expect(wrapper.findAll('.exception-return-batch')).toHaveLength(2) + expect(wrapper.find('[data-test="start"]').exists()).toBe(false) + expect(wrapper.find('[data-test="complete"]').exists()).toBe(false) + expect(wrapper.find('[data-test="cancel"]').exists()).toBe(false) + expect(commandException).not.toHaveBeenCalled(); expect(uploadExceptionEvidence).not.toHaveBeenCalled() + wrapper.unmount() +}) + +it('related business numbers remain visible without independent list permissions, but no navigation links are offered', async () => { + can.mockReturnValue(false) + const completed = snapshot('EX-1', 7, 0) + completed.returnReceipts = [{ receiptNo: 'RSR-1', receivedPackages: 2, receivedAt: '', redispatches: [ + { dispatchNo: 'DS-1', status: 'DEPARTED', selectedPackages: 2, pickedPackages: 2, deliveries: [{ deliveryNo: 'DL-1', status: 'DELIVERED' }] }, + ] }] + vi.mocked(getExceptionDetail).mockResolvedValue(completed) + const wrapper = open(); await flushPromises() + expect(wrapper.text()).toContain('DS-1'); expect(wrapper.text()).toContain('DL-1') + expect(wrapper.find('.exception-return-history a').exists()).toBe(false) + expect(commandException).not.toHaveBeenCalled() + wrapper.unmount() +}) beforeEach(() => { vi.resetAllMocks(); can.mockReturnValue(true) vi.mocked(getExceptionDetail).mockResolvedValue(snapshot()) diff --git a/src/modules/overseas-warehouse/components/ExceptionProcessingPanel.vue b/src/modules/overseas-warehouse/components/ExceptionProcessingPanel.vue index 08d93bde..53fb65f0 100644 --- a/src/modules/overseas-warehouse/components/ExceptionProcessingPanel.vue +++ b/src/modules/overseas-warehouse/components/ExceptionProcessingPanel.vue @@ -1,16 +1,20 @@