优化
This commit is contained in:
@@ -4,13 +4,13 @@ import { readFile } from 'node:fs/promises'
|
||||
import { fulfillment, beginDispatchCommand, patchDispatchCommand, finishDispatchCommand, discardRejectedDispatchCommand, restoreFulfillmentDraft, updateFulfillmentDraft, resetFulfillmentSession } from '../src/stores/fulfillment.js'
|
||||
import { requireDispatchVersion, dispatchVersionAtLeast, normalizeDispatchTask } from '../src/api/fulfillment.js'
|
||||
const source = (await readFile(new URL('../src/pages/dispatch/dispatch.vue', import.meta.url), 'utf8')).match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||
const item = (overrides = {}) => ({ dispatchItemId: 'item-1', sourceKey: 'source-1', sourceBusinessType: 'RELABEL_ORDER', sourceBusinessNo: 'RL-1', customerId: 'customer-1', customerName: '客户甲', palletNo: 'PALLET-1', scheduledPackages: 2, pickedPackages: 0, positions: [{ positionId: 'position-1', availablePackages: 2, allocatedPackages: 2, pickedPackages: 0 }], returnCartons: [], ...overrides })
|
||||
const item = (overrides = {}) => ({ dispatchItemId: 'item-1', sourceKey: 'source-1', sourceBusinessType: 'RELABEL_ORDER', sourceBusinessNo: 'RL-1', customerId: 'customer-1', customerName: '客户甲', palletNo: 'PALLET-1', scheduledPackages: 2, pickedPackages: 0, positions: [{ positionId: 'position-1', availablePackages: 2, allocatedPackages: 0, pickedPackages: 0 }], returnCartons: [], ...overrides })
|
||||
test('native string quantities normalize without changing Long identity or version', () => {
|
||||
const id = '9007199254740993'
|
||||
const value = normalizeDispatchTask(JSON.parse(JSON.stringify({ dispatchNo: 'PC/1', recordVersion: id, items: [item({ dispatchItemId: id, scheduledPackages: '12', pickedPackages: '0', positions: [{ positionId: id, balanceId: id, availablePackages: '12', allocatedPackages: '12', pickedPackages: '0' }], returnCartons: [{ returnCartonId: id, productQuantity: '24' }] })] })))
|
||||
const value = normalizeDispatchTask(JSON.parse(JSON.stringify({ dispatchNo: 'PC/1', recordVersion: id, items: [item({ dispatchItemId: id, scheduledPackages: '12', pickedPackages: '0', positions: [{ positionId: id, balanceId: id, availablePackages: '12', allocatedPackages: '0', pickedPackages: '0' }], returnCartons: [{ returnCartonId: id, productQuantity: '24' }] })] })))
|
||||
assert.equal(value.recordVersion, id); assert.equal(value.items[0].dispatchItemId, id)
|
||||
assert.equal(value.items[0].scheduledPackages, 12); assert.equal(value.items[0].pickedPackages, 0)
|
||||
assert.deepEqual(value.items[0].positions[0], { positionId: id, balanceId: id, availablePackages: 12, allocatedPackages: 12, pickedPackages: 0 })
|
||||
assert.deepEqual(value.items[0].positions[0], { positionId: id, balanceId: id, availablePackages: 12, allocatedPackages: 0, pickedPackages: 0 })
|
||||
assert.equal(value.items[0].returnCartons[0].returnCartonId, id); assert.equal(value.items[0].returnCartons[0].productQuantity, 24)
|
||||
assert.equal(normalizeDispatchTask({ dispatchNo: 'PC/1', items: [item({ scheduledPackages: String(Number.MAX_SAFE_INTEGER) })] }).items[0].scheduledPackages, Number.MAX_SAFE_INTEGER)
|
||||
})
|
||||
@@ -96,13 +96,29 @@ test('a first server error remains unknown and cannot be discarded as a definite
|
||||
const state = page({ pickDispatchItem: async () => { throw { code: 'A0500', message: 'server failure' } } })
|
||||
await state.recoverCommand(); state.discardRejected(); assert.equal(fulfillment.dispatch.pendingCommand.rejected, false)
|
||||
})
|
||||
test('position picking is bounded by this dispatch allocation and skips already picked allocations', () => {
|
||||
storage(); const state = page(); state.current.items[0].positions = [{ positionId: 'already-picked', availablePackages: 20, allocatedPackages: 1, pickedPackages: 1 }, { positionId: 'actual-position', availablePackages: 20, allocatedPackages: 1, pickedPackages: 0 }]
|
||||
state.selectPallet(state.current.items[0]); assert.equal(state.selectedPosition.positionId, 'actual-position'); assert.equal(state.selectedRemaining, 1)
|
||||
updateFulfillmentDraft('dispatch', { packages: 2 }); assert.equal(state.canPick, false)
|
||||
test('position picking uses available stock and this source remaining instead of warehouse allocation or picked buckets', () => {
|
||||
storage(); const state = page(); state.current = task({ items: [item({ scheduledPackages: '3', pickedPackages: '1', positions: [{ positionId: 'empty-position', availablePackages: '0', allocatedPackages: '20', pickedPackages: '0' }, { positionId: 'actual-position', availablePackages: '20', allocatedPackages: '0', pickedPackages: '7' }] })] })
|
||||
state.selectPallet(state.current.items[0]); assert.equal(state.selectedPosition.positionId, 'actual-position'); assert.equal(state.selectedRemaining, 2)
|
||||
assert.equal(state.pickablePackages(state.current.items[0]), 2); assert.equal(state.canPick, true)
|
||||
updateFulfillmentDraft('dispatch', { packages: 2 }); assert.equal(state.canPick, true)
|
||||
updateFulfillmentDraft('dispatch', { packages: 3 }); assert.equal(state.canPick, false)
|
||||
})
|
||||
for (const scenario of [
|
||||
{ name: 'zero available stock', scheduled: '2', picked: '0', available: '0', allocated: '20', stockPicked: '0', packages: '1', maximum: 0, position: null },
|
||||
{ name: 'source already reconciled', scheduled: '1', picked: '1', available: '8', allocated: '0', stockPicked: '0', packages: '1', maximum: 0, position: null },
|
||||
{ name: 'quantity above this dispatch source remaining', scheduled: '3', picked: '2', available: '9', allocated: '0', stockPicked: '8', packages: '2', maximum: 1, position: 'position-1' },
|
||||
{ name: 'quantity above this position available stock', scheduled: '5', picked: '0', available: '2', allocated: '0', stockPicked: '0', packages: '3', maximum: 2, position: 'position-1' },
|
||||
]) test(`real stock buckets prevent a pick for ${scenario.name}`, async () => {
|
||||
storage(); let posts = 0
|
||||
const state = page({ pickDispatchItem: async () => { posts++ } })
|
||||
state.current = task({ pickingReconciled: scenario.scheduled === scenario.picked, items: [item({ scheduledPackages: scenario.scheduled, pickedPackages: scenario.picked, positions: [{ positionId: 'position-1', availablePackages: scenario.available, allocatedPackages: scenario.allocated, pickedPackages: scenario.stockPicked }] })] })
|
||||
state.selectPallet(state.current.items[0]); state.updatePackages({ detail: { value: scenario.packages } })
|
||||
assert.equal(state.selectedPosition?.positionId ?? null, scenario.position); assert.equal(state.selectedRemaining, scenario.maximum)
|
||||
assert.equal(state.pickablePackages(state.current.items[0]), scenario.maximum); assert.equal(state.canPick, false)
|
||||
await state.submitPick(); assert.equal(posts, 0); assert.equal(fulfillment.dispatch.pendingCommand, null)
|
||||
})
|
||||
test('a shared pallet exposes both relabel sources and picking the second preserves its exact position and quantity', async () => {
|
||||
storage(); const rows = [item({ scheduledPackages: 3 }), item({ dispatchItemId: 'item-2', sourceKey: 'source-2', sourceBusinessNo: 'RL-2', customerId: 'customer-2', customerName: '客户乙', scheduledPackages: 5, positions: [{ positionId: 'position-2', availablePackages: 5, allocatedPackages: 5, pickedPackages: 0 }] })]
|
||||
storage(); const rows = [item({ scheduledPackages: 3 }), item({ dispatchItemId: 'item-2', sourceKey: 'source-2', sourceBusinessNo: 'RL-2', customerId: 'customer-2', customerName: '客户乙', scheduledPackages: 5, positions: [{ positionId: 'position-2', availablePackages: 5, allocatedPackages: 0, pickedPackages: 0 }] })]
|
||||
let sent
|
||||
const state = page({ pickDispatchItem: async (...args) => { sent = args; return proof(fulfillment.dispatch.pendingCommand) }, getDispatchTask: async () => task({ recordVersion: 1, items: rows.map((row, index) => index ? { ...row, pickedPackages: 5 } : row) }) })
|
||||
state.current = task({ items: rows }); state.handleScan('PALLET-1')
|
||||
@@ -123,17 +139,37 @@ test('selection restoration cannot reuse the same item ID for a different logica
|
||||
storage(); const state = page(); state.selectPallet(state.current.items[0]); assert.equal(state.canPick, true)
|
||||
state.current.items[0].sourceKey = 'different-source'; assert.equal(state.selectedItem, null); assert.equal(state.canPick, false)
|
||||
})
|
||||
for (const sourceType of ['INBOUND_PLAN', 'RETURN_RECEIPT']) test(`ordinary ${sourceType} null business fields survive real wire normalization and exact picking`, async () => {
|
||||
for (const sourceType of ['INBOUND_PLAN', 'RETURN_RECEIPT']) test(`ordinary ${sourceType} real 1/0/0 stock and null business fields allow one exact pick`, async () => {
|
||||
storage(); let sent
|
||||
const raw = JSON.parse(JSON.stringify({ dispatchNo: 'PC/1', taskNo: 'TASK-1', recordVersion: '0', status: 'ACTIVE', stage: 'LOADING', actions: { PICK: { allowed: true } }, pickingReconciled: false, items: [item({ sourceKey: `${sourceType}:source-1`, sourceBusinessType: null, sourceBusinessNo: null, scheduledPackages: '4', pickedPackages: '0', positions: [{ positionId: '9007199254740993', balanceId: '9007199254740994', availablePackages: '4', allocatedPackages: '4', pickedPackages: '0' }] })] }))
|
||||
const raw = JSON.parse(JSON.stringify({ dispatchNo: 'PC/1', taskNo: 'TASK-1', recordVersion: '0', status: 'ACTIVE', stage: 'LOADING', actions: { PICK: { allowed: true } }, pickingReconciled: false, items: [item({ sourceKey: `${sourceType}:source-1`, sourceBusinessType: null, sourceBusinessNo: null, scheduledPackages: '1', pickedPackages: '0', positions: [{ positionId: '9007199254740993', balanceId: '9007199254740994', availablePackages: '1', allocatedPackages: '0', pickedPackages: '0' }] })] }))
|
||||
const normalized = normalizeDispatchTask(raw)
|
||||
assert.equal(normalized.items[0].sourceBusinessType, null); assert.equal(normalized.items[0].sourceBusinessNo, null)
|
||||
const state = page({ pickDispatchItem: async (...args) => { sent = args; return proof(fulfillment.dispatch.pendingCommand) }, getDispatchTask: async () => normalizeDispatchTask({ ...raw, recordVersion: 1 }) })
|
||||
state.current = normalized; state.handleScan('PALLET-1'); assert.equal(state.canPick, true)
|
||||
state.updatePackages({ detail: { value: '4' } }); await state.submitPick()
|
||||
assert.equal(sent[1], 'item-1'); assert.equal(sent[2].positionId, '9007199254740993'); assert.equal(sent[2].packages, 4); assert.equal(sent[2].recordVersion, '0')
|
||||
state.current = normalized; state.handleScan('PALLET-1'); assert.equal(state.canPick, true); assert.equal(state.selectedRemaining, 1)
|
||||
assert.equal(state.pickablePackages(state.current.items[0]), 1)
|
||||
state.updatePackages({ detail: { value: '1' } }); await state.submitPick()
|
||||
assert.equal(sent[1], 'item-1'); assert.equal(sent[2].positionId, '9007199254740993'); assert.equal(sent[2].packages, 1); assert.equal(sent[2].recordVersion, '0')
|
||||
assert.equal('customerId' in sent[2], false)
|
||||
})
|
||||
test('shared relabel pallet with real string 1/0/0 positions requires a source choice and picks each exact source once', async () => {
|
||||
storage(); const sent = []; let version = 5
|
||||
let rows = ['1', '2'].map(number => item({ dispatchItemId: `item-${number}`, sourceKey: `source-${number}`, sourceBusinessNo: `RL-${number}`, customerId: `customer-${number}`, scheduledPackages: '1', pickedPackages: '0', positions: [{ positionId: `position-${number}`, availablePackages: '1', allocatedPackages: '0', pickedPackages: '0' }] }))
|
||||
const state = page({ pickDispatchItem: async (no, itemId, body) => {
|
||||
sent.push({ no, itemId, body }); rows = rows.map(row => row.dispatchItemId === itemId ? { ...row, pickedPackages: '1', positions: row.positions.map(position => ({ ...position, availablePackages: '0' })) } : row)
|
||||
return proof(fulfillment.dispatch.pendingCommand, ++version)
|
||||
}, getDispatchTask: async () => task({ recordVersion: version, items: rows, pickingReconciled: rows.every(row => row.pickedPackages === '1') }) })
|
||||
state.current = task({ recordVersion: version, items: rows }); state.handleScan('PALLET-1')
|
||||
assert.equal(fulfillment.dispatch.dispatchItemId, ''); assert.equal(state.canPick, false)
|
||||
assert.deepEqual(state.sourceChoices.map(row => [row.sourceKey, state.pickablePackages(row)]), [['source-1', 1], ['source-2', 1]])
|
||||
state.chooseSource(state.sourceChoices[1]); assert.equal(state.selectedRemaining, 1); assert.equal(state.canPick, true); await state.submitPick()
|
||||
assert.equal(state.current.stage, 'LOADING'); assert.equal(state.canComplete, false)
|
||||
assert.equal(state.current.items[0].pickedPackages, 0); assert.equal(state.current.items[1].pickedPackages, 1)
|
||||
state.handleScan('PALLET-1'); assert.equal(state.selectedItem.sourceKey, 'source-1'); assert.equal(state.canPick, true); await state.submitPick()
|
||||
assert.deepEqual(sent.map(entry => [entry.no, entry.itemId, entry.body.positionId, entry.body.packages, entry.body.recordVersion]), [['PC/1', 'item-2', 'position-2', 1, 5], ['PC/1', 'item-1', 'position-1', 1, 6]])
|
||||
assert.notEqual(sent[0].body.idempotencyKey, sent[1].body.idempotencyKey)
|
||||
assert.ok(sent.every(entry => !('customerId' in entry.body) && !('warehouseId' in entry.body)))
|
||||
assert.equal(fulfillment.dispatch.pendingCommand, null); assert.equal(state.canPick, false); assert.equal(state.canComplete, true)
|
||||
})
|
||||
test('RELABEL_ORDER still requires its true business number and all sources require sourceKey', () => {
|
||||
storage(); const state = page()
|
||||
state.selectPallet(item({ sourceBusinessNo: null })); assert.equal(fulfillment.dispatch.dispatchItemId, ''); assert.match(state.error.message, /来源业务身份/)
|
||||
|
||||
Reference in New Issue
Block a user