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 @@
- 添加退货单
+
+ 待执行合箱计划
+ 无计划时自主合箱
+
+
+ {{ activePlan.planNo }}{{ activePlan.mode === 'CUSTOMER_INSTRUCTION' ? '客户合箱指令' : '仓库自主合箱' }} · 来源订单 {{ activePlan.returnNos.length }} 个计划未配置箱明细时,系统已按单 SKU 一箱生成默认方案。可以合并为混箱,也可以拆成多个单箱。第 {{ index + 1 }} 箱
没有匹配的已完成退货单或可用库存
@@ -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 @@