464 lines
29 KiB
JavaScript
464 lines
29 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
const source = readFileSync(new URL('../src/pages/outbound/outbound.vue', import.meta.url), 'utf8').match(/<script>([\s\S]*?)<\/script>/)[1]
|
|
const deferred = () => { let resolve; const promise = new Promise(done => { resolve = done }); return { promise, resolve } }
|
|
function page(overrides = {}) {
|
|
const calls = []
|
|
const deps = { PortalHeader: {}, ScanTargetInput: {}, session: { currentUser: {} }, fulfillment: { outbound: { packages: 1, currentTaskNo: 'OLD-TASK' } },
|
|
updateFulfillmentDraft() {}, fulfillmentIdempotencyKey: () => ({ value: 'key' }),
|
|
playErrorSound() {}, playSuccessSound() {}, pickOutboundPallet: async () => calls.push('pick'), completeOutboundTask: async () => calls.push('complete'),
|
|
listCargoDiscardPhotos: async () => [], getOutboundTask: async () => ({ taskNo: 'TASK', status: 'COMPLETED', items: [] }), normalizeCargoDiscardTask: value => value, resolveCargoDiscardPallet: async code => vm.discardTasks.find(task => task.palletNo === code || task.cartonMarks?.includes(code)),
|
|
...overrides }
|
|
const definition = new Function(...Object.keys(deps), source.replace(/^\s*import .*$/gm, '').replace('export default', 'return'))(...Object.values(deps))
|
|
const vm = definition.data()
|
|
for (const [name, fn] of Object.entries(definition.methods)) vm[name] = fn.bind(vm)
|
|
for (const [name, fn] of Object.entries(definition.computed)) Object.defineProperty(vm, name, { get: () => fn.call(vm) })
|
|
return { vm, calls, definition }
|
|
}
|
|
|
|
test('scanning a discard outbound document opens the discard workflow with its linked number', () => {
|
|
const { vm } = page()
|
|
vm.discardTasks = [{ discardNo: 'CD-1', outboundNo: 'OB-1', palletNo: 'P-1', status: 'PENDING' }]
|
|
vm.openScannedTask = () => { throw new Error('must not enter regular inventory picking') }
|
|
vm.handleScan('OB-1')
|
|
assert.equal(vm.discardCurrent.discardNo, 'CD-1')
|
|
assert.equal(vm.discardCurrent.outboundNo, 'OB-1')
|
|
})
|
|
|
|
test('退货废弃一箱五件分别展示,混装SKU按箱唛去重', () => {
|
|
const { vm } = page()
|
|
vm.discardTask = { cartonMarks: ['RETURN-BOX-1'], skus: [{ quantityUnit: '件', plannedPackages: 5 }] }
|
|
assert.equal(vm.discardQuantitySummary, '1 箱 · 5 件')
|
|
vm.discardTask.skus = [{ quantityUnit: '件', plannedPackages: 2 }, { quantityUnit: '件', plannedPackages: 3 }]
|
|
assert.equal(vm.discardQuantitySummary, '1 箱 · 5 件')
|
|
vm.discardTask = { cartonMarks: ['BOX-1', 'BOX-1'], skus: [{ quantityUnit: '箱', plannedPackages: 1 }] }
|
|
assert.equal(vm.discardQuantitySummary, '1 箱')
|
|
})
|
|
|
|
test('discard pallet resolution passes the selected discard number', async () => {
|
|
let received
|
|
const { vm } = page({ resolveCargoDiscardPallet: async (...args) => {
|
|
received = args
|
|
return { discardNo: 'CD-2', palletNo: 'P-1', skus: [] }
|
|
} })
|
|
vm.discardCurrent = { discardNo: 'CD-2' }
|
|
await vm.resolveDiscardPallet('P-1')
|
|
assert.deepEqual(received, ['P-1', 'CD-2'])
|
|
})
|
|
|
|
test('点击打开废弃作业使用输入框值,不把 tap 事件当作托盘号', async () => {
|
|
let received
|
|
const { vm } = page({ resolveCargoDiscardPallet: async (...args) => {
|
|
received = args
|
|
return { discardNo: 'CD-2', palletNo: 'P-1', skus: [] }
|
|
} })
|
|
vm.discardCurrent = { discardNo: 'CD-2' }
|
|
vm.discardPalletCode = ' P-1 '
|
|
await vm.resolveDiscardPallet({ type: 'tap', detail: { x: 10, y: 20 } })
|
|
assert.deepEqual(received, ['P-1', 'CD-2'])
|
|
assert.equal(vm.discardPalletCode, 'P-1')
|
|
})
|
|
|
|
test('同单按箱废弃允许扫描当前托盘及其他托盘所选箱唛,范围不扩展到其他单据', async () => {
|
|
const requests = []
|
|
const tasks = [
|
|
{ discardNo: 'CD-1', palletNo: 'P-1', scanMode: 'CARTON', cartonMarks: ['BOX-1'], skus: [], status: 'PENDING' },
|
|
{ discardNo: 'CD-1', palletNo: 'P-2', scanMode: 'CARTON', cartonMarks: ['BOX-2'], skus: [], status: 'PENDING' },
|
|
{ discardNo: 'CD-OTHER', palletNo: 'P-3', scanMode: 'CARTON', cartonMarks: ['BOX-3'], skus: [], status: 'PENDING' },
|
|
]
|
|
const { vm } = page({ resolveCargoDiscardPallet: async code => {
|
|
requests.push(code)
|
|
return tasks.find(task => task.palletNo === code || task.cartonMarks.includes(code))
|
|
} })
|
|
vm.discardTasks = tasks
|
|
vm.openDiscardOrder('CD-1')
|
|
await Promise.resolve()
|
|
assert.equal(vm.discardCurrentTasks.length, 2)
|
|
await vm.resolveDiscardPallet('P-1')
|
|
assert.equal(vm.error, null)
|
|
await vm.resolveDiscardPallet('BOX-2')
|
|
assert.equal(vm.discardTask.palletNo, 'P-1')
|
|
await vm.resolveDiscardPallet('P-2')
|
|
await vm.resolveDiscardPallet('BOX-2')
|
|
assert.equal(vm.discardTask.palletNo, 'P-2')
|
|
assert.deepEqual(vm.discardScannedMarks, ['BOX-2'])
|
|
await vm.resolveDiscardPallet('BOX-3')
|
|
assert.deepEqual(requests, ['P-1', 'P-2'])
|
|
assert.equal(vm.discardTask.palletNo, 'P-2')
|
|
assert.deepEqual(vm.discardScannedMarks, ['BOX-2'])
|
|
})
|
|
|
|
test('discard requires restored photos and retains task while native camera returns', async () => {
|
|
const { vm, definition } = page({ requireSession: () => true, restoreFulfillmentDraft() {} })
|
|
vm.discardCurrent = { discardNo: 'CD-1' }; vm.discardTask = { palletNo: 'PALLET', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 3 }] }; vm.discardActuals = { sku: 3 }
|
|
assert.equal(vm.canConfirmDiscard, false)
|
|
vm.discardPhotoReady = true; assert.equal(vm.canConfirmDiscard, false)
|
|
vm.discardPhotos = [{ fileId: '101' }]; assert.equal(vm.canConfirmDiscard, true)
|
|
vm.discardPhotoBusy = true; assert.equal(vm.canConfirmDiscard, false)
|
|
let loads = 0; vm.startScanner = () => {}; vm.loadTasks = () => { loads++ }
|
|
definition.onShow.call(vm)
|
|
assert.equal(loads, 0); assert.equal(vm.discardTask.palletNo, 'PALLET')
|
|
})
|
|
|
|
test('discard list failure blocks confirmation and stale downloads cannot replace a new pallet', async () => {
|
|
const pending = deferred()
|
|
const { vm } = page({ listCargoDiscardPhotos: async () => { throw new Error('offline photos') }, downloadCargoDiscardPhoto: () => pending.promise })
|
|
vm.discardCurrent = { discardNo: 'CD-1' }; vm.discardTask = { palletNo: 'A', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 1 }] }; vm.discardActuals = { sku: 1 }
|
|
vm.discardPhotos = [{ fileId: '101' }]; vm.discardPhotoReady = true
|
|
await vm.restoreDiscardPhotos(); assert.equal(vm.canConfirmDiscard, false)
|
|
const first = vm.loadDiscardPhoto('101'); vm.discardTask = { palletNo: 'B' }; vm.resetDiscardPhotos()
|
|
pending.resolve('old-photo-url'); await first; assert.deepEqual(vm.discardPhotoUrls, {})
|
|
})
|
|
|
|
test('return pallet rows sum carton counts without changing raw pick lines', () => {
|
|
const { vm } = page()
|
|
const cartons = Array.from({ length: 5 }, (_, index) => ({ outboundPalletItemId: index + 1, palletNo: 'PALLET', plannedPackages: 1, pickedPackages: index < 3 ? 1 : 0, positions: [] }))
|
|
const loose = { outboundPalletItemId: 6, palletNo: '', plannedPackages: 2, pickedPackages: 0, positions: [] }
|
|
vm.current = { outboundType: 'RETURN_OUTBOUND', items: [...cartons, loose, { outboundPalletItemId: 7, palletNo: 'OTHER', plannedPackages: 2, pickedPackages: 1, positions: [] }] }
|
|
const before = JSON.stringify(vm.current.items)
|
|
assert.equal(vm.displayItems.length, 3)
|
|
assert.equal(vm.displayItems[0].plannedPackages, 5)
|
|
assert.equal(vm.displayItems[0].pickedPackages, 3)
|
|
assert.equal(vm.displayItems[1], loose)
|
|
assert.equal(vm.displayItems[2].palletNo, 'OTHER')
|
|
assert.equal(JSON.stringify(vm.current.items), before)
|
|
assert.match(vm.scanLabel, /扫描托盘号/)
|
|
assert.match(vm.scanPlaceholder, /扫描托盘号/)
|
|
cartons.forEach(item => { item.pickedPackages = 1 })
|
|
assert.equal(vm.displayItems[0].pickedPackages, 5)
|
|
})
|
|
|
|
test('other outbound types preserve individual selectable lines', () => {
|
|
const { vm } = page()
|
|
const items = [{ palletNo: 'PALLET', plannedPackages: 1, pickedPackages: 0, positions: [] }, { palletNo: 'PALLET', plannedPackages: 2, pickedPackages: 0, positions: [] }]
|
|
vm.current = { outboundType: 'NORMAL', items }
|
|
assert.equal(vm.displayItems, items)
|
|
assert.equal(vm.hasReturnPallet, false)
|
|
})
|
|
|
|
test('completed and cancelled tasks cannot submit pallet picking or task completion', async () => {
|
|
for (const status of ['COMPLETED', 'CANCELLED']) {
|
|
const { vm, calls } = page()
|
|
vm.current = { taskNo: 'TASK', status, items: [{ palletNo: 'PALLET', plannedPackages: 1, pickedPackages: 1, positions: [] }] }
|
|
await vm.handleScan('PALLET'); await vm.submitPalletPick('PALLET'); await vm.completeTask()
|
|
assert.deepEqual(calls, [])
|
|
}
|
|
})
|
|
|
|
test('pending and in-progress tasks still submit exactly once while busy', async () => {
|
|
for (const status of ['PENDING', 'IN_PROGRESS']) {
|
|
const pending = deferred(); let submits = 0
|
|
const { vm } = page({ pickOutboundPallet: async () => { submits++; await pending.promise } })
|
|
vm.current = { taskNo: 'TASK', status, items: [{ palletNo: 'PALLET', plannedPackages: 1, pickedPackages: 0, positions: [] }] }
|
|
const first = vm.handleScan('PALLET'); const second = vm.handleScan('PALLET')
|
|
pending.resolve(); await Promise.all([first, second]); assert.equal(submits, 1)
|
|
}
|
|
})
|
|
|
|
test('failed new pallet lookup clears the previous selectable pallet and preserves retry code', async () => {
|
|
const { vm } = page({ resolveCargoDiscardPallet: async () => { throw new Error('offline') } })
|
|
vm.discardCurrent = { discardNo: 'DISCARD' }; vm.discardTask = { palletNo: 'OLD', status: 'PENDING', skus: [] }
|
|
await vm.resolveDiscardPallet('NEW')
|
|
assert.equal(vm.discardTask, null); assert.equal(vm.canConfirmDiscard, false)
|
|
assert.equal(vm.discardPalletCode, 'NEW'); assert.equal(vm.error.message, 'offline'); assert.equal(vm.discardLoading, false)
|
|
})
|
|
|
|
test('discard pallet scans do not overlap and cannot leave the previous pallet ready to submit', async () => {
|
|
const pending = deferred(); let resolves = 0
|
|
const { vm } = page({ resolveCargoDiscardPallet: async () => { resolves++; return pending.promise } })
|
|
vm.discardCurrent = { discardNo: 'DISCARD' }; vm.discardTask = { palletNo: 'OLD', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 1 }] }; vm.discardActuals = { sku: 1 }
|
|
const first = vm.resolveDiscardPallet('NEW')
|
|
const second = vm.resolveDiscardPallet('SECOND')
|
|
const canConfirmDuringLoad = vm.canConfirmDiscard
|
|
pending.resolve({ discardNo: 'DISCARD', palletNo: 'NEW', status: 'PENDING', skus: [] }); await Promise.all([first, second])
|
|
assert.equal(resolves, 1); assert.equal(canConfirmDuringLoad, false)
|
|
assert.equal(vm.discardTask.palletNo, 'NEW')
|
|
})
|
|
|
|
test('discard confirmation is guarded against double submission', async () => {
|
|
const pending = deferred(); let submits = 0
|
|
const { vm } = page({ confirmCargoDiscard: async () => { submits++; await pending.promise }, listCargoDiscardTasks: async () => [] })
|
|
vm.discardCurrent = { discardNo: 'DISCARD' }; vm.discardTask = { palletNo: 'PALLET', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 1 }] }; vm.discardActuals = { sku: 1 }
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]; const first = vm.confirmDiscard(); const second = vm.confirmDiscard()
|
|
pending.resolve(); await Promise.all([first, second]); assert.equal(submits, 1)
|
|
})
|
|
|
|
test('无关联库存按现场箱唛去重计数,提交扫描清单而非手填数量', async () => {
|
|
let payload
|
|
const { vm } = page({ confirmCargoDiscard: async (...args) => { payload = args }, listCargoDiscardTasks: async () => [] })
|
|
vm.discardCurrent = { discardNo: 'CD-A' }
|
|
vm.discardTask = { palletNo: 'P-A', status: 'PENDING', scanMode: 'PALLET', cartonMarks: [], skus: [{ itemId: 'sku', plannedPackages: 10, actualQuantityEditable: true }] }
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
for (const value of ['', ' ', null, -1, 11, 1.5, 'abc']) {
|
|
vm.discardActuals = { sku: value }; assert.equal(vm.canConfirmDiscard, false)
|
|
}
|
|
vm.discardActuals = { sku: '0' }; assert.equal(vm.canConfirmDiscard, true)
|
|
vm.discardActuals = { sku: '6' }; assert.equal(vm.canConfirmDiscard, false)
|
|
vm.discardActuals = { sku: 0 }; for (let i=0;i<6;i++) await vm.scanDiscardCarton('FIELD-'+i)
|
|
await vm.scanDiscardCarton('FIELD-0'); assert.equal(vm.discardActuals.sku, 6); assert.equal(vm.canConfirmDiscard, true)
|
|
await vm.confirmDiscard()
|
|
assert.equal(payload[0], 'CD-A'); assert.equal(payload[1], 'P-A')
|
|
assert.deepEqual(payload[2], [{ itemId: 'sku', actualPackages: 6, scannedCartonMarks: Array.from({ length: 6 }, (_, i) => 'FIELD-'+i) }])
|
|
})
|
|
|
|
test('无关联托盘扫码后从零开始,等待扫描现场箱唛', async () => {
|
|
const { vm } = page({ resolveCargoDiscardPallet: async () => ({ discardNo: 'CD-A', palletNo: 'P-A', cartonMarks: [], scanMode: 'PALLET', skus: [{ itemId: 'sku', plannedPackages: 10, actualQuantityEditable: true }] }) })
|
|
vm.discardCurrent = { discardNo: 'CD-A' }
|
|
await vm.resolveDiscardPallet('P-A')
|
|
assert.equal(vm.discardActuals.sku, 0)
|
|
assert.equal(vm.canConfirmDiscard, false)
|
|
})
|
|
|
|
test('无关联任务先扫托盘,再扫现场箱唛自动计数', async () => {
|
|
const { vm } = page()
|
|
vm.discardTasks = [{ discardNo: 'CD-NO-BOX', palletNo: 'P-1', status: 'PENDING', scanMode: 'PALLET', cartonMarks: [], skus: [{ itemId: 'sku', plannedPackages: 2, actualQuantityEditable: true }] }]
|
|
vm.openDiscardOrder('CD-NO-BOX'); await Promise.resolve()
|
|
assert.equal(vm.discardTask, null)
|
|
await vm.resolveDiscardPallet('P-1')
|
|
assert.equal(vm.discardTask.palletNo, 'P-1')
|
|
assert.equal(vm.discardActuals.sku, 0)
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
await vm.scanDiscardCarton('FIELD-A'); await vm.scanDiscardCarton('FIELD-B')
|
|
assert.equal(vm.discardActuals.sku, 2); assert.equal(vm.canConfirmDiscard, true)
|
|
})
|
|
|
|
test('discard retry stays in the discard workflow instead of restoring an old outbound task', async () => {
|
|
const { vm } = page(); const calls = []
|
|
vm.discardCurrent = { discardNo: 'DISCARD' }; vm.discardPalletCode = 'PALLET'
|
|
vm.resolveDiscardPallet = async code => calls.push(['discard', code]); vm.openTask = async code => calls.push(['outbound', code])
|
|
await vm.retry(); assert.deepEqual(calls, [['discard', 'PALLET']])
|
|
})
|
|
|
|
test('closing or switching discard order ignores the previous resolve response', async () => {
|
|
const pending = deferred()
|
|
const { vm } = page({ resolveCargoDiscardPallet: () => pending.promise })
|
|
vm.discardCurrent = { discardNo: 'FIRST' }
|
|
const first = vm.resolveDiscardPallet('PALLET')
|
|
vm.openDiscardOrder('SECOND')
|
|
pending.resolve({ discardNo: 'FIRST', palletNo: 'PALLET', skus: [] }); await first
|
|
assert.equal(vm.discardTask, null); assert.equal(vm.error, null)
|
|
})
|
|
|
|
|
|
test('loose disposal opens all marks and requires every carton scan', async () => {
|
|
const { vm } = page()
|
|
vm.discardTasks = [{ discardNo: 'CD-L', palletNo: '散箱', scanMode: 'CARTON', cartonMarks: ['BOX-1', 'BOX-2'], status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 2 }] }]
|
|
vm.openDiscardOrder('CD-L')
|
|
assert.equal(vm.discardTask, null)
|
|
await vm.resolveDiscardPallet('BOX-1')
|
|
assert.equal(vm.discardTask.scanMode, 'CARTON')
|
|
assert.deepEqual(vm.discardTask.cartonMarks, ['BOX-1', 'BOX-2'])
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
assert.equal(vm.canConfirmDiscard, false)
|
|
await vm.resolveDiscardPallet('BOX-1'); await vm.resolveDiscardPallet('BOX-1')
|
|
assert.deepEqual(vm.discardScannedMarks, ['BOX-1'])
|
|
assert.equal(vm.canConfirmDiscard, false)
|
|
await vm.resolveDiscardPallet('BOX-2')
|
|
assert.equal(vm.canConfirmDiscard, true)
|
|
vm.discardActuals.sku = 1; assert.equal(vm.canConfirmDiscard, false)
|
|
vm.discardActuals.sku = 2; vm.discardScannedMarks = []; vm.discardAllCartons = true
|
|
assert.equal(vm.canConfirmDiscard, false)
|
|
vm.discardPhotos = []; assert.equal(vm.canConfirmDiscard, false)
|
|
})
|
|
|
|
|
|
test('partial bound pallet confirmation sends only selected marks and preserves all photo gates', async () => {
|
|
let payload
|
|
const { vm } = page({ confirmCargoDiscard: async (...args) => { payload = args }, listCargoDiscardTasks: async () => [] })
|
|
vm.discardTasks = [{ discardNo: 'CD-P', outboundNo: 'OB-P', palletNo: 'P-1', scanMode: 'CARTON', cartonMarks: ['BOX-2'], status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 1 }] }]
|
|
vm.openDiscardOrder('CD-P'); await Promise.resolve()
|
|
assert.equal(vm.discardTask, null)
|
|
await vm.resolveDiscardPallet('P-1')
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
await vm.resolveDiscardPallet('BOX-2'); assert.equal(vm.canConfirmDiscard, true)
|
|
await vm.resolveDiscardPallet('BOX-OTHER'); assert.deepEqual(vm.discardScannedMarks, ['BOX-2']); assert.equal(vm.discardTask.palletNo, 'P-1')
|
|
await vm.confirmDiscard()
|
|
assert.deepEqual(payload, ['CD-P', 'P-1', [{ itemId: 'sku', actualPackages: 1 }], ['101'], ['BOX-2'], false, { idempotencyKey: 'key' }])
|
|
})
|
|
|
|
test('discard confirmation preserves inputs after stock command failure and starts a new explicit request', async () => {
|
|
const submitted = []
|
|
const retry = deferred()
|
|
let attempt = 0
|
|
const { vm } = page({
|
|
confirmCargoDiscard: async (...args) => { submitted.push(args); if (++attempt === 1) throw new Error('库存状态已变化'); await retry.promise },
|
|
fulfillmentIdempotencyKey: () => ({ value: `discard-${attempt}` }), listCargoDiscardTasks: async () => []
|
|
})
|
|
vm.discardCurrent = { discardNo: 'CD-RETRY' }
|
|
vm.discardTask = { palletNo: 'P-1', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 1 }] }
|
|
vm.discardTasks = [{ ...vm.discardTask, discardNo: 'CD-RETRY' }]
|
|
vm.discardActuals = { sku: 1 }; vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
await vm.confirmDiscard()
|
|
assert.equal(vm.discardBusy, false)
|
|
assert.ok(vm.discardTask)
|
|
assert.equal(vm.error.message, '库存状态已变化')
|
|
vm.successMessage = '上次作业已完成'
|
|
const retried = vm.confirmDiscard()
|
|
assert.equal(vm.error, null)
|
|
assert.equal(vm.successMessage, '')
|
|
retry.resolve(); await retried
|
|
assert.deepEqual(submitted[0].slice(0, 6), submitted[1].slice(0, 6))
|
|
assert.notEqual(submitted[0][6].idempotencyKey, submitted[1][6].idempotencyKey)
|
|
assert.equal(vm.discardBusy, false)
|
|
assert.equal(vm.error, null)
|
|
assert.match(vm.successMessage, /已完成/)
|
|
})
|
|
|
|
test('dedicated scanner validates carton scope and preserves photos', () => {
|
|
const { vm } = page()
|
|
vm.discardTask = { palletNo: 'P-1', scanMode: 'CARTON', cartonMarks: ['BX-1', 'BX-2'], status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 2 }] }
|
|
vm.discardActuals = { sku: 2 }; vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
assert.deepEqual(vm.discardPendingMarks, ['BX-1', 'BX-2'])
|
|
assert.deepEqual(vm.discardQuantityLines, [])
|
|
vm.discardCartonCode = ' BX-1 '; vm.scanDiscardCarton()
|
|
assert.equal(vm.discardCartonCode, '')
|
|
vm.scanDiscardCarton('BX-1'); vm.scanDiscardCarton('OTHER')
|
|
assert.deepEqual(vm.discardScannedMarks, ['BX-1']); assert.equal(vm.canConfirmDiscard, false)
|
|
assert.deepEqual(vm.discardPendingMarks, ['BX-2'])
|
|
assert.match(vm.error.message, /箱号不属于/)
|
|
vm.discardCartonsExpanded = false
|
|
vm.scanDiscardCarton('BX-2'); assert.equal(vm.canConfirmDiscard, true)
|
|
assert.deepEqual(vm.discardPendingMarks, [])
|
|
assert.equal(vm.discardCartonsExpanded, false)
|
|
assert.deepEqual(vm.discardPhotos, [{ fileId: '101' }]); assert.equal(vm.discardTask.palletNo, 'P-1')
|
|
vm.discardTask.status = 'COMPLETED'; vm.scanDiscardCarton('OTHER'); assert.equal(vm.error, null)
|
|
})
|
|
|
|
test('bound discard opens only after its pallet scan and summary hides carton numbers', async () => {
|
|
const { vm } = page()
|
|
vm.discardTasks = [{ discardNo: 'CD-1', palletNo: 'P-1', status: 'PENDING', scanMode: 'CARTON', cartonMarks: ['BX-1'], skus: [{ itemId: 'sku', plannedPackages: 1 }] }]
|
|
vm.openDiscardOrder('CD-1'); assert.equal(vm.discardTask, null)
|
|
await vm.resolveDiscardPallet('BX-1'); assert.equal(vm.discardTask, null)
|
|
await vm.resolveDiscardPallet('OTHER'); assert.equal(vm.discardTask, null)
|
|
await vm.resolveDiscardPallet('P-1'); assert.equal(vm.discardTask.palletNo, 'P-1')
|
|
assert.deepEqual(vm.discardScannedMarks, [])
|
|
assert.deepEqual(vm.discardTask.cartonMarks, ['BX-1'])
|
|
const template = readFileSync(new URL('../src/pages/outbound/outbound.vue', import.meta.url), 'utf8')
|
|
const summary = template.slice(template.indexOf('废弃箱关联托盘'), template.indexOf('<ScanTargetInput name="cargo-discard"'))
|
|
assert.doesNotMatch(summary, /task.cartonMarks/)
|
|
})
|
|
|
|
test('multi-pallet discard prompts remaining pallet numbers and requires a new pallet scan', async () => {
|
|
const first = { discardNo: 'CD-M', palletNo: 'P-1', status: 'PENDING', scanMode: 'CARTON', cartonMarks: ['BX-1'], skus: [{ itemId: 'sku1', plannedPackages: 1 }] }
|
|
const second = { ...first, palletNo: 'P-2', cartonMarks: ['BX-2'], skus: [{ itemId: 'sku2', plannedPackages: 1 }] }
|
|
const other = { ...first, discardNo: 'CD-OTHER', palletNo: 'P-OTHER' }
|
|
let completed = 0
|
|
const { vm } = page({ confirmCargoDiscard: async () => { completed++ }, listCargoDiscardTasks: async () => [{ ...first, status: 'COMPLETED' }, { ...second, status: completed === 2 ? 'COMPLETED' : 'PENDING' }, other] })
|
|
vm.discardTasks = [first, second, other]; vm.openDiscardOrder('CD-M')
|
|
await vm.resolveDiscardPallet('P-1'); vm.scanDiscardCarton('BX-1')
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
await vm.confirmDiscard()
|
|
assert.equal(vm.discardTask, null); assert.equal(vm.discardPalletCode, '')
|
|
assert.deepEqual(vm.discardPendingTasks.map(task => task.palletNo), ['P-2'])
|
|
assert.match(vm.successMessage, /P-1.*已完成.*P-2.*继续扫描/)
|
|
assert.doesNotMatch(vm.successMessage, /P-OTHER|全部/)
|
|
assert.equal(vm.canConfirmDiscard, false)
|
|
await vm.resolveDiscardPallet('P-2'); assert.equal(vm.successMessage, '')
|
|
assert.deepEqual(vm.discardPendingMarks, ['BX-2']); assert.deepEqual(vm.discardPhotos, [])
|
|
vm.scanDiscardCarton('BX-2'); vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '102' }]
|
|
await vm.confirmDiscard()
|
|
assert.deepEqual(vm.discardPendingTasks, []); assert.match(vm.successMessage, /本单全部.*已完成/)
|
|
})
|
|
|
|
test('successful discard followed by refresh failure does not claim the whole order completed', async () => {
|
|
let confirms = 0
|
|
const { vm } = page({ confirmCargoDiscard: async () => { confirms++ }, listCargoDiscardTasks: async () => { throw new Error('offline') } })
|
|
const first = { discardNo: 'CD-M', palletNo: 'P-1', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 1 }] }
|
|
vm.discardTasks = [first, { ...first, palletNo: 'P-2' }]; vm.openDiscardOrder('CD-M')
|
|
await vm.resolveDiscardPallet('P-1'); vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
await vm.confirmDiscard(); await vm.confirmDiscard()
|
|
assert.equal(confirms, 1); assert.equal(vm.discardTask, null)
|
|
assert.match(vm.successMessage, /P-1.*已完成.*刷新失败/); assert.doesNotMatch(vm.successMessage, /全部/)
|
|
assert.deepEqual(vm.discardPendingTasks.map(task => task.palletNo), ['P-2'])
|
|
})
|
|
|
|
test('仓租余额不足显示原因并保留当前托盘、已扫箱号、实际数与现场照片', async () => {
|
|
const { vm } = page({ confirmCargoDiscard: async () => { throw Object.assign(new Error('仓租校验未通过:可用余额和授信额度不足'), { code: 'A0409' }) } })
|
|
vm.discardCurrent = { discardNo: 'CD-1' }
|
|
vm.discardTask = { palletNo: 'P-1', scanMode: 'CARTON', status: 'PENDING', cartonMarks: ['BX-1'], skus: [{ itemId: 'sku', plannedPackages: 1 }] }
|
|
vm.discardActuals = { sku: 1 }; vm.discardScannedMarks = ['BX-1']
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
await vm.confirmDiscard()
|
|
assert.match(vm.error.message, /余额和授信额度不足/); assert.equal(vm.error.code, 'A0409')
|
|
assert.equal(vm.discardTask.palletNo, 'P-1'); assert.deepEqual(vm.discardScannedMarks, ['BX-1'])
|
|
assert.deepEqual(vm.discardActuals, { sku: 1 }); assert.deepEqual(vm.discardPhotos, [{ fileId: '101' }])
|
|
assert.equal(vm.successMessage, ''); assert.equal(vm.discardBusy, false)
|
|
})
|
|
|
|
for (const action of ['pallet', 'carton', 'retry']) {
|
|
test(`出库资金不足后通过 ${action} 重新核对并再次手动提交`, async () => {
|
|
let submits = 0; let reads = 0; let key = 0; const keys = []
|
|
const task = { discardNo: 'CD-1', palletNo: 'P-1', status: 'PENDING', scanMode: 'CARTON', cartonMarks: ['BX-1'], skus: [{ itemId: 'sku', plannedPackages: 1 }] }
|
|
const { vm } = page({
|
|
resolveCargoDiscardPallet: async () => { reads++; return { ...task } },
|
|
confirmCargoDiscard: async (...args) => { keys.push(args.at(-1).idempotencyKey); if (++submits === 1) throw new Error('可用余额和授信额度不足') },
|
|
fulfillmentIdempotencyKey: () => ({ value: `key-${++key}` }), listCargoDiscardTasks: async () => []
|
|
})
|
|
vm.discardCurrent = { discardNo: 'CD-1' }; vm.discardTask = task; vm.discardTasks = [task]
|
|
vm.discardActuals = { sku: 1 }; vm.discardScannedMarks = ['BX-1']
|
|
vm.discardPhotos = [{ fileId: '101' }]; vm.discardPhotoReady = true
|
|
await vm.confirmDiscard(); assert.equal(vm.discardNeedsRefresh, true)
|
|
if (action === 'pallet') await vm.resolveDiscardPallet('P-1')
|
|
else if (action === 'carton') await vm.scanDiscardCarton('BX-1')
|
|
else await vm.retry()
|
|
assert.equal(reads, 1); assert.equal(submits, 1); assert.equal(vm.error, null)
|
|
assert.equal(vm.discardNeedsRefresh, false); assert.deepEqual(vm.discardScannedMarks, ['BX-1'])
|
|
assert.deepEqual(vm.discardActuals, { sku: 1 }); assert.deepEqual(vm.discardPhotos, [{ fileId: '101' }])
|
|
await vm.confirmDiscard(); assert.equal(submits, 2); assert.notEqual(keys[0], keys[1])
|
|
assert.match(vm.successMessage, /全部.*已完成/)
|
|
})
|
|
}
|
|
|
|
test('结果不明后刷新发现已完成,不再重复出库', async () => {
|
|
let submits = 0
|
|
const task = { discardNo: 'CD-1', palletNo: 'P-1', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 1 }] }
|
|
const { vm } = page({ confirmCargoDiscard: async () => { submits++; throw Object.assign(new Error('网络中断'), { network: true }) }, resolveCargoDiscardPallet: async () => ({ ...task, status: 'COMPLETED' }) })
|
|
vm.discardCurrent = { discardNo: 'CD-1' }; vm.discardTask = task
|
|
vm.discardActuals = { sku: 1 }; vm.discardPhotos = [{ fileId: '101' }]; vm.discardPhotoReady = true
|
|
await vm.confirmDiscard(); await vm.confirmDiscard()
|
|
assert.equal(submits, 1); assert.equal(vm.discardTask.status, 'COMPLETED'); assert.equal(vm.canConfirmDiscard, false)
|
|
})
|
|
|
|
test('失败后刷新仍报错,保留草稿且不再发送出库', async () => {
|
|
let submits = 0
|
|
const { vm } = page({ confirmCargoDiscard: async () => { submits++; throw new Error('出库失败') }, resolveCargoDiscardPallet: async () => { throw new Error('任务读取失败') } })
|
|
vm.discardCurrent = { discardNo: 'CD-1' }; vm.discardTask = { palletNo: 'P-1', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 1 }] }
|
|
vm.discardActuals = { sku: 1 }; vm.discardPhotos = [{ fileId: '101' }]; vm.discardPhotoReady = true
|
|
await vm.confirmDiscard(); await vm.confirmDiscard()
|
|
assert.equal(submits, 1); assert.match(vm.error.message, /任务读取失败/)
|
|
assert.equal(vm.discardNeedsRefresh, true); assert.equal(vm.discardBusy, false); assert.equal(vm.discardLoading, false)
|
|
assert.deepEqual(vm.discardPhotos, [{ fileId: '101' }])
|
|
})
|
|
|
|
test('无关联扫码显示备注,不能扫托盘或超过目标,失败刷新后保留现场箱唛再提交', async () => {
|
|
let attempts = 0
|
|
const task = { discardNo: 'CD-FIELD', palletNo: 'PALLET-FIELD', status: 'PENDING', scanMode: 'PALLET', cartonMarks: [], remark: '只废弃破损外箱', remainingPackages: 2, skus: [{ itemId: 'sku', plannedPackages: 8, actualQuantityEditable: true }] }
|
|
const { vm } = page({ confirmCargoDiscard: async () => { if (++attempts === 1) throw new Error('余额不足') }, listCargoDiscardTasks: async () => [] })
|
|
vm.discardTasks = [task]; vm.openDiscardOrder(task.discardNo); await vm.resolveDiscardPallet(task.palletNo)
|
|
assert.equal(vm.discardOrderRemark, '只废弃破损外箱')
|
|
await vm.scanDiscardCarton(task.palletNo); assert.equal(vm.discardActuals.sku, 0)
|
|
await vm.scanDiscardCarton('FIELD-A'); await vm.scanDiscardCarton('FIELD-B'); await vm.scanDiscardCarton('FIELD-C')
|
|
assert.equal(vm.discardActuals.sku, 2); assert.deepEqual(vm.discardUnboundScans.map(scan => scan.mark), ['FIELD-A', 'FIELD-B'])
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
await vm.confirmDiscard(); assert.equal(vm.discardNeedsRefresh, true)
|
|
await vm.resolveDiscardPallet(task.palletNo)
|
|
assert.equal(vm.discardActuals.sku, 2); assert.equal(vm.discardUnboundScans.length, 2)
|
|
assert.equal(vm.discardPhotos.length, 1); await vm.confirmDiscard(); assert.equal(attempts, 2)
|
|
})
|
|
|
|
test('已扫描现场箱唛可移除并同步回减数量,确认只提交剩余箱唛', async () => {
|
|
let payload
|
|
const task = { discardNo: 'CD-REMOVE', palletNo: 'P-REMOVE', status: 'PENDING', scanMode: 'PALLET', cartonMarks: [], remainingPackages: 4, skus: [{ itemId: 'sku', plannedPackages: 8, actualQuantityEditable: true }] }
|
|
const { vm } = page({ confirmCargoDiscard: async (...args) => { payload = args }, listCargoDiscardTasks: async () => [] })
|
|
vm.discardTasks = [task]; vm.openDiscardOrder(task.discardNo); await vm.resolveDiscardPallet(task.palletNo)
|
|
await vm.scanDiscardCarton('BOX-1'); await vm.scanDiscardCarton('BOX-2')
|
|
vm.removeDiscardScan('BOX-1')
|
|
assert.equal(vm.discardActuals.sku, 1)
|
|
assert.deepEqual(vm.discardUnboundScans.map(scan => scan.mark), ['BOX-2'])
|
|
vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }]
|
|
await vm.confirmDiscard()
|
|
assert.deepEqual(payload[2], [{ itemId: 'sku', actualPackages: 1, scannedCartonMarks: ['BOX-2'] }])
|
|
})
|