fix(dispatch): submit and preserve independent loading groups
This commit is contained in:
@@ -1,10 +1,27 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/shared/api/http'
|
import { http } from '@/shared/api/http'
|
||||||
import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, getDispatchExternalFreight, getDispatchFeeCoverage, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches'
|
import { cancelDispatch, createDispatchDraftContainer, deleteDispatchDraftContainer, getDispatch, getDispatchBolPdf, getDispatchExternalFreight, getDispatchFeeCoverage, listDispatchCandidates, listDispatchDrafts, listDispatches, lockDispatchDraftPallet, preArrangeDispatch, preArrangeDraftDispatch, releaseDispatchDraft, transferDispatchToDelivery, unlockDispatchDraftPallet, updateDispatch } from './dispatches'
|
||||||
|
|
||||||
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } }))
|
vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal<typeof import('@/shared/api/http')>()), http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } }))
|
||||||
|
|
||||||
describe('dispatch overseas API', () => {
|
describe('dispatch overseas API', () => {
|
||||||
|
it('submits all explicit draft groups once and preserves container and source identities', async () => {
|
||||||
|
const payload = { draftToken: 'original-draft', groups: [
|
||||||
|
{ draftContainerId: '190000000000000099', planPalletIds: ['11', '13'], returnReceiptIds: [] },
|
||||||
|
{ draftContainerId: '190000000000000100', planPalletIds: ['12'], returnReceiptIds: ['22'] },
|
||||||
|
] }
|
||||||
|
const result = payload.groups.map((group, index) => ({ draftContainerId: group.draftContainerId, dispatch: { id: String(index), dispatchNo: `DS-${index}` } }))
|
||||||
|
vi.mocked(http.post).mockResolvedValue({ code: '200', data: result })
|
||||||
|
expect(await preArrangeDraftDispatch(payload)).toEqual(result)
|
||||||
|
expect(http.post).toHaveBeenCalledExactlyOnceWith('/platform/v1/last-leg/dispatches/drafts/pre-arrange', payload)
|
||||||
|
})
|
||||||
|
it('propagates a batch rejection without falling back to a flat or repeated command', async () => {
|
||||||
|
vi.mocked(http.post).mockResolvedValue({ code: 'A0409', message: '草稿来源已变化', data: null })
|
||||||
|
await expect(preArrangeDraftDispatch({ draftToken: 'original-draft', groups: [
|
||||||
|
{ draftContainerId: 'container', planPalletIds: ['11'], returnReceiptIds: [] },
|
||||||
|
] })).rejects.toThrow('草稿来源已变化')
|
||||||
|
expect(http.post).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
it('locks and submits actual return receipts without pretending they are plan pallets', async () => {
|
it('locks and submits actual return receipts without pretending they are plan pallets', async () => {
|
||||||
vi.mocked(http.post).mockResolvedValue({ code: '200', data: null })
|
vi.mocked(http.post).mockResolvedValue({ code: '200', data: null })
|
||||||
await lockDispatchDraftPallet('RETURN:190000000000000099', 'draft-return', 'container-1')
|
await lockDispatchDraftPallet('RETURN:190000000000000099', 'draft-return', 'container-1')
|
||||||
|
|||||||
@@ -71,6 +71,20 @@ export interface PreArrangeDispatchPayload {
|
|||||||
relabelNo?: string
|
relabelNo?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreArrangeDispatchDraftGroup {
|
||||||
|
draftContainerId: string
|
||||||
|
planPalletIds: string[]
|
||||||
|
returnReceiptIds: string[]
|
||||||
|
}
|
||||||
|
export interface PreArrangeDispatchDraftPayload {
|
||||||
|
draftToken: string
|
||||||
|
groups: PreArrangeDispatchDraftGroup[]
|
||||||
|
}
|
||||||
|
export interface PreArrangeDispatchDraftResult {
|
||||||
|
draftContainerId: string
|
||||||
|
dispatch: DispatchSummaryDto
|
||||||
|
}
|
||||||
|
|
||||||
export interface DispatchSummaryDto {
|
export interface DispatchSummaryDto {
|
||||||
id: string
|
id: string
|
||||||
dispatchNo: string
|
dispatchNo: string
|
||||||
@@ -202,6 +216,13 @@ export async function preArrangeDispatch(payload: PreArrangeDispatchPayload) {
|
|||||||
return unwrapApiResponse(response)
|
return unwrapApiResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function preArrangeDraftDispatch(payload: PreArrangeDispatchDraftPayload) {
|
||||||
|
const response = await http.post<ApiEnvelope<PreArrangeDispatchDraftResult[]>, ApiEnvelope<PreArrangeDispatchDraftResult[]>>(
|
||||||
|
'/platform/v1/last-leg/dispatches/drafts/pre-arrange', payload,
|
||||||
|
)
|
||||||
|
return unwrapApiResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
function draftSource(selection: string) {
|
function draftSource(selection: string) {
|
||||||
return selection.startsWith('RETURN:')
|
return selection.startsWith('RETURN:')
|
||||||
? { sourceType: 'RETURN_RECEIPT', returnReceiptId: selection.slice(7) }
|
? { sourceType: 'RETURN_RECEIPT', returnReceiptId: selection.slice(7) }
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { defineComponent, h } from 'vue'
|
|||||||
import { beforeEach, expect, it, vi } from 'vitest'
|
import { beforeEach, expect, it, vi } from 'vitest'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import SmartDispatchDialog from './SmartDispatchDialog.vue'
|
import SmartDispatchDialog from './SmartDispatchDialog.vue'
|
||||||
import { listDispatchCandidates, listDispatchDrafts, preArrangeDispatch, type DispatchPalletCandidateDto } from '../api/dispatches'
|
import { listDispatchCandidates, listDispatchDrafts, preArrangeDispatch, preArrangeDraftDispatch, unlockDispatchDraftPallet, deleteDispatchDraftContainer, type DispatchPalletCandidateDto, type DispatchDraftDto } from '../api/dispatches'
|
||||||
|
|
||||||
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
|
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
|
||||||
vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn(), error: vi.fn() } }))
|
vi.mock('element-plus', () => ({ ElMessage: { warning: vi.fn(), success: vi.fn(), error: vi.fn() } }))
|
||||||
vi.mock('../api/dispatches', () => ({
|
vi.mock('../api/dispatches', () => ({
|
||||||
listDispatchCandidates: vi.fn(), listDispatchDrafts: vi.fn(), preArrangeDispatch: vi.fn(),
|
listDispatchCandidates: vi.fn(), listDispatchDrafts: vi.fn(), preArrangeDispatch: vi.fn(), preArrangeDraftDispatch: vi.fn(),
|
||||||
createDispatchDraftContainer: vi.fn(), deleteDispatchDraftContainer: vi.fn(),
|
createDispatchDraftContainer: vi.fn(), deleteDispatchDraftContainer: vi.fn(),
|
||||||
lockDispatchDraftPallet: vi.fn(), unlockDispatchDraftPallet: vi.fn(),
|
lockDispatchDraftPallet: vi.fn(), unlockDispatchDraftPallet: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@@ -27,7 +27,173 @@ const returned: DispatchPalletCandidateDto = { orderId: '2', orderNo: 'LM-B', re
|
|||||||
|
|
||||||
beforeEach(() => vi.resetAllMocks())
|
beforeEach(() => vi.resetAllMocks())
|
||||||
|
|
||||||
|
function mountDraft() {
|
||||||
|
return shallowMount(SmartDispatchDialog, { props: { modelValue: true, orders: [] }, global: {
|
||||||
|
stubs: { AppDialog: shell, ElButton: button, LoadErrorState: retry, ElDialog: true,
|
||||||
|
ElInput: true, ElOption: true, ElSelect: true, ElTable: true, ElTableColumn: true,
|
||||||
|
ElTag: true, ElDropdown: true, ElDropdownMenu: true, ElDropdownItem: true,
|
||||||
|
ElEmpty: true, ElRadio: true, ElRadioGroup: true },
|
||||||
|
} })
|
||||||
|
}
|
||||||
|
const secondPlan = { ...plan, orderId: '3', orderNo: 'LM-C', planPalletId: '13', palletNo: 'PAL-C' }
|
||||||
|
const thirdPlan = { ...plan, orderId: '4', orderNo: 'LM-D', planPalletId: '14', palletNo: 'PAL-D' }
|
||||||
|
const twoGroups: DispatchDraftDto = { draftToken: 'original-draft', containers: [
|
||||||
|
{ id: 'container-1', capacity: 30, palletIds: ['11', '13'] },
|
||||||
|
{ id: 'container-2', capacity: 30, palletIds: ['14', 'RETURN:22'] },
|
||||||
|
{ id: 'empty-container', capacity: 15, palletIds: [] },
|
||||||
|
] }
|
||||||
|
const expectedBatch = { draftToken: 'original-draft', groups: [
|
||||||
|
{ draftContainerId: 'container-1', planPalletIds: ['11', '13'], returnReceiptIds: [] },
|
||||||
|
{ draftContainerId: 'container-2', planPalletIds: ['14'], returnReceiptIds: ['22'] },
|
||||||
|
] }
|
||||||
|
const goodResult = [{ draftContainerId: 'container-2', dispatch: { id: '102', dispatchNo: 'DS-2' } },
|
||||||
|
{ draftContainerId: 'container-1', dispatch: { id: '101', dispatchNo: 'DS-1' } }]
|
||||||
|
async function submitDraft(wrapper: ReturnType<typeof mountDraft>) {
|
||||||
|
await wrapper.findAll('button').find(node => node.text() === '预排车')!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
}
|
||||||
|
|
||||||
|
it('keeps two explicit groups in one atomic request, omits empty containers and accepts reordered matching results', async () => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
||||||
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue(goodResult)
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith(expectedBatch)
|
||||||
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['one collapsed result', goodResult.slice(0, 1)],
|
||||||
|
['foreign container', [{ ...goodResult[0], draftContainerId: 'foreign' }, goodResult[1]]],
|
||||||
|
['duplicate container', [goodResult[0], goodResult[0]]],
|
||||||
|
['duplicate dispatch', [goodResult[0], { ...goodResult[1], dispatch: goodResult[0].dispatch }]],
|
||||||
|
['missing business number', [goodResult[0], { ...goodResult[1], dispatch: { id: '101', dispatchNo: '' } }]],
|
||||||
|
])('retains the original groups and token after %s', async (_label, result) => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
||||||
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue(result)
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
expect(wrapper.emitted('created')).toBeUndefined()
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
expect(wrapper.text()).toContain('托盘 4 · 箱数 8')
|
||||||
|
await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDraftDispatch).toHaveBeenNthCalledWith(2, expectedBatch)
|
||||||
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retains all groups on batch rejection without issuing a flat request', async () => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
||||||
|
vi.mocked(preArrangeDraftDispatch).mockRejectedValue(new Error('草稿来源已变化'))
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDraftDispatch).toHaveBeenCalledTimes(1)
|
||||||
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(wrapper.emitted('created')).toBeUndefined()
|
||||||
|
expect(wrapper.text()).toContain('托盘 4 · 箱数 8')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not submit a physical pallet fallback when its actual plan source identity is missing', async () => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [
|
||||||
|
{ id: 'container', capacity: 30, palletIds: ['101'] },
|
||||||
|
] }])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([{ ...plan, planPalletId: undefined }])
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('来源标识缺失'))
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not send a second batch while the original command is pending', async () => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
||||||
|
let resolveCommand!: (result: typeof goodResult) => void
|
||||||
|
vi.mocked(preArrangeDraftDispatch).mockReturnValue(new Promise(resolve => { resolveCommand = resolve }))
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper); await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith(expectedBatch)
|
||||||
|
expect(wrapper.emitted('created')).toBeUndefined()
|
||||||
|
resolveCommand(goodResult); await flushPromises()
|
||||||
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps group membership frozen while the submitted batch is awaiting its response', async () => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan, thirdPlan, returned])
|
||||||
|
let resolveCommand!: (result: typeof goodResult) => void
|
||||||
|
vi.mocked(preArrangeDraftDispatch).mockReturnValue(new Promise(resolve => { resolveCommand = resolve }))
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
await wrapper.findAll('button').find(node => node.text() === '移除')!.trigger('click')
|
||||||
|
await wrapper.findAll('button').find(node => node.attributes('aria-label') === '删除装车车型1')!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(unlockDispatchDraftPallet).not.toHaveBeenCalled()
|
||||||
|
expect(deleteDispatchDraftContainer).not.toHaveBeenCalled()
|
||||||
|
expect(wrapper.text()).toContain('托盘 4 · 箱数 8')
|
||||||
|
resolveCommand(goodResult); await flushPromises()
|
||||||
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retains unresolved sources when retry hydration still cannot recover the complete original draft', async () => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([twoGroups])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, returned])
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('原选择已保留'))
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ ...twoGroups, containers: [{ id: 'c1', capacity: 30, palletIds: ['11'] }, { id: 'c2', capacity: 30, palletIds: ['11'] }] },
|
||||||
|
{ ...twoGroups, containers: [{ id: 'c1', capacity: 30, palletIds: ['11'] }, { id: 'c1', capacity: 30, palletIds: ['13'] }] },
|
||||||
|
{ ...twoGroups, containers: [{ id: '', capacity: 30, palletIds: ['11'] }] },
|
||||||
|
])('rejects duplicate sources or invalid container identities before writing', async draft => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([draft])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan])
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a relabel order split across multiple containers before either API command', async () => {
|
||||||
|
const relabel: DispatchPalletCandidateDto = { ...plan, sourceStage: 'RELABEL', orderRefs: 'RL-1' }
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [
|
||||||
|
{ id: 'c1', capacity: 30, palletIds: ['11'] }, { id: 'c2', capacity: 30, palletIds: ['13'] },
|
||||||
|
] }])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([relabel, { ...secondPlan, sourceStage: 'RELABEL', orderRefs: 'RL-1' }])
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('一组排车'))
|
||||||
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps one complete relabel order on the single-dispatch endpoint', async () => {
|
||||||
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [
|
||||||
|
{ id: 'c1', capacity: 30, palletIds: ['11', '13'] },
|
||||||
|
] }])
|
||||||
|
vi.mocked(listDispatchCandidates).mockResolvedValue([plan, secondPlan].map(p => ({ ...p, sourceStage: 'RELABEL', orderRefs: 'RL-1' })))
|
||||||
|
vi.mocked(preArrangeDispatch).mockResolvedValue({ id: '1', dispatchNo: 'DS-RL' })
|
||||||
|
const wrapper = mountDraft(); await flushPromises(); await submitDraft(wrapper)
|
||||||
|
expect(preArrangeDispatch).toHaveBeenCalledExactlyOnceWith({ planPalletIds: ['11', '13'], returnReceiptIds: [], relabelNo: 'RL-1' })
|
||||||
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
it('retains every restored selection on candidate failure and submits both sources only after retry hydration', async () => {
|
it('retains every restored selection on candidate failure and submits both sources only after retry hydration', async () => {
|
||||||
|
vi.mocked(preArrangeDraftDispatch).mockResolvedValue([{ draftContainerId: 'container', dispatch: { id: '1', dispatchNo: 'DS-1' } }])
|
||||||
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [{ id: 'container', capacity: 30, palletIds: ['11', 'RETURN:22'] }] }])
|
vi.mocked(listDispatchDrafts).mockResolvedValue([{ draftToken: 'original-draft', containers: [{ id: 'container', capacity: 30, palletIds: ['11', 'RETURN:22'] }] }])
|
||||||
vi.mocked(listDispatchCandidates)
|
vi.mocked(listDispatchCandidates)
|
||||||
.mockResolvedValueOnce([plan])
|
.mockResolvedValueOnce([plan])
|
||||||
@@ -45,13 +211,15 @@ it('retains every restored selection on candidate failure and submits both sourc
|
|||||||
await submit().trigger('click')
|
await submit().trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
|
expect(preArrangeDraftDispatch).not.toHaveBeenCalled()
|
||||||
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('原选择已保留'))
|
expect(ElMessage.warning).toHaveBeenCalledWith(expect.stringContaining('原选择已保留'))
|
||||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||||
await wrapper.get('[data-test="retry"]').trigger('click')
|
await wrapper.get('[data-test="retry"]').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
await submit().trigger('click')
|
await submit().trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(preArrangeDispatch).toHaveBeenCalledExactlyOnceWith({ planPalletIds: ['11'], returnReceiptIds: ['22'], draftToken: 'original-draft' })
|
expect(preArrangeDraftDispatch).toHaveBeenCalledExactlyOnceWith({ draftToken: 'original-draft', groups: [{ draftContainerId: 'container', planPalletIds: ['11'], returnReceiptIds: ['22'] }] })
|
||||||
|
expect(preArrangeDispatch).not.toHaveBeenCalled()
|
||||||
expect(listDispatchCandidates).toHaveBeenLastCalledWith({ draftToken: 'original-draft' })
|
expect(listDispatchCandidates).toHaveBeenLastCalledWith({ draftToken: 'original-draft' })
|
||||||
expect(wrapper.emitted('created')).toHaveLength(1)
|
expect(wrapper.emitted('created')).toHaveLength(1)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|||||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||||
import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
|
import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, preArrangeDraftDispatch, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
|
||||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||||
import type { InboundOrderListItem } from '../types'
|
import type { InboundOrderListItem } from '../types'
|
||||||
import { dispatchVehicleSpecs } from '../dispatchVehicleSpecs'
|
import { dispatchVehicleSpecs } from '../dispatchVehicleSpecs'
|
||||||
@@ -198,10 +198,12 @@ function reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addContainer() {
|
function addContainer() {
|
||||||
|
if (saving.value) return
|
||||||
selectedVehicle.value = ''
|
selectedVehicle.value = ''
|
||||||
vehicleDialogOpen.value = true
|
vehicleDialogOpen.value = true
|
||||||
}
|
}
|
||||||
async function confirmAddContainer() {
|
async function confirmAddContainer() {
|
||||||
|
if (saving.value) return
|
||||||
const spec = selectedVehicleSpec.value
|
const spec = selectedVehicleSpec.value
|
||||||
if (!spec) {
|
if (!spec) {
|
||||||
ElMessage.warning('请选择车型')
|
ElMessage.warning('请选择车型')
|
||||||
@@ -224,6 +226,7 @@ async function confirmAddContainer() {
|
|||||||
|
|
||||||
async function movePallet(item: DispatchPalletCandidateDto, containerId: string) {
|
async function movePallet(item: DispatchPalletCandidateDto, containerId: string) {
|
||||||
const id = palletId(item)
|
const id = palletId(item)
|
||||||
|
if (saving.value || busyPalletIds.value.includes(id)) return
|
||||||
const target = containers.value.find((container) => container.id === containerId)
|
const target = containers.value.find((container) => container.id === containerId)
|
||||||
if (!target || target.palletIds.includes(id)) return
|
if (!target || target.palletIds.includes(id)) return
|
||||||
if (!String(item.overseasWarehouseId || '').trim()) {
|
if (!String(item.overseasWarehouseId || '').trim()) {
|
||||||
@@ -267,6 +270,7 @@ async function movePallet(item: DispatchPalletCandidateDto, containerId: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removePallet(id: string) {
|
async function removePallet(id: string) {
|
||||||
|
if (saving.value || busyPalletIds.value.includes(id)) return
|
||||||
const owner = containerFor(id)
|
const owner = containerFor(id)
|
||||||
if (!owner) return
|
if (!owner) return
|
||||||
busyPalletIds.value.push(id)
|
busyPalletIds.value.push(id)
|
||||||
@@ -282,6 +286,7 @@ async function removePallet(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteContainer(container: LoadingContainer) {
|
async function deleteContainer(container: LoadingContainer) {
|
||||||
|
if (saving.value) return
|
||||||
for (const id of [...container.palletIds]) {
|
for (const id of [...container.palletIds]) {
|
||||||
await removePallet(id)
|
await removePallet(id)
|
||||||
}
|
}
|
||||||
@@ -300,10 +305,16 @@ function dropInto(event: DragEvent, containerId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function close(value = false) {
|
async function close(value = false) {
|
||||||
|
if (saving.value) return
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
|
if (saving.value) return
|
||||||
|
if (busyPalletIds.value.length) {
|
||||||
|
ElMessage.warning('托盘来源正在锁定,请稍后再预排车')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (loading.value || unresolvedAssignedIds.value.length) {
|
if (loading.value || unresolvedAssignedIds.value.length) {
|
||||||
ElMessage.warning('草稿中部分已选来源尚未恢复,请重试加载后再预排;原选择已保留')
|
ElMessage.warning('草稿中部分已选来源尚未恢复,请重试加载后再预排;原选择已保留')
|
||||||
return
|
return
|
||||||
@@ -312,6 +323,18 @@ async function submit() {
|
|||||||
ElMessage.warning('请至少移入一个托盘')
|
ElMessage.warning('请至少移入一个托盘')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const selectedContainers = containers.value.filter(container => container.palletIds.length > 0)
|
||||||
|
if (new Set(assignedIds.value).size !== assignedIds.value.length
|
||||||
|
|| selectedContainers.some(container => !container.id.trim())
|
||||||
|
|| new Set(selectedContainers.map(container => container.id)).size !== selectedContainers.length) {
|
||||||
|
ElMessage.warning('草稿中存在重复托盘或无效装车容器,请核对分组;原草稿已保留')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (assigned.value.some(item => item.sourceType === 'RETURN_RECEIPT'
|
||||||
|
? !String(item.returnReceiptId || '').trim() : !String(item.planPalletId || '').trim())) {
|
||||||
|
ElMessage.warning('所选托盘来源标识缺失,请重新加载核对;原草稿已保留')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (assigned.value.some((item) => !String(item.overseasWarehouseId || '').trim())) {
|
if (assigned.value.some((item) => !String(item.overseasWarehouseId || '').trim())) {
|
||||||
ElMessage.warning('所选托盘中存在尚未关联海外仓的托盘')
|
ElMessage.warning('所选托盘中存在尚未关联海外仓的托盘')
|
||||||
return
|
return
|
||||||
@@ -326,14 +349,39 @@ async function submit() {
|
|||||||
ElMessage.warning('一张换标单的托盘需要单独排车')
|
ElMessage.warning('一张换标单的托盘需要单独排车')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (relabelOrders.length && selectedContainers.length !== 1) {
|
||||||
|
ElMessage.warning('一张换标单仅支持一组排车,请将全部托盘移入同一个装车容器')
|
||||||
|
return
|
||||||
|
}
|
||||||
saving.value = true
|
saving.value = true
|
||||||
mutationError.value = null
|
mutationError.value = null
|
||||||
try {
|
try {
|
||||||
await preArrangeDispatch({
|
if (relabelOrders.length) {
|
||||||
planPalletIds: assigned.value.filter(item => item.sourceType !== 'RETURN_RECEIPT').map(palletId),
|
await preArrangeDispatch({
|
||||||
returnReceiptIds: assigned.value.filter(item => item.sourceType === 'RETURN_RECEIPT').map(item => String(item.returnReceiptId)),
|
planPalletIds: assigned.value.filter(item => item.sourceType !== 'RETURN_RECEIPT').map(palletId),
|
||||||
...(relabelOrders.length ? { relabelNo: relabelOrders[0] } : { draftToken: ensureDraftToken() }),
|
returnReceiptIds: assigned.value.filter(item => item.sourceType === 'RETURN_RECEIPT').map(item => String(item.returnReceiptId)),
|
||||||
})
|
relabelNo: relabelOrders[0],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const groups = selectedContainers.map(container => {
|
||||||
|
const sources = container.palletIds.map(id => palletSnapshot(id)!)
|
||||||
|
return {
|
||||||
|
draftContainerId: container.id,
|
||||||
|
planPalletIds: sources.filter(item => item.sourceType !== 'RETURN_RECEIPT').map(item => String(item.planPalletId)),
|
||||||
|
returnReceiptIds: sources.filter(item => item.sourceType === 'RETURN_RECEIPT').map(item => String(item.returnReceiptId)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const results = await preArrangeDraftDispatch({ draftToken: ensureDraftToken(), groups })
|
||||||
|
const expectedIds = new Set(groups.map(group => group.draftContainerId))
|
||||||
|
if (!Array.isArray(results) || results.length !== groups.length
|
||||||
|
|| results.some(result => !result || typeof result.draftContainerId !== 'string'
|
||||||
|
|| typeof result.dispatch?.dispatchNo !== 'string' || !result.dispatch.dispatchNo.trim())
|
||||||
|
|| new Set(results.map(result => result.draftContainerId)).size !== groups.length
|
||||||
|
|| results.some(result => !expectedIds.has(result.draftContainerId))
|
||||||
|
|| new Set(results.map(result => result.dispatch.dispatchNo)).size !== groups.length) {
|
||||||
|
throw new Error('预排车返回的分组结果与原草稿不一致,原草稿已保留,请核对保存结果')
|
||||||
|
}
|
||||||
|
}
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
emit('created')
|
emit('created')
|
||||||
ElMessage.success('排车明细已同步到预排列表')
|
ElMessage.success('排车明细已同步到预排列表')
|
||||||
@@ -541,14 +589,14 @@ watch([warehouseTab, () => query.orderType], () => {
|
|||||||
><span
|
><span
|
||||||
>托数 <b>{{ container.palletIds.length }}</b
|
>托数 <b>{{ container.palletIds.length }}</b
|
||||||
>/{{ container.capacity }} · 重量 <b>{{ formatWeightLb(containerWeightLb(container)) }}</b>/{{ container.maxWeightLb }}LB <em v-if="containerOverweight(container)">超限10%</em></span
|
>/{{ container.capacity }} · 重量 <b>{{ formatWeightLb(containerWeightLb(container)) }}</b>/{{ container.maxWeightLb }}LB <em v-if="containerOverweight(container)">超限10%</em></span
|
||||||
><el-button class="density-action" data-tone="danger" :aria-label="`删除装车车型${index + 1}`" @click="deleteContainer(container)">删除</el-button>
|
><el-button class="density-action" data-tone="danger" :disabled="saving" :aria-label="`删除装车车型${index + 1}`" @click="deleteContainer(container)">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="container-body">
|
<div class="container-body">
|
||||||
<div v-for="id in container.palletIds" :key="id" class="loaded-pallet">
|
<div v-for="id in container.palletIds" :key="id" class="loaded-pallet">
|
||||||
<span
|
<span
|
||||||
><strong>{{ text(palletSnapshot(id)?.palletNo || '托盘号待补充') }}</strong
|
><strong>{{ text(palletSnapshot(id)?.palletNo || '托盘号待补充') }}</strong
|
||||||
><OrderReferenceText :value="text(palletSnapshot(id)?.orderNo)" /></span
|
><OrderReferenceText :value="text(palletSnapshot(id)?.orderNo)" /></span
|
||||||
><el-button class="density-action" data-tone="danger" :loading="busyPalletIds.includes(id)" @click="removePallet(id)">移除</el-button>
|
><el-button class="density-action" data-tone="danger" :disabled="saving" :loading="busyPalletIds.includes(id)" @click="removePallet(id)">移除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="!container.palletIds.length" class="drop-hint">拖入托盘,移入后立即锁定</span>
|
<span v-if="!container.palletIds.length" class="drop-hint">拖入托盘,移入后立即锁定</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -560,7 +608,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
|||||||
|
|
||||||
<template #footer><footer class="dialog-footer">
|
<template #footer><footer class="dialog-footer">
|
||||||
<span>托盘移入即锁定,重量按 1kg = 2.2046lb 换算。</span>
|
<span>托盘移入即锁定,重量按 1kg = 2.2046lb 换算。</span>
|
||||||
<div><el-button :disabled="saving" @click="close(false)">取消</el-button><el-button type="primary" :loading="saving" :disabled="!assignedIds.length" @click="submit">预排车</el-button></div>
|
<div><el-button :disabled="saving" @click="close(false)">取消</el-button><el-button type="primary" :loading="saving" :disabled="!assignedIds.length || busyPalletIds.length > 0" @click="submit">预排车</el-button></div>
|
||||||
</footer></template>
|
</footer></template>
|
||||||
</AppDialog>
|
</AppDialog>
|
||||||
<el-dialog class="admin-density-dialog" v-model="vehicleDialogOpen" title="选择车型" width="360px" append-to-body>
|
<el-dialog class="admin-density-dialog" v-model="vehicleDialogOpen" title="选择车型" width="360px" append-to-body>
|
||||||
|
|||||||
Reference in New Issue
Block a user