From 51ecbee653b8afe24b254482cc88a7e0aa7eeac8 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Mon, 19 Oct 2026 16:40:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/fulfillment.js | 1 + src/pages/outbound/outbound.vue | 18 +++++++++--- tests/outbound-lifecycle.test.mjs | 49 +++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) 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 @@ {{ 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 }