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 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } }, {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user