fix(pda): display physical remaining cartons after partial picks

This commit is contained in:
2026-10-02 13:47:41 +08:00
parent 8d2d80ec11
commit 7aebbd2a70
3 changed files with 62 additions and 3 deletions
+6 -3
View File
@@ -167,7 +167,7 @@
<view v-if="activeStage === 'picking'" class="submit-section"> <view v-if="activeStage === 'picking'" class="submit-section">
<view class="submit-info"> <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> <text class="submit-status-hint">{{ submitStatusHint }}</text>
</view> </view>
<view v-if="remoteError && !pickingCompleted" class="remote-error-row submit-error-row"> <view v-if="remoteError && !pickingCompleted" class="remote-error-row submit-error-row">
@@ -337,7 +337,7 @@
</template> </template>
<script> <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 { SKIP_REASONS, INCOMPLETE_REASONS } from '@/data/demo-data.js'
import { session, requireSession } from '@/stores/session.js' import { session, requireSession } from '@/stores/session.js'
import { clearLoadingCompletionAttempt, clearPickingDraft, getPickingSession, restoreLoadingCompletionAttempt, restorePickingSession, saveLoadingCompletionAttempt, savePickingSession } from '@/stores/picking.js' import { clearLoadingCompletionAttempt, clearPickingDraft, getPickingSession, restoreLoadingCompletionAttempt, restorePickingSession, saveLoadingCompletionAttempt, savePickingSession } from '@/stores/picking.js'
@@ -417,6 +417,9 @@
pendingItems() { pendingItems() {
return this.currentBox.items.filter((item) => this.itemState(item) === ITEM_STATUS.PENDING) return this.currentBox.items.filter((item) => this.itemState(item) === ITEM_STATUS.PENDING)
}, },
remainingCartons() {
return remainingPhysicalCartons(this.currentBox.items, this.itemStates, this.partialQuantities)
},
scanInputLabel() { scanInputLabel() {
return this.pendingItems.some((item) => isSealedPalletItem(item)) ? '托盘码或托内箱码扫码框' : '箱码扫码框' return this.pendingItems.some((item) => isSealedPalletItem(item)) ? '托盘码或托内箱码扫码框' : '箱码扫码框'
}, },
@@ -1028,7 +1031,7 @@
[ITEM_STATUS.PARTIAL]: '已部分拣货', [ITEM_STATUS.PARTIAL]: '已部分拣货',
[ITEM_STATUS.SKIPPED]: '已跳拣' [ITEM_STATUS.SKIPPED]: '已跳拣'
} }
this.recordScanResult('warning', '重复扫描', value, `该条码已${labels[result.status]},无需重复处理`) this.recordScanResult('warning', '重复扫描', value, `该条码${labels[result.status]},无需重复处理`)
this.showToast(`该条码已处理:${value}`, 'warning') this.showToast(`该条码已处理:${value}`, 'warning')
return return
} }
+42
View File
@@ -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 source = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1] 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 noop = () => {}
const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no }); return { promise, resolve, reject } } const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no }); return { promise, resolve, reject } }
function mount(overrides = {}, state = {}) { function mount(overrides = {}, state = {}) {
@@ -37,6 +39,46 @@ function mount(overrides = {}, state = {}) {
return { page, ctx } 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 () => { 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 snapshot = deferred(), batch = deferred(); let snapshots = 0, batches = 0
const { ctx } = mount({ submitContainerPickingBatch: async () => { batches++; return batch.promise } }, { const { ctx } = mount({ submitContainerPickingBatch: async () => { batches++; return batch.promise } }, {
+14
View File
@@ -9,6 +9,20 @@ export function isSealedPalletItem(item) {
return item?.unitType === 'SEALED_PALLET' 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) { export function summarizeInventoryResources(resources) {
const list = Array.isArray(resources) ? resources : [] const list = Array.isArray(resources) ? resources : []
const locationCodes = [...new Set(list const locationCodes = [...new Set(list