diff --git a/src/api/fulfillment.js b/src/api/fulfillment.js
index e558403..eb2eeba 100644
--- a/src/api/fulfillment.js
+++ b/src/api/fulfillment.js
@@ -43,6 +43,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 @@
+
+
+
+ 箱与箱合并
+ {{ plan.planNo }}
+
+
+ {{ mode === 'NEW' ? '从下方来源箱选择 SKU 和扣除数量,完成后生成新箱唛。' : '扫描来源箱,再扫描另一只箱作为目标;目标箱原有库存不扣除。' }}
+
+
+
+
+
+ {{ box.cartonMark }}
+ {{ item.skuCode }} · {{ item.quantity }} 件
+ 来源箱已确认
+
+
+
+
+
+ {{ mode === 'NEW' ? '新增目标箱' : '已有目标箱' }}
+
+
+
+ {{ target.cartonMark }}{{ item.skuCode }} · 原有 {{ item.quantity }} 件(不扣除)
+
+ 体积:{{ volume }} m³
+
+ 计划箱子明细
+
+ {{ box.cartonMark }}
+ {{ item.skuCode }} · {{ item.quantity }} 件
+ {{ target?.cartonMark === box.cartonMark ? '目标箱 · 保留原有库存' : sources.some(source => source.cartonMark === box.cartonMark) ? '来源箱已确认 · 下方填写扣除数量' : '待扫描确认' }}
+
+
+
+ {{ box.cartonMark }}
+ {{ item.skuCode }} · 可扣 {{ item.quantity }} 件
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue
index ba9b125..033ed8a 100644
--- a/src/pages/dispatch/dispatch.vue
+++ b/src/pages/dispatch/dispatch.vue
@@ -10,7 +10,7 @@
当前任务排车拣货详情
-
+
网络失败不会清空草稿,再次提交会沿用同一幂等键。
@@ -47,9 +47,10 @@
-
+
{{ item.palletNo || '未关联托盘码' }}{{ item.pickedPackages }}/{{ item.scheduledPackages }} 箱
{{ routeLabel(item) }}
+ {{ carton.cartonMark }} · {{ carton.status === 'OUTBOUNDED' ? '已拣货' : '待扫描' }}
@@ -57,7 +58,8 @@
本次拣货:{{ selectedItem.palletNo }}
- 拣货箱数最多 {{ selectedRemaining }} 箱
+ {{ selectedReturnCarton ? `已扫描 ${selectedReturnCarton.cartonMark}` : '请扫描此托盘上的来源箱唛,每次拣货一箱' }}
+ 拣货箱数最多 {{ selectedRemaining }} 箱
@@ -105,16 +107,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.current?.canPick === true && this.selectedPosition && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining && !this.pickingCompleted },
+ canPick() { return this.current?.canPick === true && (this.selectedItem?.returnCartons?.length ? Boolean(this.selectedReturnCarton) : Boolean(this.selectedPosition) && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining) && !this.pickingCompleted },
canCompleteStage() { return Boolean(this.current) && !this.pickingCompleted },
canComplete() { return this.current?.canPick === true && this.plannedPackages > 0 && this.remainingPackages === 0 },
canDepart() { return this.pickingCompleted && this.allPicked && this.current?.canDepart === true && !this.departed }
@@ -144,26 +147,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 在多个客户有可用退货库存,请先选择客户
+ 没有匹配的已完成退货单或可用库存
-
- 已选退货单不同客户的退货单不能合箱;选择时会自动校验。
- 新箱 SKU 数量{{ item.skuCode }}{{ item.productNameZh || '商品名称未填写' }}可用 {{ item.availableQuantity }}装入数量
- 新箱箱规
+
+ {{ activePlan ? `合箱计划 ${activePlan.planNo}` : '客户散件库存' }}{{ activePlan ? '数量已由计划锁定,请分配到目标箱。' : '可用数量来自该客户同仓全部散件库存,后台自动追溯来源退货单。' }}
+ 新箱 SKU 数量{{ item.skuCode }}{{ item.productNameZh || '商品名称未填写' }}可用 {{ item.availableQuantity }}装入数量
+
+
+ 目标箱 {{ index + 1 }}
+ {{ item.skuCode }}
+
+ 体积 {{ boxVolume(box) }} m³
+
+
- 退货合箱成功{{ result.label.cartonMark || result.cartonMark }}来源退货单:{{ result.label.sourceReturnNo || '—' }}箱规:{{ sizeText(result.label) }}{{ item.skuCode }}{{ item.quantity }} 件{{ item.productNameZh || '商品名称未填写' }}
+ 合箱完成 · {{ result.labels.length }} 箱
+
+ {{ label.cartonMark }}来源退货单:{{ label.sourceReturnNo }}
+ {{ item.skuCode }} · {{ item.quantity }} 件
+
+
+
+
{{ error.message }}错误码:{{ error.code }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}
@@ -22,40 +40,103 @@