fix(pda): display physical remaining cartons after partial picks
This commit is contained in:
@@ -167,7 +167,7 @@
|
||||
|
||||
<view v-if="activeStage === 'picking'" class="submit-section">
|
||||
<view class="submit-info">
|
||||
<view class="submit-pending"><text class="submit-pending-number">{{ summary.pending }}</text><text class="submit-pending-label"> 箱待拣</text></view>
|
||||
<view class="submit-pending"><text class="submit-pending-number">{{ remainingCartons }}</text><text class="submit-pending-label"> 箱待拣</text></view>
|
||||
<text class="submit-status-hint">{{ submitStatusHint }}</text>
|
||||
</view>
|
||||
<view v-if="remoteError && !pickingCompleted" class="remote-error-row submit-error-row">
|
||||
@@ -337,7 +337,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ITEM_STATUS, classifyScan, createItemStates, getPickSummary, isSealedPalletItem, submitStates, summarizeInventoryResources } from '@/utils/picking-state.js'
|
||||
import { ITEM_STATUS, classifyScan, createItemStates, getPickSummary, isSealedPalletItem, submitStates, summarizeInventoryResources, remainingPhysicalCartons } from '@/utils/picking-state.js'
|
||||
import { SKIP_REASONS, INCOMPLETE_REASONS } from '@/data/demo-data.js'
|
||||
import { session, requireSession } from '@/stores/session.js'
|
||||
import { clearLoadingCompletionAttempt, clearPickingDraft, getPickingSession, restoreLoadingCompletionAttempt, restorePickingSession, saveLoadingCompletionAttempt, savePickingSession } from '@/stores/picking.js'
|
||||
@@ -417,6 +417,9 @@
|
||||
pendingItems() {
|
||||
return this.currentBox.items.filter((item) => this.itemState(item) === ITEM_STATUS.PENDING)
|
||||
},
|
||||
remainingCartons() {
|
||||
return remainingPhysicalCartons(this.currentBox.items, this.itemStates, this.partialQuantities)
|
||||
},
|
||||
scanInputLabel() {
|
||||
return this.pendingItems.some((item) => isSealedPalletItem(item)) ? '托盘码或托内箱码扫码框' : '箱码扫码框'
|
||||
},
|
||||
@@ -1028,7 +1031,7 @@
|
||||
[ITEM_STATUS.PARTIAL]: '已部分拣货',
|
||||
[ITEM_STATUS.SKIPPED]: '已跳拣'
|
||||
}
|
||||
this.recordScanResult('warning', '重复扫描', value, `该条码已${labels[result.status]},无需重复处理`)
|
||||
this.recordScanResult('warning', '重复扫描', value, `该条码${labels[result.status]},无需重复处理`)
|
||||
this.showToast(`该条码已处理:${value}`, 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { readFile } from 'node:fs/promises'
|
||||
|
||||
const source = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
|
||||
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||
const stateSource = await readFile(new URL('../utils/picking-state.js', import.meta.url), 'utf8')
|
||||
const pickingState = await import(`data:text/javascript;base64,${Buffer.from(stateSource).toString('base64')}`)
|
||||
const noop = () => {}
|
||||
const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no }); return { promise, resolve, reject } }
|
||||
function mount(overrides = {}, state = {}) {
|
||||
@@ -37,6 +39,46 @@ function mount(overrides = {}, state = {}) {
|
||||
return { page, ctx }
|
||||
}
|
||||
|
||||
test('real scan handlers display remaining 2→1→1→0, preserve pending semantics and avoid duplicate POST', async () => {
|
||||
let posts = 0
|
||||
const messages = []
|
||||
const { page, ctx } = mount({ ...pickingState, pickContainerItem: async () => {
|
||||
posts++
|
||||
return { version: posts + 1, items: [{ id: 'item', plannedPackageCount: 2, pickedPackageCount: posts,
|
||||
itemStatus: posts === 2 ? 'PICKED' : 'PICKING' }] }
|
||||
} }, { scannedBoxCodes: {}, requirePickingOperation: () => true, recordScanResult: (...args) => messages.push(args) })
|
||||
ctx.currentBox.items[0].boxCodes = ['BOX-1', 'BOX-2']
|
||||
ctx.currentBox.items[0].plannedPackageCount = 2
|
||||
ctx.currentBox.items[0].pickedPackageCount = 0
|
||||
const remaining = () => page.computed.remainingCartons.call(ctx)
|
||||
assert.equal(remaining(), 2)
|
||||
await ctx.processScannedValue('BOX-1')
|
||||
assert.equal(remaining(), 1)
|
||||
assert.equal(page.computed.summary.call(ctx).pending, 0)
|
||||
await ctx.processScannedValue('BOX-1')
|
||||
assert.equal(remaining(), 1); assert.equal(posts, 1)
|
||||
assert.equal(messages.at(-1)[3], '该条码已部分拣货,无需重复处理')
|
||||
await ctx.processScannedValue('BOX-2')
|
||||
assert.equal(remaining(), 0); assert.equal(posts, 2)
|
||||
assert.match(source, /submit-pending-number">\{\{ remainingCartons \}\}/)
|
||||
})
|
||||
|
||||
test('physical remaining distinguishes skipped and partial from business pending and trusts server counts', () => {
|
||||
const items = [{ id: 'partial', qty: 4, pickedPackageCount: 1 }, { id: 'skip', qty: 3, pickedPackageCount: 0 }]
|
||||
const states = { partial: pickingState.ITEM_STATUS.PARTIAL, skip: pickingState.ITEM_STATUS.SKIPPED }
|
||||
const quantities = { partial: 1 }
|
||||
const before = JSON.stringify({ items, states, quantities })
|
||||
const summary = pickingState.getPickSummary(items, states, quantities)
|
||||
assert.equal(summary.pending, 0); assert.equal(summary.partial, 1); assert.equal(summary.skipped, 3)
|
||||
assert.equal(pickingState.remainingPhysicalCartons(items, states, quantities), 6)
|
||||
assert.equal(pickingState.getPickSummary(items, states, quantities).canSubmit, summary.canSubmit)
|
||||
assert.equal(JSON.stringify({ items, states, quantities }), before)
|
||||
assert.equal(pickingState.remainingPhysicalCartons([{ id: 'partial', qty: 4, pickedPackageCount: 1 }],
|
||||
{ partial: pickingState.ITEM_STATUS.PICKED }, { partial: 4 }), 3)
|
||||
assert.equal(pickingState.remainingPhysicalCartons([{ id: 'partial', qty: 4 }], states, quantities), 3)
|
||||
assert.equal(pickingState.remainingPhysicalCartons([{ id: 'skip', qty: 3 }], states, quantities), 3)
|
||||
})
|
||||
|
||||
test('double confirmation and direct reentry send exactly one snapshot and one batch', async () => {
|
||||
const snapshot = deferred(), batch = deferred(); let snapshots = 0, batches = 0
|
||||
const { ctx } = mount({ submitContainerPickingBatch: async () => { batches++; return batch.promise } }, {
|
||||
|
||||
@@ -9,6 +9,20 @@ export function isSealedPalletItem(item) {
|
||||
return item?.unitType === 'SEALED_PALLET'
|
||||
}
|
||||
|
||||
// Physical cartons still absent from the picked count, independent of whether
|
||||
// the operator has handled the line as partial or skipped.
|
||||
export function remainingPhysicalCartons(items, states = {}, partialQuantities = {}) {
|
||||
const count = (value) => Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : 0
|
||||
return items.reduce((total, item) => {
|
||||
const planned = count(item.plannedPackageCount ?? item.qty)
|
||||
const hasServerCount = item.pickedPackageCount !== null && item.pickedPackageCount !== undefined && item.pickedPackageCount !== ''
|
||||
const picked = hasServerCount ? count(item.pickedPackageCount)
|
||||
: states[item.id] === ITEM_STATUS.PICKED ? planned
|
||||
: states[item.id] === ITEM_STATUS.PARTIAL ? count(partialQuantities[item.id]) : 0
|
||||
return total + Math.max(0, planned - picked)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
export function summarizeInventoryResources(resources) {
|
||||
const list = Array.isArray(resources) ? resources : []
|
||||
const locationCodes = [...new Set(list
|
||||
|
||||
Reference in New Issue
Block a user