feat(appointment): complete customer receiving and trip continuation workflows

This commit is contained in:
2026-10-16 15:41:56 +08:00
parent 148085286d
commit 45f662ecf2
45 changed files with 2042 additions and 131 deletions
@@ -9,7 +9,7 @@ vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn()
vi.mock('@/modules/overseas-warehouse', async original => ({ ...await original<typeof import('@/modules/overseas-warehouse')>(), getBatchDispatchCandidates: vi.fn(), createDispatchDraftContainer: vi.fn(), listDispatchDrafts: vi.fn(), lockDispatchDraftPallet: vi.fn(), preArrangeDraftDispatch: vi.fn(), releaseDispatchDraft: vi.fn() }))
const candidate: BatchDispatchCandidate = { planPalletId: '9007199254740993', palletNo: 'PL-TEST', planId: 'plan-1', planNo: 'IP-001', customerId: 'customer-a', sourceStage: 'RECEIVED', packageCount: 12, allocations: [{ planPalletId: '9007199254740993', batchKey: 'FBA_APPOINTMENT:1', customerId: 'customer-a', sourceKey: 'BOOKING:9', destinationKey: 'FBA:ONT8', packageCount: 12 }], blockReasons: [] }
let guard: LeaveGuard
function render() { return mount(WorkDispatchForm, { props: { batchKeys: ['FBA_APPOINTMENT:1'], canWrite: true }, global: { provide: { 'appointment-work-leave-guard': (value: LeaveGuard) => { guard = value; return () => undefined } }, stubs: { ElButton: { props: ['disabled'], template: '<button :disabled="disabled"><slot /></button>' }, LoadErrorState: { props: ['message'], template: '<p>{{ message }}</p>' }, ApiErrorMeta: true, TripPreparationDialog: { name: 'TripPreparationDialog', props: ['warehouseId', 'dispatchNos'], template: '<div />' } } } }) }
function render() { return mount(WorkDispatchForm, { props: { batchKeys: ['FBA_APPOINTMENT:1'], canWrite: true }, global: { provide: { 'appointment-work-leave-guard': (value: LeaveGuard) => { guard = value; return () => undefined } }, stubs: { WorkSharedTripForm: { name: 'WorkSharedTripForm', props: ['dispatchNo', 'canWrite'], template: '<div />' }, ElButton: { props: ['disabled'], template: '<button :disabled="disabled"><slot /></button>' }, LoadErrorState: { props: ['message'], template: '<p>{{ message }}</p>' }, ApiErrorMeta: true, TripPreparationDialog: { name: 'TripPreparationDialog', props: ['warehouseId', 'dispatchNos'], template: '<div />' } } } }) }
function button(wrapper: ReturnType<typeof render>, text: string) { return wrapper.findAll('button').find(button => button.text() === text)! }
beforeEach(() => {
vi.clearAllMocks(); access.allowed = true
@@ -17,7 +17,7 @@ beforeEach(() => {
vi.mocked(listDispatchDrafts).mockResolvedValue([])
vi.mocked(createDispatchDraftContainer).mockResolvedValue({ id: 'container-1', capacity: 30, palletIds: [] })
vi.mocked(lockDispatchDraftPallet).mockResolvedValue(null)
vi.mocked(preArrangeDraftDispatch).mockResolvedValue([{ draftContainerId: 'container-1', dispatch: { id: 'internal-dispatch', dispatchNo: 'DS-001', overseasWarehouseId: 'origin-1', overseasWarehouseName: '测试起运仓' } }])
vi.mocked(preArrangeDraftDispatch).mockResolvedValue([{ draftContainerId: 'container-1', dispatch: { id: 'internal-dispatch', dispatchNo: 'DS-001', appointmentBatches: [], overseasWarehouseId: 'origin-1', overseasWarehouseName: '测试起运仓' } }])
})
it('copies exact server allocations into a real draft group and continues into original trip preparation', async () => {
const wrapper = render(); await flushPromises(); await wrapper.get('input[type=checkbox]').setValue(true)
@@ -29,6 +29,23 @@ it('copies exact server allocations into a real draft group and continues into o
prepare.vm.$emit('saved', { trip: { tripNo: 'TR-001' } }); expect(wrapper.emitted('external')).toEqual([['/overseas-master-data/trips/TR-001']])
wrapper.unmount()
})
it('opens quantity splitting for only the current batch without locking physical cargo or creating a new full-source appointment', async () => {
const wrapper = render(); await flushPromises()
await button(wrapper, '按当前批次箱数分批').trigger('click')
expect(wrapper.emitted('batch')).toEqual([['FBA_APPOINTMENT:1']])
expect(lockDispatchDraftPallet).not.toHaveBeenCalled(); expect(preArrangeDraftDispatch).not.toHaveBeenCalled(); wrapper.unmount()
})
it('preselects only eligible physical cargo of the entered batch, never unrelated allocations or unknown quantity', async () => {
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:1', version: 4, blockReasons: [], candidates: [candidate, { ...candidate, planPalletId: 'other-batch', allocations: [{ ...candidate.allocations[0]!, batchKey: 'FBA_APPOINTMENT:999' }] }, { ...candidate, planPalletId: 'unknown', packageCount: null }] })
const wrapper = render(); await flushPromises()
const inputs = wrapper.findAll('input[type=checkbox]')
expect(inputs.map(input => (input.element as HTMLInputElement).checked)).toEqual([true, false, false])
expect(inputs[1]!.attributes('disabled')).toBeDefined()
await button(wrapper, '创建本客户排车单').trigger('click'); await flushPromises()
expect(vi.mocked(preArrangeDraftDispatch).mock.calls[0]![0].groups[0]!.planPalletIds).toEqual([candidate.planPalletId])
expect(vi.mocked(preArrangeDraftDispatch).mock.calls[0]![0].groups[0]!.batchAllocations).toEqual(candidate.allocations)
wrapper.unmount()
})
it('replays a submitted draft without re-locking already dispatched pallets after an unknown response', async () => {
vi.mocked(preArrangeDraftDispatch).mockRejectedValueOnce(new Error('lost response'))
const wrapper = render(); await flushPromises(); await wrapper.get('input[type=checkbox]').setValue(true)
@@ -49,7 +66,7 @@ it('recovers a draft container from the same token when its creation response wa
expect(createDispatchDraftContainer).toHaveBeenCalledTimes(1); expect(preArrangeDraftDispatch).toHaveBeenCalledTimes(1); wrapper.unmount()
})
it('shows server blockers and prevents mixing customers or selecting blocked physical cargo', async () => {
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:1', version: 4, blockReasons: [], candidates: [candidate, { ...candidate, planPalletId: '2', customerId: 'customer-b' }, { ...candidate, planPalletId: '3', blockReasons: ['本托货量超过本批剩余可排量'] }] })
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:1', version: 4, blockReasons: [], candidates: [candidate, { ...candidate, planPalletId: '2', customerId: 'customer-b', allocations: [{ ...candidate.allocations[0]!, customerId: 'customer-b', planPalletId: '2' }] }, { ...candidate, planPalletId: '3', blockReasons: ['本托货量超过本批剩余可排量'] }] })
const wrapper = render(); await flushPromises(); const inputs = wrapper.findAll('input[type=checkbox]')
expect(inputs[2]!.attributes('disabled')).toBeDefined(); expect(wrapper.text()).toContain('本托货量超过本批剩余可排量')
await inputs[0]!.setValue(true); await inputs[1]!.setValue(true)
@@ -61,3 +78,17 @@ it('does not read or write scoped cargo without all original permissions', async
access.allowed = false; const wrapper = render(); await flushPromises()
expect(getBatchDispatchCandidates).not.toHaveBeenCalled(); expect(preArrangeDraftDispatch).not.toHaveBeenCalled(); wrapper.unmount()
})
it('continues the actual later 60-box dispatch through the shared-trip entry without re-creating or reallocating cargo', async () => {
const sixty = { ...candidate, packageCount: 60, allocations: [{ ...candidate.allocations[0]!, packageCount: 60 }] }
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: 'FBA_APPOINTMENT:1', version: 4, candidates: [sixty], blockReasons: [] })
vi.mocked(preArrangeDraftDispatch).mockResolvedValue([{ draftContainerId: 'container-1', dispatch: { id: 'internal-later', dispatchNo: 'DS-60', appointmentBatches: [], overseasWarehouseId: 'origin-1', packageCount: 60 } }])
const wrapper = render(); await flushPromises(); await button(wrapper, '创建本客户排车单').trigger('click'); await flushPromises()
await button(wrapper, '与同仓其他排车单共车').trigger('click'); await flushPromises()
const shared = wrapper.getComponent({ name: 'WorkSharedTripForm' })
expect(shared.props()).toMatchObject({ dispatchNo: 'DS-60', canWrite: true })
expect(preArrangeDraftDispatch).toHaveBeenCalledTimes(1); expect(vi.mocked(preArrangeDraftDispatch).mock.calls[0]![0].groups[0]!.batchAllocations).toEqual(sixty.allocations)
shared.vm.$emit('busy', true); await flushPromises(); expect(button(wrapper, '继续安排其他实物').attributes('disabled')).toBeDefined()
shared.vm.$emit('external', '/overseas-master-data/trips/TR-100'); expect(wrapper.emitted('external')).toEqual([['/overseas-master-data/trips/TR-100']])
wrapper.unmount()
})