From bef074097aea9a1fc99a20c6eacd660f3e0226d5 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 30 Oct 2026 17:59:39 +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 --- .../overseas-business/orderFunding.test.ts | 17 ++++++++ .../pages/OverseasBusinessViews.test.ts | 7 ++-- .../pages/TransportOrdersView.vue | 2 +- .../procurementPresentation.ts | 4 +- .../overseas-business/truckWorkflow.ts | 2 +- src/modules/overseas-business/types.ts | 4 +- .../overseas-warehouse/api/dispatches.ts | 3 ++ .../overseas-warehouse/api/exceptionOrders.ts | 8 +++- .../components/DispatchReturnCargo.vue | 39 +++++++++++++++++++ .../components/DispatchWorkflowDialog.test.ts | 4 +- .../components/DispatchWorkflowDialog.vue | 8 +++- .../components/ExceptionProcessingPanel.vue | 15 ++++++- .../overseas-warehouse/dispatchContracts.ts | 5 ++- .../lastMilePresentation.ts | 1 + 14 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 src/modules/overseas-business/orderFunding.test.ts create mode 100644 src/modules/overseas-warehouse/components/DispatchReturnCargo.vue diff --git a/src/modules/overseas-business/orderFunding.test.ts b/src/modules/overseas-business/orderFunding.test.ts new file mode 100644 index 00000000..1d871105 --- /dev/null +++ b/src/modules/overseas-business/orderFunding.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' +import { statusLabel, statusTone } from './procurementPresentation' +import { availableTruckCommands, truckStatusLabels } from './truckWorkflow' + +describe('异步下单资金门禁', () => { + it.each(['FUNDING_PENDING', 'FUNDING_FAILED'] as const)('资金状态 %s 不开放卡车审核或排车', status => { + expect(availableTruckCommands({ status, fleetArrangement: null })).toEqual([]) + expect(truckStatusLabels[status]).toBe(status === 'FUNDING_PENDING' ? '订单受理中' : '订单处理失败') + }) + it('采购清晰区分订单受理中、失败和待审核', () => { + expect(statusLabel('FUNDING_PENDING')).toBe('订单受理中') + expect(statusLabel('FUNDING_FAILED')).toBe('订单处理失败') + expect(statusTone('FUNDING_FAILED')).toBe('danger') + expect(statusLabel('PENDING_REVIEW')).toBe('待审核') + expect(availableTruckCommands({ status: 'PENDING_REVIEW', fleetArrangement: null })).toContain('review') + }) +}) diff --git a/src/modules/overseas-business/pages/OverseasBusinessViews.test.ts b/src/modules/overseas-business/pages/OverseasBusinessViews.test.ts index c7ef27f3..111edc0e 100644 --- a/src/modules/overseas-business/pages/OverseasBusinessViews.test.ts +++ b/src/modules/overseas-business/pages/OverseasBusinessViews.test.ts @@ -300,7 +300,7 @@ describe('overseas business mounted pages', () => { ? ['B点取货地', 'C点送达地', 'B点联系人', 'C点联系人', 'B点联系电话', 'C点联系电话', 'B点取货地址', 'C点送达地址'] : ['起运地', '目的地', '起运地联系人', '目的地联系人', '起运地联系电话', '目的地联系电话', '起运详细地址', '目的详细地址'] expect(labels.slice(0, domain === 'truck' ? 10 : 9)).toEqual(['客户', ...(domain === 'truck' ? ['车辆类型'] : []), ...routeLabels]) - Object.assign(model, { vehicleType: 'PALLET_15', origin: ' LA ', destination: ' ONT ', originContactName: ' 发货人 ', originContactPhone: ' 1 ', destinationContactName: ' 收货人 ', destinationContactPhone: ' 2 ', pickupAt: '2026-10-20T09:30:45', deliveryAt: '2026-10-20T15:45:12', budgetAmount: 0, currency: 'CNY' }) + Object.assign(model, { vehicleType: 'PALLET_15', estimatedPalletCount: 1, origin: ' LA ', destination: ' ONT ', originContactName: ' 发货人 ', originContactPhone: ' 1 ', destinationContactName: ' 收货人 ', destinationContactPhone: ' 2 ', pickupAt: '2026-10-20T09:30:45', deliveryAt: '2026-10-20T15:45:12', budgetAmount: domain === 'truck' ? 1 : 0, currency: 'CNY' }) const submit = wrapper.findAll('button').find(button => button.text() === '提交订单')! await submit.trigger('click'); await flushPromises() expect(createUsTruckOrder).not.toHaveBeenCalled(); expect(createLocalLooseboardOrder).not.toHaveBeenCalled() @@ -310,7 +310,7 @@ describe('overseas business mounted pages', () => { expect(command).toHaveBeenCalledOnce() const [customer, payload] = command.mock.calls[0]! expect(customer).toBe('10001') - expect(payload).toEqual(expect.objectContaining({ origin: 'LA', destination: 'ONT', originContactName: '发货人', originContactPhone: '1', destinationContactName: '收货人', destinationContactPhone: '2', totalWeightKg: 0, totalVolumeCbm: 0, budgetAmount: 0, currency: 'CNY', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00' })) + expect(payload).toEqual(expect.objectContaining({ origin: 'LA', destination: 'ONT', originContactName: '发货人', originContactPhone: '1', destinationContactName: '收货人', destinationContactPhone: '2', totalWeightKg: 0, totalVolumeCbm: 0, budgetAmount: domain === 'truck' ? 1 : 0, currency: 'CNY', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00' })) for (const field of ['customerId', 'tenantId', 'orderNo', 'status', 'transportMode', 'containerType']) expect(payload).not.toHaveProperty(field) }) @@ -327,7 +327,7 @@ describe('overseas business mounted pages', () => { it('loads and submits pending-review truck edit', async () => { routing.route.params = { orderId: 'TK-200' }; routing.route.query = { customerId: '10002' }; const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(getUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002'); expect(updateUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002', expect.objectContaining({ version: 1, vehicleType: 'PALLET_15', originContactName: 'Bob', destinationContactName: 'Alice', pickupAt: '2026-08-28T09:30:00' })) }) it('keeps truck edit form and version unchanged after a 409', async () => { routing.route.params = { orderId: 'TK-200' }; routing.route.query = { customerId: '10002' }; vi.mocked(updateUsTruckOrder).mockRejectedValue(new ApiResponseError('订单版本冲突', 'B0409', 409)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await destination.setValue('Ontario-MARKER-EDIT'); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(updateUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002', expect.objectContaining({ version: 1, destination: 'Ontario-MARKER-EDIT' })); expect((destination.element as HTMLInputElement).value).toBe('Ontario-MARKER-EDIT'); expect(document.body.textContent).toContain('订单版本冲突'); expect(routing.push).not.toHaveBeenCalled() }) it('mounts truck create with explicit customer selector and enabled submit', async () => { const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' }); expect(wrapper.text()).toContain('客户信息'); expect(wrapper.text()).toContain('请选择客户'); expect(wrapper.findAll('button').find((button) => button.text().includes('提交订单'))?.attributes('disabled')).toBeUndefined() }) - it('reuses one idempotency key and preserves truck create input after retries fail', async () => { vi.mocked(createUsTruckOrder).mockRejectedValue(new ApiResponseError('temporary failure', 'S0500', 500)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0].vm.$emit('update:modelValue', '10001'); selects[1].vm.$emit('update:modelValue', 'PALLET_30'); const origin = wrapper.find('input[placeholder="城市 / 港口 / 州"]'); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await origin.setValue('LA-MARKER'); await destination.setValue('ONT-MARKER'); for (const label of ['B点联系人', 'C点联系人', 'B点联系电话', 'C点联系电话']) await wrapper.findAll('.el-form-item').find(item => item.text().startsWith(label))!.find('input').setValue(label.includes('电话') ? '+1-555-0100' : '联系人'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总重量'))!.find('input').setValue('100'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总体积'))!.find('input').setValue('2.5'); wrapper.findAllComponents({ name: 'ElDatePicker' })[0].vm.$emit('update:modelValue', '2026-08-31T09:30:00'); wrapper.findAllComponents({ name: 'ElDatePicker' })[1].vm.$emit('update:modelValue', '2026-08-31T15:45:00'); await wrapper.vm.$nextTick(); const submit = wrapper.findAll('button').find((button) => button.text().includes('提交订单')); await submit?.trigger('click'); await flushPromises(); await submit?.trigger('click'); await flushPromises(); expect(createUsTruckOrder).toHaveBeenCalledTimes(2); const keys = vi.mocked(createUsTruckOrder).mock.calls.map((call) => call[1].idempotencyKey); expect(keys[0]).toBeTruthy(); expect(keys[1]).toBe(keys[0]); expect((origin.element as HTMLInputElement).value).toBe('LA-MARKER'); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER'); expect(document.body.textContent).toContain('服务暂时不可用'); expect(routing.push).not.toHaveBeenCalled() }) + it('reuses one idempotency key and preserves truck create input after retries fail', async () => { vi.mocked(createUsTruckOrder).mockRejectedValue(new ApiResponseError('temporary failure', 'S0500', 500)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0].vm.$emit('update:modelValue', '10001'); selects[1].vm.$emit('update:modelValue', 'PALLET_30'); const model = wrapper.findComponent({ name: 'ElForm' }).props('model'); Object.assign(model, { estimatedPalletCount: 1, budgetAmount: 1 }); const origin = wrapper.find('input[placeholder="城市 / 港口 / 州"]'); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await origin.setValue('LA-MARKER'); await destination.setValue('ONT-MARKER'); for (const label of ['B点联系人', 'C点联系人', 'B点联系电话', 'C点联系电话']) await wrapper.findAll('.el-form-item').find(item => item.text().startsWith(label))!.find('input').setValue(label.includes('电话') ? '+1-555-0100' : '联系人'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总重量'))!.find('input').setValue('100'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总体积'))!.find('input').setValue('2.5'); wrapper.findAllComponents({ name: 'ElDatePicker' })[0].vm.$emit('update:modelValue', '2026-08-31T09:30:00'); wrapper.findAllComponents({ name: 'ElDatePicker' })[1].vm.$emit('update:modelValue', '2026-08-31T15:45:00'); await wrapper.vm.$nextTick(); const submit = wrapper.findAll('button').find((button) => button.text().includes('提交订单')); await submit?.trigger('click'); await flushPromises(); await submit?.trigger('click'); await flushPromises(); expect(createUsTruckOrder).toHaveBeenCalledTimes(2); const keys = vi.mocked(createUsTruckOrder).mock.calls.map((call) => call[1].idempotencyKey); expect(keys[0]).toBeTruthy(); expect(keys[1]).toBe(keys[0]); expect((origin.element as HTMLInputElement).value).toBe('LA-MARKER'); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER'); expect(document.body.textContent).toContain('服务暂时不可用'); expect(routing.push).not.toHaveBeenCalled() }) it('loads the real looseboard list', async () => { const wrapper = mountPage(TransportOrdersView, { domain: 'looseboard' }); await flushPromises(); expect(listLocalLooseboardOrders).toHaveBeenCalledWith({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' }); expect(wrapper.text()).toContain('LB-300'); expect(wrapper.text()).toContain('待接单'); expect(wrapper.text()).toContain('2026-08-01 10:00'); expect(listUsTruckOrders).not.toHaveBeenCalled() }) it('loads and submits pending-acceptance looseboard edit with PUT adapter', async () => { routing.route.params = { orderId: 'LB-300' }; routing.route.query = { customerId: '10003' }; const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(getLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003'); expect(updateLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003', expect.objectContaining({ version: 0, estimatedPalletCount: 2 })) }) it('requires split contacts and both local times, rejecting reversed times', async () => { @@ -366,3 +366,4 @@ describe('overseas business mounted pages', () => { }) }) + diff --git a/src/modules/overseas-business/pages/TransportOrdersView.vue b/src/modules/overseas-business/pages/TransportOrdersView.vue index 4213725d..3efe1462 100644 --- a/src/modules/overseas-business/pages/TransportOrdersView.vue +++ b/src/modules/overseas-business/pages/TransportOrdersView.vue @@ -44,7 +44,7 @@ const looseboardStatusLabels: Record = { PENDING_ const transportTypeLabels: Record = { PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } const statusLabels = computed>(() => isLooseboard.value ? looseboardStatusLabels : truckStatusLabels) const statusTones: Record = { - PENDING_REVIEW: 'warning', PENDING_ACCEPTANCE: 'warning', WAITING_INBOUND: 'warning', + FUNDING_PENDING: 'warning', FUNDING_FAILED: 'danger', PENDING_REVIEW: 'warning', PENDING_ACCEPTANCE: 'warning', WAITING_INBOUND: 'warning', RECEIVING: 'primary', WAITING_CONSOLIDATION: 'warning', PALLET_OVERAGE_REVIEW: 'warning', PARTIALLY_ALLOCATED: 'primary', ALLOCATED: 'primary', PENDING_DISPATCH: 'warning', DISPATCHED: 'primary', IN_TRANSIT: 'primary', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info', diff --git a/src/modules/overseas-business/procurementPresentation.ts b/src/modules/overseas-business/procurementPresentation.ts index 6c45db61..ef41ff65 100644 --- a/src/modules/overseas-business/procurementPresentation.ts +++ b/src/modules/overseas-business/procurementPresentation.ts @@ -2,7 +2,7 @@ import { regionText } from './procurementDirection' import type { Amount, ProcurementFee } from './procurementTypes' export const procurementStatuses: Record = { - PENDING_REVIEW: '待审核', RECEIVING: '收货中', WAIT_CUSTOMER_CONFIRM: '待客户确认', CONFIRMED: '已确认', + FUNDING_PENDING: '订单受理中', FUNDING_FAILED: '订单处理失败', PENDING_REVIEW: '待审核', RECEIVING: '收货中', WAIT_CUSTOMER_CONFIRM: '待客户确认', CONFIRMED: '已确认', IN_TRANSIT: '运输中', DELIVERING: '派送中', COMPLETED: '签收完成', } export const feePurposes: Record = { @@ -12,7 +12,7 @@ export const feePurposes: Record = { export function statusLabel(status: string | null | undefined, direction?: string | null) { return regionText(status ? procurementStatuses[status] || ({ REJECTED: '已拒绝', CANCELLED: '已取消', EXCEPTION_CLOSED: '异常关闭' } as Record)[status] || '待核对进度' : '—', direction) } export function statusTone(status: string | null | undefined): 'success' | 'warning' | 'danger' | 'info' | 'primary' { if (status === 'COMPLETED') return 'success' - if (status === 'REJECTED' || status === 'EXCEPTION_CLOSED') return 'danger' + if ((status === 'REJECTED' || status === 'FUNDING_FAILED') || status === 'EXCEPTION_CLOSED') return 'danger' if (status === 'CANCELLED') return 'info' if (status === 'IN_TRANSIT' || status === 'DELIVERING') return 'primary' return 'warning' diff --git a/src/modules/overseas-business/truckWorkflow.ts b/src/modules/overseas-business/truckWorkflow.ts index e44f8324..e9ce3e8c 100644 --- a/src/modules/overseas-business/truckWorkflow.ts +++ b/src/modules/overseas-business/truckWorkflow.ts @@ -1,7 +1,7 @@ import type { LocalTransportDetail, LocalTransportStatus } from './types' export const truckStatusLabels: Record = { - PENDING_REVIEW: '待审核', WAITING_INBOUND: '待取货', RECEIVING: '待取货', WAITING_CONSOLIDATION: '待取货', + FUNDING_PENDING: '订单受理中', FUNDING_FAILED: '订单处理失败', PENDING_REVIEW: '待审核', WAITING_INBOUND: '待取货', RECEIVING: '待取货', WAITING_CONSOLIDATION: '待取货', PALLET_OVERAGE_REVIEW: '待取货', PARTIALLY_ALLOCATED: '待取货', ALLOCATED: '待取货', PENDING_DISPATCH: '待取货', DISPATCHED: '待取货', IN_TRANSIT: '派送中', COMPLETED: '已完成', REJECTED: '已拒绝', CANCELLED: '已取消', } diff --git a/src/modules/overseas-business/types.ts b/src/modules/overseas-business/types.ts index bb9cc388..7df13176 100644 --- a/src/modules/overseas-business/types.ts +++ b/src/modules/overseas-business/types.ts @@ -5,7 +5,7 @@ export interface OverseasBusinessHistory { operatorId: string | null; changeDetail?: string | null; remark: string | null; createdAt: string } -export type OverseasPurchaseStatus = 'PENDING_REVIEW' | 'RECEIVING' | 'WAIT_CUSTOMER_CONFIRM' | 'CONFIRMED' | 'IN_TRANSIT' | 'DELIVERING' | 'COMPLETED' +export type OverseasPurchaseStatus = 'FUNDING_PENDING' | 'FUNDING_FAILED' | 'PENDING_REVIEW' | 'RECEIVING' | 'WAIT_CUSTOMER_CONFIRM' | 'CONFIRMED' | 'IN_TRANSIT' | 'DELIVERING' | 'COMPLETED' export interface OverseasPurchaseQuery { page?: number; size?: number; customerId?: string; orderNo?: string; customerKeyword?: string purchaseCategory?: string; productName?: string; status?: OverseasPurchaseStatus | '' @@ -29,7 +29,7 @@ export interface OverseasPurchaseEditorDetail { internalRemark: string | null } export type LocalTransportType = 'PORT_PICKUP' | 'WAREHOUSE_TO_WAREHOUSE' | 'WAREHOUSE_TO_DOOR' -export type LocalTransportStatus = 'PENDING_REVIEW' | 'WAITING_INBOUND' | 'RECEIVING' | 'WAITING_CONSOLIDATION' | 'PALLET_OVERAGE_REVIEW' | 'PARTIALLY_ALLOCATED' | 'ALLOCATED' | 'PENDING_DISPATCH' | 'DISPATCHED' | 'IN_TRANSIT' | 'COMPLETED' | 'REJECTED' | 'CANCELLED' +export type LocalTransportStatus = 'FUNDING_PENDING' | 'FUNDING_FAILED' | 'PENDING_REVIEW' | 'WAITING_INBOUND' | 'RECEIVING' | 'WAITING_CONSOLIDATION' | 'PALLET_OVERAGE_REVIEW' | 'PARTIALLY_ALLOCATED' | 'ALLOCATED' | 'PENDING_DISPATCH' | 'DISPATCHED' | 'IN_TRANSIT' | 'COMPLETED' | 'REJECTED' | 'CANCELLED' export type TruckVehicleType = 'PALLET_15' | 'PALLET_30' export interface LocalTransportQuery { page?: number; size?: number; orderNo?: string; routeKeyword?: string; status?: LocalTransportStatus | '' diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index 9e5a0514..595bdaa7 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -74,3 +74,6 @@ export const convertDispatchDraft = (body: ConvertDraft) => { versioned(body); r export interface BatchDispatchCandidate { planPalletId: string; palletNo: string; planId: string; planNo: string; customerId: string; sourceStage: string; packageCount: number | null; allocations: DispatchBatchAllocation[]; blockReasons: string[] } export interface BatchDispatchCandidates { batchKey: string; version: number; candidates: BatchDispatchCandidate[]; blockReasons: string[] } export const getBatchDispatchCandidates = (key: string) => get(`/platform/v1/last-leg/appointment-planning/batches/${encodeURIComponent(key)}/dispatch-candidates`).then(value => { requireBatchVersions(value.candidates.flatMap(row => row.allocations)); return value }) + +/** 退回货物仅使用当前排车服务端给出的原出库候选。 */ +export const getDispatchReturnOptions = (no: string) => get( `${path(no)}/exception-return-options`) diff --git a/src/modules/overseas-warehouse/api/exceptionOrders.ts b/src/modules/overseas-warehouse/api/exceptionOrders.ts index 5d865060..8f51fbac 100644 --- a/src/modules/overseas-warehouse/api/exceptionOrders.ts +++ b/src/modules/overseas-warehouse/api/exceptionOrders.ts @@ -18,7 +18,7 @@ export interface ExceptionReturnSource extends ReturnSourceIdentity { outboundPa 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 ExceptionDetail { order: ExceptionOrderRow; returnSources: ExceptionReturnSource[]; returnReceipts: ExceptionReturnReceipt[]; returnOrderNo?: string | null } export interface ExceptionReceipt extends ReturnSourceIdentity { outboundPalletItemId: 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' @@ -68,3 +68,9 @@ export async function createExceptionOrder(payload: CreateExceptionOrderPayload) const response = await http.post, ApiEnvelope<{ id: string; exceptionNo: string; status: string }>>('/platform/v1/last-leg/exception-orders', payload) return unwrapApiResponse(response) } + +/** 异常单生成关联原单的退回尾程单,现场从PDA列表点单实收。 */ +export async function generateExceptionReturn(customerId: string, number: string, expectedVersion: number) { + return unwrapApiResponse(await http.post, ApiEnvelope<{ orderNo: string }>>(`${exceptionPath(number)}/generate-return-inbound`, + { expectedVersion, idempotencyKey: crypto.randomUUID() }, { params: { customerId } })) +} diff --git a/src/modules/overseas-warehouse/components/DispatchReturnCargo.vue b/src/modules/overseas-warehouse/components/DispatchReturnCargo.vue new file mode 100644 index 00000000..9c3d58ab --- /dev/null +++ b/src/modules/overseas-warehouse/components/DispatchReturnCargo.vue @@ -0,0 +1,39 @@ + + diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts index cb8ab388..d192aa45 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.test.ts @@ -15,7 +15,7 @@ vi.mock('./DispatchResourceFields.vue', () => ({ default: { template: '
' vi.mock('../api/dispatches', () => ({ markDispatchException: vi.fn(), getDispatch: vi.fn(), getDispatchItems: vi.fn(), getDispatchRoute: vi.fn(), reorderDispatchRoute: vi.fn(), removeDispatchItem: vi.fn(), getDispatchLoading: vi.fn(), getDispatchCommand: vi.fn(), listDispatches: vi.fn(), listDispatchCandidates: vi.fn(), completeDispatchLoading: vi.fn(), reviewDispatch: vi.fn(), reviewDispatchLoading: vi.fn(), returnDispatchToReview: vi.fn(), cancelDispatch: vi.fn(), updateDispatch: vi.fn(), addDispatchCargo: vi.fn(), submitDispatchReview: vi.fn() })) vi.mock('../api/transportTasks', () => ({ getTransport: vi.fn() })) import * as transportApi from '../api/transportTasks' -interface State { exception: { exceptionType: string; returnDirection: string; description: string; imageFileIds: string[] }; submit(submitReviewAfterSave?: boolean): Promise; retryError(): Promise; measurements: Array<{ weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null; palletSpaces: number | null }>; form: { reason: string; decision: string; remark: string }; detail: DispatchDetail } +interface State { returnSelections: import('../dispatchContracts').DispatchReturnSelection[]; exception: { exceptionType: string; returnDirection: string; description: string; imageFileIds: string[] }; submit(submitReviewAfterSave?: boolean): Promise; retryError(): Promise; measurements: Array<{ weightKg: number | null; volumeCbm: number | null; deliveryIndex: number | null; palletSpaces: number | null }>; form: { reason: string; decision: string; remark: string }; detail: DispatchDetail } async function open(action: DispatchAction, allowed = true, overrides: Partial = {}) { vi.mocked(api.getDispatch).mockResolvedValue(detail({ ...overrides, actions: { [action]: { allowed, blockCode: null, blockReason: allowed ? null : '前置审核未通过' } } })) const wrapper = mount(DispatchWorkflowDialog, { props: { modelValue: true, dispatchNo: 'PC/1', action }, global: { plugins: [ElementPlus] } }); await flushPromises() @@ -209,6 +209,8 @@ it('requires an exception type and submits optional direction, description and i await state.submit(); expect(api.markDispatchException).not.toHaveBeenCalled() expect(wrapper.text()).toContain('请选择异常类型') Object.assign(state.exception, { exceptionType: 'PARTIAL_RECEIPT', returnDirection: 'OVERSEAS_WAREHOUSE', description: ' 现场部分签收 ', imageFileIds: ['91'] }) + await state.submit(); expect(api.markDispatchException).not.toHaveBeenCalled() + state.returnSelections = [{ deliveryNo: 'DL1', lastMileOrderId: '9', items: [{ outboundPalletItemId: '1', inventoryPositionId: '2', returnCartonId: null, packageCount: 1 }] }] vi.mocked(api.markDispatchException).mockImplementation(async (_, body) => ({ ...command('MARK_EXCEPTION', body.idempotencyKey), stage: 'DELIVERING' })) vi.mocked(api.getDispatch).mockResolvedValue(detail({ stage: 'DELIVERING', recordVersion: 1 })) await state.submit() diff --git a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue index f26827b3..cb5e13fb 100644 --- a/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue +++ b/src/modules/overseas-warehouse/components/DispatchWorkflowDialog.vue @@ -1,7 +1,10 @@