From 79482b4de8673887feea64a6d0d67ecfa6802794 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 20 Oct 2026 14:01:10 +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 | 14 ++++++++------ tests/outbound-lifecycle.test.mjs | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/api/fulfillment.js b/src/api/fulfillment.js index acd177a..9d9c5d5 100644 --- a/src/api/fulfillment.js +++ b/src/api/fulfillment.js @@ -162,6 +162,7 @@ export function normalizeCargoDiscardTask(value) { quantityUnit: sku?.quantityUnit === '件' ? '件' : '箱', plannedPackages: count(sku?.plannedPackages), actualPackages: sku?.actualPackages == null ? null : count(sku.actualPackages), + actualQuantityEditable: sku?.actualQuantityEditable === true, status: String(sku?.status || 'PENDING') })) } diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue index bca0969..439cf9d 100644 --- a/src/pages/outbound/outbound.vue +++ b/src/pages/outbound/outbound.vue @@ -45,9 +45,9 @@ {{ successMessage }} @@ -110,7 +110,7 @@ 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)))))) } + 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 => this.validDiscardActual(sku)) && (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() }, onHide() { this.stopScanner() }, @@ -155,7 +155,7 @@ if (task.discardNo !== order.discardNo) throw new Error('扫描的托盘不属于当前废弃订单') this.discardTask = normalizeCargoDiscardTask(task) if (this.discardTask.scanMode === 'CARTON' && this.discardTask.cartonMarks.includes(code)) this.discardScannedMarks = [code] - this.discardActuals = Object.fromEntries(this.discardTask.skus.map((sku) => [sku.itemId, sku.actualPackages == null ? sku.plannedPackages : sku.actualPackages])); await this.restoreDiscardPhotos() + this.discardActuals = Object.fromEntries(this.discardTask.skus.map((sku) => [sku.itemId, sku.actualPackages == null ? (sku.actualQuantityEditable ? '' : sku.plannedPackages) : sku.actualPackages])); await this.restoreDiscardPhotos() } catch (error) { if (this.discardCurrent === order) this.setError(error, '废弃托盘读取失败') } finally { if (this.discardCurrent === order) this.discardLoading = false } }, @@ -193,8 +193,10 @@ finally { this.discardPhotoBusy = false } }, fail: () => { this.discardPhotoBusy = false } }) }, - updateDiscardActual(itemId, event) { const value = Math.max(0, Math.trunc(Number(event.detail.value || 0))); this.discardActuals = { ...this.discardActuals, [itemId]: value } }, - async confirmDiscard() { if (this.discardBusy || this.discardLoading || !this.canConfirmDiscard || !this.discardTask || !this.discardCurrent) return; this.discardBusy = true; try { const lines = this.discardTask.skus.map((sku) => ({ itemId: sku.itemId, actualPackages: sku.plannedPackages })); await confirmCargoDiscard(this.discardCurrent.discardNo, this.discardTask.palletNo, lines, this.discardPhotos.map(photo => String(photo.fileId)), this.discardScannedMarks, this.discardAllCartons); this.successMessage = '废弃出库作业及照片已提交,已完成'; this.resetDiscardPhotos(); this.discardPalletCode = ''; this.discardTask = null; this.discardTasks = await listCargoDiscardTasks(); playSuccessSound() } catch (error) { this.setError(error, '废弃出库失败') } finally { this.discardBusy = false } }, + // 未填写、负数、小数或超出预留数均阻止提交,保留输入供仓库修正。 + validDiscardActual(sku) { const raw = this.discardActuals[sku.itemId]; if (raw == null || String(raw).trim() === '') return false; const actual = Number(raw); return Number.isSafeInteger(actual) && actual >= 0 && (sku.actualQuantityEditable === true ? actual <= sku.plannedPackages : actual === sku.plannedPackages) }, + updateDiscardActual(itemId, event) { this.discardActuals = { ...this.discardActuals, [itemId]: event.detail.value } }, + async confirmDiscard() { if (this.discardBusy || this.discardLoading || !this.canConfirmDiscard || !this.discardTask || !this.discardCurrent) return; this.discardBusy = true; try { const lines = this.discardTask.skus.map((sku) => ({ itemId: sku.itemId, actualPackages: Number(this.discardActuals[sku.itemId]) })); await confirmCargoDiscard(this.discardCurrent.discardNo, this.discardTask.palletNo, lines, this.discardPhotos.map(photo => String(photo.fileId)), this.discardScannedMarks, this.discardAllCartons); this.successMessage = '废弃出库作业及照片已提交,已完成'; this.resetDiscardPhotos(); this.discardPalletCode = ''; this.discardTask = null; this.discardTasks = await listCargoDiscardTasks(); playSuccessSound() } catch (error) { this.setError(error, '废弃出库失败') } finally { this.discardBusy = false } }, handleScan(raw) { const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim(); if (!code || this.busy || this.loading || this.discardBusy || this.discardLoading) return console.info('[Overseas PDA outbound] scan', { code, task: this.current?.taskNo || '', discard: this.discardCurrent?.discardNo || '' }); this.scanCode = code; updateFulfillmentDraft('outbound', { scanCode: code }) diff --git a/tests/outbound-lifecycle.test.mjs b/tests/outbound-lifecycle.test.mjs index d7d92df..1a6f0e4 100644 --- a/tests/outbound-lifecycle.test.mjs +++ b/tests/outbound-lifecycle.test.mjs @@ -182,6 +182,30 @@ test('discard confirmation is guarded against double submission', async () => { pending.resolve(); await Promise.all([first, second]); assert.equal(submits, 1) }) +test('无精确箱唛对应的托盘必须填写实际数并按实点提交,不能超量或用空值代替零', async () => { + let payload + const { vm } = page({ confirmCargoDiscard: async (...args) => { payload = args }, listCargoDiscardTasks: async () => [] }) + vm.discardCurrent = { discardNo: 'CD-A' } + vm.discardTask = { palletNo: 'P-A', status: 'PENDING', scanMode: 'PALLET', cartonMarks: [], skus: [{ itemId: 'sku', plannedPackages: 10, actualQuantityEditable: true }] } + vm.discardPhotoReady = true; vm.discardPhotos = [{ fileId: '101' }] + for (const value of ['', ' ', null, -1, 11, 1.5, 'abc']) { + vm.discardActuals = { sku: value }; assert.equal(vm.canConfirmDiscard, false) + } + vm.discardActuals = { sku: '0' }; assert.equal(vm.canConfirmDiscard, true) + vm.discardActuals = { sku: '6' }; assert.equal(vm.canConfirmDiscard, true) + await vm.confirmDiscard() + assert.equal(payload[0], 'CD-A'); assert.equal(payload[1], 'P-A') + assert.deepEqual(payload[2], [{ itemId: 'sku', actualPackages: 6 }]) +}) + +test('无箱唛对应托盘扫码后实际数保持未填,必须由仓库实点填写', async () => { + const { vm } = page({ resolveCargoDiscardPallet: async () => ({ discardNo: 'CD-A', palletNo: 'P-A', cartonMarks: [], scanMode: 'PALLET', skus: [{ itemId: 'sku', plannedPackages: 10, actualQuantityEditable: true }] }) }) + vm.discardCurrent = { discardNo: 'CD-A' } + await vm.resolveDiscardPallet('P-A') + assert.equal(vm.discardActuals.sku, '') + assert.equal(vm.canConfirmDiscard, false) +}) + test('discard retry stays in the discard workflow instead of restoring an old outbound task', async () => { const { vm } = page(); const calls = [] vm.discardCurrent = { discardNo: 'DISCARD' }; vm.discardPalletCode = 'PALLET'