From e379e5b586fd6f8d585996ddde76d45ce185a8b6 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 29 Sep 2026 20:52:44 +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 | 7 +++- src/api/return-packing.js | 37 +++++++++++++++++ src/api/return-receipt.js | 19 ++++++++- src/pages/outbound/outbound.vue | 21 +++++----- src/pages/return-packing/return-packing.vue | 26 +++++++++--- src/pages/return-receipt/return-receipt.vue | 45 +++++++++++++++------ src/stores/warehouse-work.js | 2 +- tests/fulfillment-workflows.test.mjs | 23 +++++++++-- 8 files changed, 144 insertions(+), 36 deletions(-) diff --git a/src/api/fulfillment.js b/src/api/fulfillment.js index 729bb73..41971bf 100644 --- a/src/api/fulfillment.js +++ b/src/api/fulfillment.js @@ -54,6 +54,7 @@ export function normalizeOutboundTask(value) { ...header, taskNo: String(header?.taskNo || ''), outboundNo: String(header?.outboundNo || ''), + outboundType: String(header?.outboundType || ''), status: String(header?.status || ''), items: asList(value?.items).map((item) => ({ ...item, @@ -117,12 +118,14 @@ export function pickOutboundPallet(taskNo, palletNo, request) { return overseasRequest('POST', `${OUTBOUND_BASE}/${encoded(taskNo)}/pallets/${encoded(palletNo)}/pick`, request) } -export function outboundPickPayload(item, position, packages) { +export function outboundPickPayload(item, position, packages, scannedCode = '') { + const skuScan = item.skuCode && scannedCode === item.skuCode return { outboundPalletItemId: item.outboundPalletItemId, inventoryPositionId: position.inventoryPositionId, packages: Number(packages), - cartonMarks: position.cartonMark ? [position.cartonMark] : [] + cartonMarks: skuScan ? [] : position.cartonMark ? [position.cartonMark] : [], + ...(skuScan ? { skuCode: item.skuCode } : {}) } } diff --git a/src/api/return-packing.js b/src/api/return-packing.js index 9b43e69..0510fc9 100644 --- a/src/api/return-packing.js +++ b/src/api/return-packing.js @@ -55,3 +55,40 @@ export async function mergeReturnCartons(request) { export async function getReturnCartonLabel(cartonMark) { return normalizeReturnLabel(await overseasRequest('GET', `${BASE}/cartons/${encoded(cartonMark)}/label`)) } + +function normalizePlanBox(box = {}) { + return { + boxNo: Number(box.boxNo || 0), + items: Array.isArray(box.items) ? box.items.map((item) => ({ skuCode: String(item.skuCode || ''), quantity: count(item.quantity) })) : [], + length: count(box.length), width: count(box.width), height: count(box.height), weight: count(box.weight), + } +} + +export function normalizeReturnPackingPlan(value = {}) { + const skuLines = Array.isArray(value.skuLines) ? value.skuLines.map((item) => ({ skuCode: String(item.skuCode || ''), productNameZh: String(item.productNameZh || ''), quantity: count(item.quantity) })) : [] + const boxes = Array.isArray(value.boxes) && value.boxes.length ? value.boxes.map(normalizePlanBox) : skuLines.map((item, index) => ({ boxNo: index + 1, items: [{ skuCode: item.skuCode, quantity: item.quantity }], length: 0, width: 0, height: 0, weight: 0 })) + return { + planNo: String(value.planNo || ''), status: String(value.status || 'READY'), mode: String(value.mode || 'WAREHOUSE_RECOMMENDATION'), + customerName: String(value.customerName || ''), customerReferenceNo: String(value.customerReferenceNo || ''), + returnNos: Array.isArray(value.returnNos) ? value.returnNos.map((item) => String(item || '')).filter(Boolean) : [], + skuLines, boxes, remark: String(value.remark || ''), + } +} + +export async function listReturnPackingPlans() { + try { + const value = await overseasRequest('GET', `${BASE}/packing/plans`) + const records = Array.isArray(value) ? value : Array.isArray(value?.records) ? value.records : [] + return records.map(normalizeReturnPackingPlan) + } + catch (error) { if (isNotFound(error)) return []; throw error } +} + +export async function getReturnPackingPlan(planNo) { + return normalizeReturnPackingPlan(await overseasRequest('GET', `${BASE}/packing/plans/${encoded(planNo)}`)) +} + +export async function mergeReturnPackingPlan(planNo, request) { + const value = await overseasRequest('POST', `${BASE}/packing/plans/${encoded(planNo)}/complete`, request) + return { ...value, labels: Array.isArray(value?.labels) ? value.labels.map(normalizeReturnLabel) : [normalizeReturnLabel(value?.label || value)], label: normalizeReturnLabel(value?.label || value) } +} diff --git a/src/api/return-receipt.js b/src/api/return-receipt.js index 3698ddf..35ba2ae 100644 --- a/src/api/return-receipt.js +++ b/src/api/return-receipt.js @@ -47,7 +47,7 @@ export function normalizeReturnSkuCandidates(value = {}) { } export function normalizeReturnReceiptResult(value = {}) { - return { + const result = { returnNo: String(value.returnNo || ''), receiptNo: String(value.receiptNo || ''), receiptBatchNo: String(value.receiptBatchNo || ''), @@ -58,6 +58,17 @@ export function normalizeReturnReceiptResult(value = {}) { receivedQuantity: count(value.receivedQuantity), signedAt: value.signedAt || null } + if (value.cartonMark != null || value.printableHtml != null || value.totalBoxCount != null) { + result.cartonMark = String(value.cartonMark || '') + result.printableHtml = String(value.printableHtml || '') + result.totalBoxCount = count(value.totalBoxCount) + } + if (value.unitType != null || value.unitCode != null) { + result.unitType = String(value.unitType || (value.palletCode ? 'PALLET' : '')) + result.unitCode = String(value.unitCode || value.palletCode || '') + } + if (Array.isArray(value.labels)) result.labels = value.labels.map((item) => ({ cartonMark: String(item.cartonMark || ''), printableHtml: String(item.printableHtml || '') })).filter((item) => item.cartonMark && item.printableHtml) + return result } export async function startReturnReceipt(returnNo) { @@ -80,7 +91,11 @@ export async function findReturnSkuCandidates(skuCode) { } export async function confirmReturnReceipt(request) { - return normalizeReturnReceiptResult(await overseasRequest('POST', `${BASE}/confirm`, request)) + const value = String(request?.palletCode || '') + const encoded = value.startsWith('BOX:') || value.startsWith('PARCEL:') + ? { ...request, unitType: value.slice(0, value.indexOf(':')), unitCode: value.slice(value.indexOf(':') + 1), palletCode: undefined } + : request + return normalizeReturnReceiptResult(await overseasRequest('POST', `${BASE}/confirm`, encoded)) } export async function createReturnPallet(returnNo, skuCode) { diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue index 63bf4a5..3875c66 100644 --- a/src/pages/outbound/outbound.vue +++ b/src/pages/outbound/outbound.vue @@ -3,12 +3,12 @@ - 出库任务一件代发扫描托盘码可整托出库;扫描排车业务号会转入“排车拣货” + 出库任务整托扫描托盘码,散箱扫描箱唛;扫描排车业务号会转入“排车拣货” 当前任务出库作业详情 - 任务号直接恢复;进入任务后扫描 DP 托盘码,将一次拣取该托全部未拣箱件。 + 任务号直接恢复;退货整托出库扫托盘码,散箱出库扫箱唛。 正在加载出库任务… @@ -29,9 +29,10 @@ {{ item.palletNo || '散件出库' }}{{ item.pickedPackages }}/{{ item.plannedPackages }} 箱 - + {{ lineBusinessCode(position) }}{{ position.inboundCode || '无入仓码' }} · 已拣 {{ position.pickedPackages }} 箱 · 可拣 {{ position.availablePackages }} 箱 + 整托出库请扫描托盘码 {{ item.palletNo }} 当前明细缺少可扫码库存业务码,已禁止拣货;请刷新或联系平台确认任务下发。 @@ -65,7 +66,7 @@ export default { components: { PortalHeader, ScanTargetInput }, - data() { return { user: session.currentUser, tasks: [], discardTasks: [], current: null, discardCurrent: null, discardTask: null, discardActuals: {}, discardPalletCode: '', discardLoading: false, discardBusy: false, selectedItem: null, selectedPosition: null, scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } }, + data() { return { user: session.currentUser, tasks: [], discardTasks: [], current: null, discardCurrent: null, discardTask: null, discardActuals: {}, discardPalletCode: '', discardLoading: false, discardBusy: false, selectedItem: null, selectedPosition: null, selectedScanCode: '', scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } }, computed: { recentTaskNo() { return fulfillment.outbound.currentTaskNo || '' }, packages() { return fulfillment.outbound.packages }, @@ -87,7 +88,7 @@ stopScanner() { if (this._stopScanner) this._stopScanner(); this._stopScanner = null }, updateScanCode(value) { this.scanCode = value; updateFulfillmentDraft('outbound', { scanCode: value }) }, updatePackages(event) { const value = Math.trunc(Number(event.detail.value || 0)); updateFulfillmentDraft('outbound', { packages: Number.isFinite(value) ? Math.max(0, value) : 0 }) }, - async loadTasks() { await this.run('加载出库任务', async () => { const [tasks, discardTasks] = await Promise.all([listOutboundTasks(), listCargoDiscardTasks()]); this.tasks = tasks; this.discardTasks = discardTasks; this.current = null; this.discardCurrent = null; this.discardTask = null }, true) }, + async loadTasks() { await this.run('加载出库任务', async () => { const [tasks, discardTasks] = await Promise.all([listOutboundTasks('PENDING'), listCargoDiscardTasks()]); this.tasks = tasks; this.discardTasks = discardTasks; this.current = null; this.discardCurrent = null; this.discardTask = null }, true) }, async openTask(taskNo) { const code = String(taskNo || '').trim(); if (!code) return; await this.run('恢复出库任务', async () => { const pendingScan = fulfillment.outbound.currentTaskNo === code ? fulfillment.outbound.scanCode : ''; this.current = await getOutboundTask(code); this.selectedItem = null; this.selectedPosition = null; this.scanCode = pendingScan; updateFulfillmentDraft('outbound', { currentTaskNo: this.current.taskNo, scanCode: pendingScan, packages: pendingScan ? fulfillment.outbound.packages : 1 }); if (pendingScan) this.findAndSelectPosition(pendingScan, false) }, true) }, async openScannedTask(businessNo) { await this.run('解析出库作业', async () => { @@ -121,12 +122,12 @@ if (this.findAndSelectPosition(code)) return this.setError(new Error('扫码未匹配当前出库任务的托盘码、箱码、SKU 或入仓码'), '扫码未匹配') }, - findAndSelectPosition(code, feedback = true) { for (const item of this.current?.items || []) { const position = item.positions.find((value) => [value.palletNo, value.cartonMark, value.skuCode, value.inboundCode].filter(Boolean).includes(code)); if (position) { this.selectPosition(item, position); if (feedback) playSuccessSound(); return true } } return false }, - selectPosition(item, position) { if (!position || position.availablePackages <= 0) return; this.selectedItem = item; this.selectedPosition = position; updateFulfillmentDraft('outbound', { scanCode: this.lineBusinessCode(position), packages: Math.max(1, Math.min(item.plannedPackages - item.pickedPackages, position.availablePackages)) }) }, + findAndSelectPosition(code, feedback = true) { for (const item of this.current?.items || []) { if (this.current?.outboundType === 'RETURN_OUTBOUND' && item.palletNo) continue; const position = item.positions.find((value) => value.availablePackages > 0 && [value.palletNo, value.cartonMark, value.skuCode, value.inboundCode].filter(Boolean).includes(code)); if (position) { this.selectPosition(item, position, code); if (feedback) playSuccessSound(); return true } } return false }, + selectPosition(item, position, scannedCode = '') { if (!position || position.availablePackages <= 0 || (this.current?.outboundType === 'RETURN_OUTBOUND' && item.palletNo)) return; this.selectedItem = item; this.selectedPosition = position; this.selectedScanCode = scannedCode || this.lineBusinessCode(position); updateFulfillmentDraft('outbound', { scanCode: this.selectedScanCode, packages: Math.max(1, Math.min(item.plannedPackages - item.pickedPackages, position.availablePackages)) }) }, async submitPick() { if (!this.canPick) return - const payload = outboundPickPayload(this.selectedItem, this.selectedPosition, this.packages); const key = fulfillmentIdempotencyKey('outbound', 'pick', { taskNo: this.current.taskNo, ...payload }) - await this.run('提交出库拣货', async () => { await pickOutboundTask(this.current.taskNo, { ...payload, idempotencyKey: key.value }); this.current = await getOutboundTask(this.current.taskNo); releaseFulfillmentKey('outbound', key.signature); this.selectedItem = null; this.selectedPosition = null; updateFulfillmentDraft('outbound', { scanCode: '', packages: 1 }); this.successMessage = '本次出库拣货已记录'; playSuccessSound() }) + const payload = outboundPickPayload(this.selectedItem, this.selectedPosition, this.packages, this.selectedScanCode); const key = fulfillmentIdempotencyKey('outbound', 'pick', { taskNo: this.current.taskNo, ...payload }) + await this.run('提交出库拣货', async () => { await pickOutboundTask(this.current.taskNo, { ...payload, idempotencyKey: key.value }); this.current = await getOutboundTask(this.current.taskNo); releaseFulfillmentKey('outbound', key.signature); this.selectedItem = null; this.selectedPosition = null; this.selectedScanCode = ''; updateFulfillmentDraft('outbound', { scanCode: '', packages: 1 }); this.successMessage = '本次出库拣货已记录'; playSuccessSound() }) }, async submitPalletPick(palletNo) { const code = String(palletNo || '').trim(); if (!code || !this.current) return @@ -144,7 +145,7 @@ lineBusinessCode(position) { return position?.cartonMark || position?.palletNo || position?.skuCode || position?.inboundCode || '业务码缺失' }, businessLineKey(position) { return [position?.cartonMark, position?.palletNo, position?.skuCode, position?.inboundCode].filter(Boolean).join(':') || `line-${position?.inventoryPositionId || 'missing'}` }, statusLabel(status) { return ({ PENDING: '待拣货', IN_PROGRESS: '拣货中', COMPLETED: '已完成', CANCELLED: '已取消' })[status] || status || '状态未知' }, - taskType() { return '普通/一件代发共用出库' } + taskType(task) { return ({ RETURN_OUTBOUND: '退货出库', RELABEL_OUTBOUND: '换标出库', ONE_CLICK_DROPSHIP: '一件代发出库', WAREHOUSE_TRANSFER_OUTBOUND: '仓储转运出库' })[task?.outboundType] || '普通出库' } } } diff --git a/src/pages/return-packing/return-packing.vue b/src/pages/return-packing/return-packing.vue index 835c67b..c237bff 100644 --- a/src/pages/return-packing/return-packing.vue +++ b/src/pages/return-packing/return-packing.vue @@ -2,7 +2,12 @@ - 添加退货单 + + + 没有匹配的已完成退货单或可用库存 @@ -13,7 +18,7 @@ - 退货合箱成功{{ result.label.cartonMark || result.cartonMark }}来源退货单:{{ result.label.sourceReturnNo || '—' }}箱规:{{ sizeText(result.label) }}{{ item.skuCode }}{{ item.quantity }} 件{{ item.productNameZh || '商品名称未填写' }} + 退货合箱成功,共 {{ (result.labels || [result.label]).length }} 箱{{ label.cartonMark }}来源退货单:{{ label.sourceReturnNo || '—' }}箱规:{{ sizeText(label) }}{{ item.skuCode }}{{ item.quantity }} 件 {{ error.message }}错误码:{{ error.code }}requestId:{{ error.requestId }}traceId:{{ error.traceId }} @@ -22,7 +27,7 @@