保留待确认箱后的排队扫码并阻止提前结束收货

This commit is contained in:
2026-11-04 14:47:41 +08:00
parent a42ecf4a59
commit 8199234bd7
2 changed files with 43 additions and 3 deletions
+3 -3
View File
@@ -122,7 +122,7 @@
</template>
</view>
<view v-if="lastReceipt" class="success-card">本次已收 {{ lastReceipt.acceptedPackages }} 箱,整柜剩余 {{ current.pendingPackages }} 箱</view>
<view v-if="canFinishOnsite" class="sticky-action-dock"><view class="sticky-action one"><button class="primary-button complete-button" :disabled="busy || activeTarget === 'quantity' || scanQueue.length" @tap="completeContainer">{{ allInboundsReconciled ? '完成整柜入库' : '现场收货结束(标记差异)' }}</button></view></view>
<view v-if="canFinishOnsite" class="sticky-action-dock"><view class="sticky-action one"><button class="primary-button complete-button" :disabled="busy || canSubmitReceipt || activeTarget === 'quantity' || scanQueue.length" @tap="completeContainer">{{ allInboundsReconciled ? '完成整柜入库' : '现场收货结束(标记差异)' }}</button></view></view>
</template>
<template v-else-if="phase === 'completed'">
@@ -202,7 +202,7 @@
this.cartonInput = scan.value
return this.resolveCarton(scan.value)
},
drainScanQueue() { if (!this._pageActive || this.loading || this.busy || this.activeTarget === 'quantity' || !this.scanQueue.length || this.phase !== 'receive') return; const next = this.scanQueue.shift(); this.dispatchHardwareScan(next) },
drainScanQueue() { if (!this._pageActive || this.loading || this.busy || this.canSubmitReceipt || this.activeTarget === 'quantity' || !this.scanQueue.length || this.phase !== 'receive') return; const next = this.scanQueue.shift(); this.dispatchHardwareScan(next) },
suspendReceiving() { this._pageActive = false; this._scanEpoch = (this._scanEpoch || 0) + 1; this._resumeReceiving = true; clearTimeout(this._drainTimer); clearTimeout(this._toastTimer); this.scanQueue = []; this.stopScanner() },
scheduleScanDrain() { clearTimeout(this._drainTimer); if (!this._pageActive) return; const epoch = this._scanEpoch || 0; this._drainTimer = setTimeout(() => { if (this._pageActive && epoch === (this._scanEpoch || 0)) this.drainScanQueue() }, 0) },
async run(action, work, isCurrent = () => true) {
@@ -265,7 +265,7 @@
} catch (error) { if (!this.error) this.error = error }
},
async submitReceipt() { const isFba = this.activeInbound?.destinationType === 'FBA'; if (isFba && Number(this.packageCount) > MAX_PACKAGES_PER_PALLET) { this.error = Object.assign(new Error(PALLET_CAPACITY_MESSAGE), { code: 'A0409' }); playErrorSound(); this.showToast(PALLET_CAPACITY_MESSAGE, 'error'); return } if (!this.canSubmitReceipt) return; const payload = isFba ? { inboundCode: this.activeInbound.inboundCode, palletCode: this.resolvedPallet.palletCode, packageCount: Number(this.packageCount), expectedPalletVersion: this.resolvedPallet.version } : { cartonMark: this.activeInbound.cartonMark, palletCode: this.resolvedPallet.palletCode }; const key = idempotencyKey(isFba ? 'bulk-receipt' : 'carton-receipt', { containerNo: this.current?.containerNo, groupKey: this.activeInbound.groupKey, ...payload }); try { const result = await this.run('提交实收', async () => { const receipt = isFba ? await receiveCurrentBulk({ ...payload, idempotencyKey: key.value }) : await receiveCurrentCarton({ ...payload, idempotencyKey: key.value }); const current = this.validateReceivingWarehouse(await getCurrentReceiving(), '当前接柜会话'); return { receipt, current } }); releaseIdempotencyKey(key.signature); receiving.lastReceipt = result.receipt; await this.applyCurrentReceiving(result.current); this.clearReceiptContext(); playSuccessSound() } catch (_) {} },
async completeContainer() { if (!this.canFinishOnsite || this.activeTarget === 'quantity' || this.scanQueue.length) return; const payload = { allowDifference: !this.allInboundsReconciled }; const key = idempotencyKey('complete', { containerNo: this.current.containerNo, ...payload }); try { const value = await this.run(this.allInboundsReconciled ? '完成入库' : '结束现场收货', async () => this.validateReceivingWarehouse(await completeCurrentReceiving({ ...payload, idempotencyKey: key.value }), '当前接柜会话')); releaseIdempotencyKey(key.signature); await this.applyCurrentReceiving(value) } catch (_) {} },
async completeContainer() { if (!this.canFinishOnsite || this.canSubmitReceipt || this.activeTarget === 'quantity' || this.scanQueue.length) return; const payload = { allowDifference: !this.allInboundsReconciled }; const key = idempotencyKey('complete', { containerNo: this.current.containerNo, ...payload }); try { const value = await this.run(this.allInboundsReconciled ? '完成入库' : '结束现场收货', async () => this.validateReceivingWarehouse(await completeCurrentReceiving({ ...payload, idempotencyKey: key.value }), '当前接柜会话')); releaseIdempotencyKey(key.signature); await this.applyCurrentReceiving(value) } catch (_) {} },
async finishAndReturn() { resetReceivingDraft(); await this.loadCandidates(false) },
showToast(message, type) { clearTimeout(this._toastTimer); this.toast = { visible: true, message, type }; this._toastTimer = setTimeout(() => { this.toast.visible = false }, 2600) }
}
+40
View File
@@ -88,6 +88,46 @@ test('queued pallet scan follows the current step while a pending carton require
assert.deepEqual(calls, [['pallet', 'PAL-A'], ['carton', 'IN-A#2']])
})
test('queued carton remains pending until the current carton receipt is confirmed', async t => {
store.resetReceivingSession(); store.setCurrentReceiving(sessionValue())
store.receiving.resolvedCarton = { inboundCode: 'IN-A', cartonMark: 'IN-A#1', destinationType: 'OVERSEAS' }
store.receiving.resolvedPallet = { palletCode: 'PAL-A', compatible: true }
const { vm } = page()
t.after(() => vm.suspendReceiving())
const parsed = []
vm.resolveCarton = value => parsed.push(value)
vm.palletInput = 'PAL-A'; vm.activeTarget = 'carton'
vm.scanQueue = [{ value: 'IN-A#2', target: 'carton' }]
vm.drainScanQueue()
assert.equal(vm.scanQueue.length, 1)
assert.deepEqual(parsed, [])
vm.clearReceiptContext()
await new Promise(resolve => setTimeout(resolve, 5))
assert.deepEqual(parsed, ['IN-A#2'])
assert.equal(vm.scanQueue.length, 0)
})
test('cannot finish a container while an overseas carton awaits receipt confirmation', async t => {
store.resetReceivingSession(); store.setCurrentReceiving(sessionValue())
store.receiving.resolvedCarton = { inboundCode: 'IN-A', cartonMark: 'IN-A#1', destinationType: 'OVERSEAS' }
store.receiving.resolvedPallet = { palletCode: 'PAL-A', compatible: true }
let completions = 0
const { vm } = page({ completeCurrentReceiving: async () => {
completions++
return sessionValue({ status: 'COMPLETED_WITH_DIFFERENCE' })
} })
t.after(() => vm.suspendReceiving())
vm.applyCurrentReceiving = async () => {}
vm.palletInput = 'PAL-A'; vm.activeTarget = 'carton'
await vm.completeContainer()
assert.equal(completions, 0)
vm.clearReceiptContext()
await vm.completeContainer()
assert.equal(completions, 1)
})
test('an incompatible pallet never reports success and a corrected scan can be validated normally', async t => {
store.resetReceivingSession(); store.setCurrentReceiving(sessionValue())
store.receiving.resolvedCarton = { inboundCode: 'IN-A', cartonMark: 'IN-A#22-1', destinationType: 'FBA' }