feat(admin): support indivisible coload groups
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import ElementPlus, { ElMessageBox } from 'element-plus'
|
||||
import { AxiosError } from 'axios'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
@@ -7,13 +7,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import SmartLoadingWorkbenchView from './SmartLoadingWorkbenchView.vue'
|
||||
import { getContainer, getSmartLoadingPlanByBooking, listSmartLoadingCandidates, recommendSmartLoading } from '../api/platformContainerLoading'
|
||||
import { assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, getSmartLoadingPlanByBooking, listSmartLoadingCandidates, recommendSmartLoading, removeSmartLoadingGroup } from '../api/platformContainerLoading'
|
||||
import { batchQueryPlatformCoLoadGroups } from '@/modules/unified-orders'
|
||||
|
||||
vi.mock('../api/platformContainerLoading', () => ({
|
||||
getContainer: vi.fn(), getSmartLoadingPlanByBooking: vi.fn(), listSmartLoadingCandidates: vi.fn(),
|
||||
createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), recommendSmartLoading: vi.fn(),
|
||||
adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), pushSmartLoadingPreload: vi.fn(),
|
||||
createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), assignSmartLoadingGroup: vi.fn(), recommendSmartLoading: vi.fn(),
|
||||
adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), removeSmartLoadingGroup: vi.fn(), pushSmartLoadingPreload: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders', () => ({ batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]) }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
|
||||
const container = {
|
||||
@@ -30,6 +32,7 @@ const planResponse = {
|
||||
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED' as const,
|
||||
pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null, items: [],
|
||||
}
|
||||
const candidate = { accountId: '1', inboundId: '2', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsSummary: '服装', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }
|
||||
|
||||
describe('SmartLoadingWorkbenchView', () => {
|
||||
beforeEach(() => {
|
||||
@@ -37,7 +40,7 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(getContainer).mockResolvedValue(container)
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(planResponse)
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '2', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsSummary: '服装', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4 }])
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([candidate])
|
||||
})
|
||||
|
||||
it('restores the real draft and explains candidate capacity', async () => {
|
||||
@@ -114,7 +117,86 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
|
||||
it('keeps the package count editor inside its table cell without overflow ellipsis', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue'), 'utf8')
|
||||
expect(source).toContain('<div class="package-count-editor"><el-input-number')
|
||||
expect(source).toContain('<div v-else class="package-count-editor"><el-input-number')
|
||||
expect(source).toContain('.workbench :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }')
|
||||
})
|
||||
|
||||
it('aggregates a two-order group and calls only the atomic whole-group assignment', async () => {
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([
|
||||
{ ...candidate, inboundId: '201', orderId: '301', inboundCode: 'IN-201', availablePackageCount: 4, availableVolumeCbm: 0.8, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 2, groupTotalBoxes: 10, locked: false, blockReason: null },
|
||||
{ ...candidate, accountId: '2', inboundId: '202', orderId: '302', inboundCode: 'IN-202', availablePackageCount: 6, availableVolumeCbm: 1.2, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 2, groupTotalBoxes: 10, locked: false, blockReason: null },
|
||||
])
|
||||
vi.mocked(assignSmartLoadingGroup).mockResolvedValue(planResponse)
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] })
|
||||
await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady()
|
||||
const wrapper = mount(SmartLoadingWorkbenchView, { attachTo: document.body, global: { plugins: [ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('CLG-9007199254740999 · 2 票')
|
||||
expect(wrapper.text()).toContain('整组 10 箱')
|
||||
expect(wrapper.findAll('button').filter((button) => button.text().includes('整组分配'))).toHaveLength(1)
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('整组分配'))!.trigger('click')
|
||||
await flushPromises()
|
||||
;([...document.body.querySelectorAll('button')].find((button) => button.textContent?.includes('确认整组分配')) as HTMLButtonElement).click()
|
||||
await flushPromises()
|
||||
expect(assignSmartLoadingGroup).toHaveBeenCalledWith(planResponse.id, '201', planResponse.version)
|
||||
expect(assignSmartLoadingItem).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('shows an exact three-member group block and prevents individual assignment', async () => {
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(['401', '402', '403'].map((inboundId, index) => ({
|
||||
...candidate, inboundId, orderId: `5${index}`, inboundCode: `IN-${inboundId}`, coLoadGroupId: '9900', groupNo: 'CLG-9900',
|
||||
memberCount: 3, groupTotalBoxes: 18, locked: false, blockReason: '同柜不可拆组成员候选不齐,缺少 1 票',
|
||||
})))
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] })
|
||||
await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady()
|
||||
const wrapper = mount(SmartLoadingWorkbenchView, { global: { plugins: [ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('CLG-9900 · 3 票')
|
||||
expect(wrapper.text()).toContain('同柜不可拆组成员候选不齐,缺少 1 票')
|
||||
const button = wrapper.findAll('button').find((item) => item.text().includes('整组分配'))!
|
||||
expect(button.attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.text()).not.toContain('本次计划装柜箱数')
|
||||
})
|
||||
|
||||
it('keeps an ordinary ungrouped inbound on the existing partial-count assignment', async () => {
|
||||
vi.mocked(assignSmartLoadingItem).mockResolvedValue(planResponse)
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] })
|
||||
await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady()
|
||||
const wrapper = mount(SmartLoadingWorkbenchView, { attachTo: document.body, global: { plugins: [ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
await wrapper.findAll('button').find((button) => button.text() === '分配')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(document.body.textContent).toContain('本次计划装柜箱数')
|
||||
;([...document.body.querySelectorAll('button')].find((button) => button.textContent?.trim() === '确认分配') as HTMLButtonElement).click()
|
||||
await flushPromises()
|
||||
expect(assignSmartLoadingItem).toHaveBeenCalledWith(planResponse.id, { inboundId: '2', packageCount: 1, expectedVersion: planResponse.version })
|
||||
expect(assignSmartLoadingGroup).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('removes a locked group once instead of looping through member items', async () => {
|
||||
const items = ['201', '202'].map((inboundId, index) => ({
|
||||
id: `70${index}`, loadingPlanId: '99', accountId: `${index + 1}`, inboundId, inboundDetailId: `80${index}`, orderId: `30${index}`,
|
||||
orderNo: `ORD-${index}`, inboundCode: `IN-${inboundId}`, customerName: `客户${index}`, destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9',
|
||||
skuCode: `SKU-${index}`, productNameZh: `货物${index}`, packageCount: 5, productQuantity: 50, grossWeightKg: 50, volumeCbm: 1,
|
||||
loadSequence: index + 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true,
|
||||
}))
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...planResponse, items })
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue(items.map((item) => ({ orderId: item.orderId, inboundId: item.inboundId, group: {
|
||||
groupId: '9900', groupNo: 'CLG-9900', status: 'LOCKED', warehouseId: '8', channelCode: 'ZBYHK', destinationType: 'WAREHOUSE', destinationWarehouseId: '9', destinationCode: 'LAX9', lockedPlanId: '99', version: 1,
|
||||
members: items.map((member) => ({ tenantId: member.accountId, orderId: member.orderId, inboundId: member.inboundId, fullPackageCount: 5 })), fullPackageCount: 10, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00',
|
||||
} })))
|
||||
vi.mocked(removeSmartLoadingGroup).mockResolvedValue({ ...planResponse, version: 3, items: [] })
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] })
|
||||
await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady()
|
||||
const wrapper = mount(SmartLoadingWorkbenchView, { global: { plugins: [ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('CLG-9900 · 2 票')
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('移除整组'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(removeSmartLoadingGroup).toHaveBeenCalledWith('99', '9900', 2)
|
||||
expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('组内成员不能单独移除'), '移除同柜不可拆组', expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user