From c20d5c53e1076318ab877ddc01470fd0be9f2582 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 8 Oct 2026 15:34:47 +0800 Subject: [PATCH 1/2] =?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/api/return-packing.js | 59 +++++++- src/components/ReturnBoxMergeWork.vue | 145 ++++++++++++++++++++ src/pages/dispatch/dispatch.vue | 33 +++-- src/pages/return-packing/return-packing.vue | 121 +++++++++++++--- src/stores/fulfillment.js | 2 +- src/stores/warehouse-work.js | 2 +- src/utils/return-packing-allocation.js | 26 ++++ tests/project-contract.test.mjs | 4 +- tests/return-box-merge-work.test.mjs | 70 ++++++++++ tests/return-packing-allocation.test.mjs | 24 ++++ tests/return-packing-api.test.mjs | 5 + tests/return-packing-completion.test.mjs | 62 +++++++++ 13 files changed, 515 insertions(+), 39 deletions(-) create mode 100644 src/components/ReturnBoxMergeWork.vue create mode 100644 src/utils/return-packing-allocation.js create mode 100644 tests/return-box-merge-work.test.mjs create mode 100644 tests/return-packing-allocation.test.mjs create mode 100644 tests/return-packing-completion.test.mjs diff --git a/src/api/fulfillment.js b/src/api/fulfillment.js index 41971bf..6f04379 100644 --- a/src/api/fulfillment.js +++ b/src/api/fulfillment.js @@ -37,6 +37,7 @@ export function normalizeDispatchTask(value) { warehouseRouteLabel: String(item?.warehouseRouteLabel || ''), scheduledPackages: count(item?.scheduledPackages), pickedPackages: count(item?.pickedPackages), + returnCartons: asList(item?.returnCartons).map((carton) => ({ ...carton, id: String(carton.id || ''), cartonMark: String(carton.cartonMark || ''), productQuantity: count(carton.productQuantity) })), positions: asList(item?.positions).map((position) => ({ ...position, id: String(position?.id || ''), diff --git a/src/api/return-packing.js b/src/api/return-packing.js index 9b43e69..c5f42b3 100644 --- a/src/api/return-packing.js +++ b/src/api/return-packing.js @@ -17,7 +17,17 @@ function normalizeItems(items) { } export function normalizePackingPreview(value = {}) { - return { + value = value && typeof value === 'object' ? value : {} + const normalizeCustomer = (customer = {}) => ({ + customerId: customer.customerId == null ? '' : String(customer.customerId), + orders: Array.isArray(customer.orders) ? customer.orders.map((item) => ({ + returnNo: String(item.returnNo || ''), customerReferenceNo: String(item.customerReferenceNo || ''), status: String(item.status || '') + })) : [], + skus: Array.isArray(customer.skus) ? customer.skus.map((item) => ({ + skuCode: String(item.skuCode || ''), productNameZh: String(item.productNameZh || ''), availableQuantity: count(item.availableQuantity) + })) : [] + }) + const result = { customerKey: value.customerId == null ? '' : String(value.customerId), orders: Array.isArray(value.orders) ? value.orders.map((item) => ({ returnNo: String(item.returnNo || ''), customerReferenceNo: String(item.customerReferenceNo || ''), status: String(item.status || '') @@ -26,6 +36,13 @@ export function normalizePackingPreview(value = {}) { skuCode: String(item.skuCode || ''), productNameZh: String(item.productNameZh || ''), availableQuantity: count(item.availableQuantity) })) : [] } + if (Array.isArray(value.customers)) result.customers = value.customers.map(normalizeCustomer).filter((item) => item.customerId) + else if (result.customerKey && result.orders.length) { + // Older service versions returned the single-customer shape. Keep the + // PDA behavior consistent by exposing it through the grouped contract. + result.customers = [{ customerId: result.customerKey, orders: result.orders, skus: result.skus }] + } + return result } export function normalizeReturnLabel(value = {}) { @@ -37,16 +54,52 @@ export function normalizeReturnLabel(value = {}) { } } -export async function findPackingOrders(returnNo = '') { +export async function findPackingOrders(returnNo = '', mode = 'RETURN_NO') { const query = String(returnNo || '').trim() try { - return normalizePackingPreview(await overseasRequest('GET', `${BASE}/packing/orders${query ? `?returnNo=${encoded(query)}` : ''}`)) + return normalizePackingPreview(await overseasRequest('GET', `${BASE}/packing/orders${query ? `?${mode === 'RETURN_NO' ? 'returnNo' : 'skuCode'}=${encoded(query)}` : ''}`)) } catch (error) { if (isNotFound(error)) return normalizePackingPreview() throw error } } +export async function listPackingPlans() { + const value = await overseasRequest('GET', `${BASE}/packing/plans`) + return Array.isArray(value) ? value.map(normalizePackingPlan) : [] +} + +export async function completePackingPlan(planNo, request) { + const value = await overseasRequest('POST', `${BASE}/packing/plans/${encoded(planNo)}/complete`, request) + return { planNo: String(value?.planNo || planNo), labels: Array.isArray(value?.labels) ? value.labels.map(normalizeReturnLabel) : [] } +} + +export function normalizePackingPlan(value = {}) { + return { planNo: String(value.planNo || ''), customerId: String(value.customerId || ''), returnNos: Array.isArray(value.returnNos) ? value.returnNos.map(String) : [], sourceCartonMarks: Array.isArray(value.sourceCartonMarks) ? value.sourceCartonMarks.map(String) : [], sources: Array.isArray(value.sources) ? value.sources : [], mode: String(value.mode || ''), status: String(value.status || ''), boxes: Array.isArray(value.boxes) ? value.boxes : [], remark: String(value.remark || '') } +} + +export async function getPackingPlan(planNo) { + return normalizePackingPlan(await overseasRequest('GET', `${BASE}/packing/plans/${encoded(planNo)}`)) +} + +export async function scanReturnCarton(mark) { + return overseasRequest('GET', `${BASE}/cartons/${encoded(mark)}`) +} + +export async function mergeReturnBoxes(request) { + const value = await overseasRequest('POST', `${BASE}/cartons/merge-boxes`, request) + return { labels: [normalizeReturnLabel(value?.label)] } +} + +export async function packReturnSkus(request) { + const value = await overseasRequest('POST', `${BASE}/cartons/pack`, request) + return { labels: Array.isArray(value?.labels) ? value.labels.map(normalizeReturnLabel) : [] } +} + +export async function putawayReturnCarton(mark, request) { + return overseasRequest('POST', `${BASE}/cartons/${encoded(mark)}/putaway`, request) +} + export async function mergeReturnCartons(request) { const value = await overseasRequest('POST', `${BASE}/cartons/merge`, request) return { cartonMark: String(value?.cartonMark || ''), skuCode: String(value?.skuCode || ''), totalQuantity: count(value?.totalQuantity), label: normalizeReturnLabel(value?.label) } diff --git a/src/components/ReturnBoxMergeWork.vue b/src/components/ReturnBoxMergeWork.vue new file mode 100644 index 0000000..9afb349 --- /dev/null +++ b/src/components/ReturnBoxMergeWork.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue index e57881b..9459b59 100644 --- a/src/pages/dispatch/dispatch.vue +++ b/src/pages/dispatch/dispatch.vue @@ -10,7 +10,7 @@ 当前任务排车拣货详情 - + 网络失败不会清空草稿,再次提交会沿用同一幂等键。 @@ -45,9 +45,10 @@ - + {{ item.palletNo || '未关联托盘码' }}{{ item.pickedPackages }}/{{ item.scheduledPackages }} 箱 {{ routeLabel(item) }} + {{ carton.cartonMark }} · {{ carton.status === 'OUTBOUNDED' ? '已拣货' : '待扫描' }} @@ -55,7 +56,8 @@ 本次拣货:{{ selectedItem.palletNo }} - 拣货箱数最多 {{ selectedRemaining }} 箱 + {{ selectedReturnCarton ? `已扫描 ${selectedReturnCarton.cartonMark}` : '请扫描此托盘上的来源箱唛,每次拣货一箱' }} + 拣货箱数最多 {{ selectedRemaining }} 箱 @@ -100,16 +102,17 @@ computed: { recentTaskNo() { return fulfillment.dispatch.currentNo || '' }, packages() { return fulfillment.dispatch.packages }, - selectedItem() { return this.current?.items?.find((item) => item.palletNo === fulfillment.dispatch.palletCode) || null }, + selectedItem() { return this.current?.items?.find((item) => fulfillment.dispatch.dispatchItemId ? item.dispatchItemId === fulfillment.dispatch.dispatchItemId : item.palletNo === fulfillment.dispatch.palletCode) || null }, + selectedReturnCarton() { return this.selectedItem?.returnCartons?.find((carton) => carton.cartonMark === fulfillment.dispatch.cartonCode && carton.status === 'OUTBOUND_PENDING') || null }, selectedPosition() { return this.selectedItem?.positions?.find((item) => item.availablePackages > 0) || this.selectedItem?.positions?.[0] || null }, - selectedRemaining() { return Math.max(0, Math.min(this.selectedItem?.scheduledPackages - this.selectedItem?.pickedPackages || 0, this.selectedPosition?.availablePackages || 0)) }, + selectedRemaining() { const remaining = this.selectedItem?.scheduledPackages - this.selectedItem?.pickedPackages || 0; return Math.max(0, this.selectedItem?.returnCartons?.length ? remaining : Math.min(remaining, this.selectedPosition?.availablePackages || 0)) }, plannedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.scheduledPackages, 0) }, pickedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.pickedPackages, 0) }, remainingPackages() { return Math.max(0, this.plannedPackages - this.pickedPackages) }, allPicked() { return this.plannedPackages > 0 && this.remainingPackages === 0 }, pickingCompleted() { return finishedStatuses.includes(this.current?.status) }, departed() { return this.current?.status === 'DEPARTED' }, - canPick() { return this.selectedPosition && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining && !this.pickingCompleted }, + canPick() { return (this.selectedItem?.returnCartons?.length ? Boolean(this.selectedReturnCarton) : Boolean(this.selectedPosition) && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining) && this.current?.canPick === true && !this.pickingCompleted }, canCompleteStage() { return Boolean(this.current) && !this.pickingCompleted }, canComplete() { return this.plannedPackages > 0 && this.remainingPackages === 0 }, canDepart() { return this.pickingCompleted && this.allPicked && this.current?.canDepart === true && !this.departed } @@ -139,26 +142,32 @@ async loadTasks() { await this.run('加载排车任务', async () => { this.tasks = await listDispatchTasks(); this.current = null }, true) }, async openTask(dispatchNo) { const code = String(dispatchNo || '').trim(); if (!code) return - await this.run('恢复排车任务', async () => { const previousNo = fulfillment.dispatch.currentNo; this.current = await getDispatchTask(code); updateFulfillmentDraft('dispatch', { currentNo: this.current.dispatchNo, palletCode: previousNo === this.current.dispatchNo ? fulfillment.dispatch.palletCode : '' }); this.scanCode = '' }, true) + await this.run('恢复排车任务', async () => { const previousNo = fulfillment.dispatch.currentNo; this.current = await getDispatchTask(code); const same = previousNo === this.current.dispatchNo; updateFulfillmentDraft('dispatch', { currentNo: this.current.dispatchNo, palletCode: same ? fulfillment.dispatch.palletCode : '', dispatchItemId: same ? fulfillment.dispatch.dispatchItemId : '', cartonCode: same ? fulfillment.dispatch.cartonCode : '' }); this.scanCode = '' }, true) }, - closeTask() { this.current = null; this.scanCode = ''; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { currentNo: '', palletCode: '', packages: 1 }); this.loadTasks() }, + closeTask() { this.current = null; this.scanCode = ''; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { currentNo: '', palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 }); this.loadTasks() }, handleScan(raw) { const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim(); if (!code || this.busy || this.loading) return console.info('[Overseas PDA dispatch] scan', { code, task: this.current?.dispatchNo || '' }) this.scanCode = code if (!this.current || code === this.current.dispatchNo || code === this.current.taskNo) return this.openTask(code === this.current?.taskNo ? this.current.dispatchNo : code) + const cartonItem = this.current.items.find((item) => item.returnCartons?.some((carton) => carton.cartonMark === code)) + if (cartonItem) { + if (cartonItem.returnCartons.find((carton) => carton.cartonMark === code)?.status !== 'OUTBOUND_PENDING') return this.setError(new Error('该箱已拣货或不可出库'), '扫码未匹配') + this.selectPallet(cartonItem); updateFulfillmentDraft('dispatch', { cartonCode: code, packages: 1 }); this.scanCode = ''; playSuccessSound(); return + } const item = this.current.items.find((value) => [value.palletNo, value.fbaWarehouseCode].includes(code)) if (!item) return this.setError(new Error('扫码未匹配当前排车任务的托盘或稳定业务码'), '扫码未匹配') this.selectPallet(item); this.scanCode = ''; playSuccessSound() }, - selectPallet(item) { if (!item?.palletNo || item.scheduledPackages <= item.pickedPackages) return; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, packages: Math.max(1, Math.min(item.scheduledPackages - item.pickedPackages, item.positions?.[0]?.availablePackages || 1)) }) }, + selectPallet(item) { if (!item?.palletNo || item.scheduledPackages <= item.pickedPackages) return; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, dispatchItemId: item.dispatchItemId, cartonCode: '', packages: Math.max(1, Math.min(item.scheduledPackages - item.pickedPackages, item.positions?.[0]?.availablePackages || 1)) }) }, async submitPick() { if (!this.canPick) return - const payload = { dispatchNo: this.current.dispatchNo, palletNo: this.selectedItem.palletNo, positionId: this.selectedPosition.id, packages: Number(this.packages), productQuantity: 0 } + const pick = this.selectedReturnCarton ? { positionId: null, cartonMark: this.selectedReturnCarton.cartonMark, packages: 1, productQuantity: this.selectedReturnCarton.productQuantity } : { positionId: this.selectedPosition.id, packages: Number(this.packages), productQuantity: 0 } + const payload = { dispatchNo: this.current.dispatchNo, dispatchItemId: this.selectedItem.dispatchItemId, ...pick } const key = fulfillmentIdempotencyKey('dispatch', 'pick', payload) await this.run('提交排车拣货', async () => { - await pickDispatchItem(this.current.dispatchNo, this.selectedItem.dispatchItemId, { positionId: this.selectedPosition.id, packages: Number(this.packages), productQuantity: 0, idempotencyKey: key.value }) - this.current = await getDispatchTask(this.current.dispatchNo); releaseFulfillmentKey('dispatch', key.signature); updateFulfillmentDraft('dispatch', { palletCode: '', packages: 1 }); this.successMessage = '本次排车拣货已记录'; playSuccessSound() + await pickDispatchItem(this.current.dispatchNo, this.selectedItem.dispatchItemId, { ...pick, idempotencyKey: key.value }) + this.current = await getDispatchTask(this.current.dispatchNo); releaseFulfillmentKey('dispatch', key.signature); updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 }); this.successMessage = '本次排车拣货已记录'; playSuccessSound() }) }, async completePicking() { diff --git a/src/pages/return-packing/return-packing.vue b/src/pages/return-packing/return-packing.vue index 835c67b..9c03496 100644 --- a/src/pages/return-packing/return-packing.vue +++ b/src/pages/return-packing/return-packing.vue @@ -2,18 +2,36 @@ - 添加退货单 + + + 扫描 SKU 或箱号 + 待作业合箱计划审核通过后推送 - 没有匹配的已完成退货单或可用库存 + 选择客户该 SKU 在多个客户有可用退货库存,请先选择客户 + 没有匹配的已完成退货单或可用库存 -