fix(pda): stage sealed pallet scans for atomic batch picking
This commit is contained in:
@@ -4,6 +4,30 @@ import { readFile } from 'node:fs/promises'
|
||||
|
||||
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
|
||||
|
||||
test('system pallet action binds through one request and keeps the same operation on retry', async () => {
|
||||
const page = await read('../pages/dropship-palletizing/dropship-palletizing.vue')
|
||||
const method = page.slice(page.indexOf('async createPallet()'), page.indexOf('statusLabel(status)'))
|
||||
const requests = []
|
||||
const { stableOperationId } = await import('../utils/idempotency.js')
|
||||
const methods = new Function('createAndBindDropshipPallet', 'stableOperationId', 'playSuccessSound', 'playErrorSound', 'formatApiError', `return ({${method}})`)(async (taskId, payload) => {
|
||||
requests.push({ taskId, payload })
|
||||
if (requests.length === 1) throw new Error('network timeout')
|
||||
return { taskStatus: 'CLAIMED', unbound: [], pallets: [{ palletNo: 'DP-260909-0001', cartons: [{ cartonId: '9007199254740993' }] }] }
|
||||
}, stableOperationId, () => {}, () => {}, (error) => error.message)
|
||||
const context = { busy: false, online: true, task: { taskStatus: 'CLAIMED' }, taskId: '10', warehouseId: '100', user: { id: '7' }, unboundCartons: [{ cartonId: '9007199254740993' }], scanState: {}, persistDraft() {}, get pallets() { return this.task.pallets || [] } }
|
||||
await methods.createPallet.call(context)
|
||||
assert.equal(context.error, 'network timeout')
|
||||
assert.equal(context.busy, false)
|
||||
await methods.createPallet.call(context)
|
||||
assert.deepEqual(requests[0], requests[1])
|
||||
assert.equal(context.activePalletNo, 'DP-260909-0001')
|
||||
assert.equal(context.scanState.activePalletNo, context.activePalletNo)
|
||||
context.unboundCartons = []
|
||||
await methods.createPallet.call(context)
|
||||
assert.equal(requests.length, 2)
|
||||
assert.match(page, /新建托盘并绑定/)
|
||||
})
|
||||
|
||||
test('dropship palletizing route and workbench entry are registered', async () => {
|
||||
const [pages, data, home, modulePage] = await Promise.all([read('../pages.json'), read('../data/demo-data.js'), read('../pages/home/home.vue'), read('../pages/module/module.vue')])
|
||||
assert.match(pages, /pages\/dropship-palletizing\/tasks/)
|
||||
|
||||
@@ -6,6 +6,38 @@ const source = await readFile(new URL('../api/pda-loading.js', import.meta.url),
|
||||
const http = await readFile(new URL('../api/http.js', import.meta.url), 'utf8')
|
||||
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('sealed pallets use atomic batch actions and cannot use manual package actions', () => {
|
||||
assert.doesNotMatch(source, /\/pallets\/pick/)
|
||||
assert.match(source, /palletNo: action\.type === 'PICK_PALLET' \? action\.palletNo : null/)
|
||||
assert.match(page, /type: 'PICK_PALLET', itemId: item\.taskItemId \?\? item\.id, palletNo: item\.palletNo/)
|
||||
assert.match(page, /if \(isPalletItem\(result\.item\)\) return this\.pickScannedPallet\(result\.item\)/)
|
||||
assert.match(page, /itemState\(item\) === ITEM_STATUS\.PENDING && !isPalletItem\(item\)/)
|
||||
assert.match(page, /if \(isPalletItem\(item\)\) \{ this\.showToast\('请扫描托盘码整体拣出'/)
|
||||
assert.match(page, /if \(isPalletItem\(item\)\) return/)
|
||||
assert.match(page, /item\.palletNo \|\| '托盘码待同步'/)
|
||||
})
|
||||
|
||||
test('a DP scan changes all same-pallet rows locally without any remote command', async () => {
|
||||
const method = page.match(/pickScannedPallet\(item\) \{[\s\S]*?\n\t\t\t\},/)?.[0]
|
||||
assert.ok(method)
|
||||
const stateSource = await readFile(new URL('../utils/picking-state.js', import.meta.url), 'utf8')
|
||||
const { isPalletItem, ITEM_STATUS } = await import(`data:text/javascript;base64,${Buffer.from(stateSource).toString('base64')}`)
|
||||
const undo = page.match(/undoPallet\(item\) \{[\s\S]*?\n\t\t\t\},/)?.[0]
|
||||
const behavior = new Function('isPalletItem', 'ITEM_STATUS', `return ({${method}${undo}})`)(isPalletItem, ITEM_STATUS)
|
||||
let saved = 0
|
||||
const rows = [{ id: '1', unitType: 'PALLET', palletNo: 'DP-260909-0143', qty: 2, itemStatus: 'PENDING' }, { id: '2', unitType: 'PALLET', palletNo: 'DP-260909-0143', qty: 3, itemStatus: 'PENDING' }, { id: '3', unitType: 'PACKAGE', qty: 1, itemStatus: 'PENDING' }]
|
||||
const context = { submitting: false, requirePickingOperation: () => true, currentBox: { items: rows }, itemStates: {}, partialQuantities: {}, scannedBoxCodes: {}, recordScanResult() {}, showToast() {}, persistSession() { saved++ } }
|
||||
behavior.pickScannedPallet.call(context, rows[0])
|
||||
assert.deepEqual(context.itemStates, { 1: 'picked', 2: 'picked' })
|
||||
assert.deepEqual(context.partialQuantities, { 1: 2, 2: 3 })
|
||||
assert.equal(saved, 1)
|
||||
behavior.undoPallet.call(context, rows[0])
|
||||
assert.deepEqual(context.itemStates, { 1: 'pending', 2: 'pending' })
|
||||
assert.deepEqual(context.scannedBoxCodes, {})
|
||||
assert.equal(saved, 2)
|
||||
assert.doesNotMatch(method, /await|pickContainer|syncRemote|request\(/)
|
||||
})
|
||||
|
||||
test('PDA loading adapter uses only task-scoped cabinet evidence paths', () => {
|
||||
for (const path of [
|
||||
'/first-leg/pda-loading/tasks/pending'
|
||||
|
||||
@@ -21,7 +21,7 @@ test('a server task never inherits a completed marker from the previously opened
|
||||
assert.match(page, /submitStatusHint\(\)[\s\S]*?if \(this\.pickingCompleted\) return '拣货已完成,请继续装柜取证'/)
|
||||
const submit = page.match(/async submitPicking\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0] || ''
|
||||
assert.doesNotMatch(submit, /completedBoxes/)
|
||||
assert.match(page, /v-else-if="pickingOperationReady" class="pick-item-actions processed-actions"/)
|
||||
assert.match(page, /v-else-if="pickingOperationReady && !isPalletItem\(item\)" class="pick-item-actions processed-actions"/)
|
||||
assert.doesNotMatch(page, /v-else-if="!(?:pickingCompleted|completedBoxes\[currentBoxIndex\])"/)
|
||||
})
|
||||
|
||||
|
||||
@@ -7,6 +7,20 @@ const state = await import(`data:text/javascript;base64,${Buffer.from(source).to
|
||||
|
||||
const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
|
||||
|
||||
test('sealed pallet matches its DP code and never accepts an individual carton', () => {
|
||||
const pallet = { id: '1', unitType: 'PALLET', indivisible: true, palletNo: 'DP-260909-0143', boxCodes: ['IN#2-1', 'IN#2-2'], qty: 2 }
|
||||
assert.equal(state.classifyScan([pallet], {}, 'IN#2-1').type, 'unknown')
|
||||
assert.equal(state.classifyScan([pallet], {}, pallet.palletNo).type, 'pending')
|
||||
assert.equal(state.classifyScan([pallet], { 1: state.ITEM_STATUS.PICKED }, pallet.palletNo).type, 'processed')
|
||||
})
|
||||
|
||||
test('mixed task keeps package scanning separate from sealed pallet scanning', () => {
|
||||
const mixed = [{ id: '1', unitType: 'PALLET', palletNo: 'DP-260909-0001', boxCodes: ['BOX-1'] }, { id: '2', unitType: 'PACKAGE', boxCodes: ['BOX-2'] }]
|
||||
assert.equal(state.classifyScan(mixed, {}, 'BOX-2').item.id, '2')
|
||||
assert.equal(state.classifyScan(mixed, {}, 'DP-260909-0001').item.id, '1')
|
||||
assert.equal(state.classifyScan(mixed, {}, 'BOX-1').type, 'unknown')
|
||||
})
|
||||
|
||||
test('createItemStates initializes every item as pending', () => {
|
||||
assert.deepEqual(state.createItemStates(items), {
|
||||
1: state.ITEM_STATUS.PENDING,
|
||||
|
||||
Reference in New Issue
Block a user