diff --git a/src/api/fulfillment.js b/src/api/fulfillment.js
index 2aa4a04..c2a189f 100644
--- a/src/api/fulfillment.js
+++ b/src/api/fulfillment.js
@@ -163,6 +163,7 @@ export function normalizeCargoDiscardTask(value) {
itemId: String(sku?.itemId || ''),
skuCode: String(sku?.skuCode || ''),
productName: String(sku?.productName || ''),
+ quantityUnit: sku?.quantityUnit === '件' ? '件' : '箱',
plannedPackages: count(sku?.plannedPackages),
actualPackages: sku?.actualPackages == null ? null : count(sku.actualPackages),
status: String(sku?.status || 'PENDING')
diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue
index 26c728c..bca0969 100644
--- a/src/pages/outbound/outbound.vue
+++ b/src/pages/outbound/outbound.vue
@@ -45,8 +45,9 @@
废弃订单{{ discardCurrent.discardNo }}出库单 {{ discardCurrent.outboundNo }}
- 整托废弃扫描托盘号;部分箱子废弃或散箱作业,核对本单所选箱唛,也可一键废弃所选箱子。未选箱子保留库存。
- {{ discardTask.scanMode === 'CARTON' ? (discardTask.palletNo === '散箱' ? '散箱废弃' : '按箱废弃 · ' + discardTask.palletNo) : '托盘 ' + discardTask.palletNo }}{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}{{ sku.skuCode || '—' }}{{ sku.productName || '—' }} · 计划 {{ sku.plannedPackages }} 箱本单所选废弃 {{ sku.plannedPackages }} 箱已核对 {{ discardScannedMarks.length }}/{{ discardTask.cartonMarks.length }} 箱{{ mark }}{{ discardAllCartons ? '全部废弃' : discardScannedMarks.includes(mark) ? '已扫描' : '待扫描' }}已确认本单所选箱子全部废弃,拍照后点击废弃出库完成。废弃现场照片(至少 1 张)照片关联当前废弃作业,完成出库后可在废弃单详情查看。
+ 本单废弃范围{{ task.palletNo === '散箱' ? '散箱' : task.palletNo }}{{ task.status === 'COMPLETED' ? '已完成' : '待废弃' }}{{ mark }}整托废弃,请核对托盘及计划数量。
+
+ {{ discardTask.scanMode === 'CARTON' ? (discardTask.palletNo === '散箱' ? '散箱废弃' : '按箱废弃 · ' + discardTask.palletNo) : '托盘 ' + discardTask.palletNo }}{{ discardTask.status === 'COMPLETED' ? '已完成' : '待废弃' }}本次废弃 {{ discardQuantitySummary }}{{ sku.skuCode || '—' }}{{ sku.productName || '—' }} · {{ sku.plannedPackages }} {{ sku.quantityUnit || '箱' }}已核对 {{ discardScannedMarks.length }}/{{ discardTask.cartonMarks.length }} 箱{{ mark }}{{ discardAllCartons ? '全部废弃' : discardScannedMarks.includes(mark) ? '已扫描' : '待扫描' }}已确认本单所选箱子全部废弃,拍照后点击废弃出库完成。废弃现场照片(至少 1 张)照片关联当前废弃作业,完成出库后可在废弃单详情查看。
{{ successMessage }}
@@ -101,6 +102,14 @@
canComplete() { return this.plannedPackages > 0 && this.remainingPackages === 0 && this.taskOperable },
discardOrders() { const groups = new Map(); for (const task of this.discardTasks) { const current = groups.get(task.discardNo) || { discardNo: task.discardNo, outboundNo: task.outboundNo, palletCount: 0, cartonCount: 0, pendingPalletCount: 0 }; if (task.scanMode === 'CARTON') current.cartonCount += (task.cartonMarks || []).length; else current.palletCount += 1; if (task.status !== 'COMPLETED') current.pendingPalletCount += 1; groups.set(task.discardNo, current) } return [...groups.values()] },
discardOrderCount() { return this.discardOrders.length },
+ discardCurrentTasks() { return this.discardTasks.filter(task => task.discardNo === this.discardCurrent?.discardNo) },
+ discardQuantitySummary() {
+ const task = this.discardTask
+ if (!task) return ''
+ const pieces = task.skus.filter(sku => sku.quantityUnit === '件').reduce((total, sku) => total + sku.plannedPackages, 0)
+ const boxes = new Set(task.cartonMarks || []).size || task.skus.filter(sku => sku.quantityUnit !== '件').reduce((total, sku) => total + sku.plannedPackages, 0)
+ return `${boxes} 箱${pieces ? ` · ${pieces} 件` : ''}`
+ },
canConfirmDiscard() { const task = this.discardTask; return Boolean(this.discardPhotoReady && this.discardPhotos.length > 0 && !this.discardPhotoBusy && !this.discardLoading && !this.discardBusy && task && task.status !== 'COMPLETED' && task.skus.length && task.skus.every(sku => Number(this.discardActuals[sku.itemId]) === sku.plannedPackages) && (task.scanMode !== 'CARTON' || (task.cartonMarks?.length && (this.discardAllCartons || task.cartonMarks.every(mark => this.discardScannedMarks.includes(mark)))))) }
},
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`); this.startScanner(); if (this.discardPhotoBusy || this.discardBusy) return; if (this.discardCurrent) { if (this.discardTask) this.restoreDiscardPhotos(); return } if (!this.bootstrapped) { this.bootstrapped = true; this.scanCode = ''; this.loadTasks(); return } if (this.current?.taskNo) this.openTask(this.current.taskNo); else this.loadTasks() },
@@ -133,10 +142,11 @@
closeDiscard() { if (this.discardBusy || this.discardPhotoBusy) return; this.resetDiscardPhotos(); this.discardLoading = false; this.discardCurrent = null; this.discardTask = null; this.discardPalletCode = ''; this.discardActuals = {}; this.loadTasks() },
updateDiscardPalletCode(value) { this.discardPalletCode = value },
async resolveDiscardPallet(raw) {
- const code = String(raw?.detail?.value ?? raw ?? this.discardPalletCode).trim()
+ const code = String(typeof raw === 'string' ? raw : raw?.detail?.value ?? this.discardPalletCode).trim()
if (!code || !this.discardCurrent || this.discardBusy || this.discardLoading || this.discardPhotoBusy) return
if (this.discardTask?.scanMode === 'CARTON' && this.discardTask.cartonMarks.includes(code)) { if (!this.discardScannedMarks.includes(code)) this.discardScannedMarks = [...this.discardScannedMarks, code]; this.error = null; playSuccessSound(); return }
- if (this.discardTask?.scanMode === 'CARTON' && !this.discardTasks.some(task => task.discardNo === this.discardCurrent.discardNo && task.scanMode !== 'CARTON' && task.palletNo === code)) { this.setError(new Error('箱唛不属于当前废弃作业'), '箱唛核对失败'); return }
+ // 当前托盘可重扫,同一废弃单内也可通过托盘或所选箱唛切换任务。
+ if (this.discardTask?.scanMode === 'CARTON' && !this.discardTasks.some(task => task.discardNo === this.discardCurrent.discardNo && (task.palletNo === code || (task.cartonMarks || []).includes(code)))) { this.setError(new Error('托盘或箱唛不属于当前废弃单所选货物'), '箱唛核对失败'); return }
const order = this.discardCurrent
this.resetDiscardPhotos(); this.discardScannedMarks = []; this.discardAllCartons = false; this.discardPalletCode = code; this.discardLoading = true; this.discardTask = null; this.discardActuals = {}; this.error = null
try {
diff --git a/tests/outbound-lifecycle.test.mjs b/tests/outbound-lifecycle.test.mjs
index 787fee3..d7d92df 100644
--- a/tests/outbound-lifecycle.test.mjs
+++ b/tests/outbound-lifecycle.test.mjs
@@ -27,6 +27,16 @@ test('scanning a discard outbound document opens the discard workflow with its l
assert.equal(vm.discardCurrent.outboundNo, 'OB-1')
})
+test('退货废弃一箱五件分别展示,混装SKU按箱唛去重', () => {
+ const { vm } = page()
+ vm.discardTask = { cartonMarks: ['RETURN-BOX-1'], skus: [{ quantityUnit: '件', plannedPackages: 5 }] }
+ assert.equal(vm.discardQuantitySummary, '1 箱 · 5 件')
+ vm.discardTask.skus = [{ quantityUnit: '件', plannedPackages: 2 }, { quantityUnit: '件', plannedPackages: 3 }]
+ assert.equal(vm.discardQuantitySummary, '1 箱 · 5 件')
+ vm.discardTask = { cartonMarks: ['BOX-1', 'BOX-1'], skus: [{ quantityUnit: '箱', plannedPackages: 1 }] }
+ assert.equal(vm.discardQuantitySummary, '1 箱')
+})
+
test('discard pallet resolution passes the selected discard number', async () => {
let received
const { vm } = page({ resolveCargoDiscardPallet: async (...args) => {
@@ -38,6 +48,45 @@ test('discard pallet resolution passes the selected discard number', async () =>
assert.deepEqual(received, ['P-1', 'CD-2'])
})
+test('点击打开废弃作业使用输入框值,不把 tap 事件当作托盘号', async () => {
+ let received
+ const { vm } = page({ resolveCargoDiscardPallet: async (...args) => {
+ received = args
+ return { discardNo: 'CD-2', palletNo: 'P-1', skus: [] }
+ } })
+ vm.discardCurrent = { discardNo: 'CD-2' }
+ vm.discardPalletCode = ' P-1 '
+ await vm.resolveDiscardPallet({ type: 'tap', detail: { x: 10, y: 20 } })
+ assert.deepEqual(received, ['P-1', 'CD-2'])
+ assert.equal(vm.discardPalletCode, 'P-1')
+})
+
+test('同单按箱废弃允许扫描当前托盘及其他托盘所选箱唛,范围不扩展到其他单据', async () => {
+ const requests = []
+ const tasks = [
+ { discardNo: 'CD-1', palletNo: 'P-1', scanMode: 'CARTON', cartonMarks: ['BOX-1'], skus: [], status: 'PENDING' },
+ { discardNo: 'CD-1', palletNo: 'P-2', scanMode: 'CARTON', cartonMarks: ['BOX-2'], skus: [], status: 'PENDING' },
+ { discardNo: 'CD-OTHER', palletNo: 'P-3', scanMode: 'CARTON', cartonMarks: ['BOX-3'], skus: [], status: 'PENDING' },
+ ]
+ const { vm } = page({ resolveCargoDiscardPallet: async code => {
+ requests.push(code)
+ return tasks.find(task => task.palletNo === code || task.cartonMarks.includes(code))
+ } })
+ vm.discardTasks = tasks
+ vm.openDiscardOrder('CD-1')
+ await Promise.resolve()
+ assert.equal(vm.discardCurrentTasks.length, 2)
+ await vm.resolveDiscardPallet('P-1')
+ assert.equal(vm.error, null)
+ await vm.resolveDiscardPallet('BOX-2')
+ assert.equal(vm.discardTask.palletNo, 'P-2')
+ assert.deepEqual(vm.discardScannedMarks, ['BOX-2'])
+ await vm.resolveDiscardPallet('BOX-3')
+ assert.deepEqual(requests, ['P-1', 'BOX-2'])
+ assert.equal(vm.discardTask.palletNo, 'P-2')
+ assert.deepEqual(vm.discardScannedMarks, ['BOX-2'])
+})
+
test('discard requires restored photos and retains task while native camera returns', async () => {
const { vm, definition } = page({ requireSession: () => true, restoreFulfillmentDraft() {} })
vm.discardCurrent = { discardNo: 'CD-1' }; vm.discardTask = { palletNo: 'PALLET', status: 'PENDING', skus: [{ itemId: 'sku', plannedPackages: 3 }] }; vm.discardActuals = { sku: 3 }