diff --git a/src/modules/overseas-warehouse/api/returnOrders.ts b/src/modules/overseas-warehouse/api/returnOrders.ts
index e6cda49..31b4ea2 100644
--- a/src/modules/overseas-warehouse/api/returnOrders.ts
+++ b/src/modules/overseas-warehouse/api/returnOrders.ts
@@ -16,7 +16,7 @@ export interface ReturnReceipt {
labels?: Array<{ cartonMark: string; printableHtml: string }>
labelRepairRequired?: boolean
}
-export interface ReturnPallet { palletCode: string; unitType: 'PALLET' | 'BOX'; unitCode: string; skuTypes: number; totalQuantity: number; totalBoxCount: number; items: Array<{ skuCode: string; productNameZh?: string; quantity: number; boxCount: number }> }
+export interface ReturnPallet { palletCode: string; unitType: 'PALLET' | 'BOX'; unitCode: string; skuTypes: number; totalQuantity: number; totalBoxCount: number; length?: number; width?: number; height?: number; weight?: number; volumeCbm?: number; items: Array<{ skuCode: string; productNameZh?: string; quantity: number; boxCount: number }> }
export interface ReturnPhoto { fileId: number; fileName: string; contentType: string; sizeBytes: number }
export interface ReturnPackingDetail { sourceCartonMark: string; targetCartonMark: string; returnNo: string; skuCode: string; quantity: number; createdAt: string }
export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[]; packingDetails?: ReturnPackingDetail[] }
diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts
index c030e92..16c3bb7 100644
--- a/src/modules/overseas-warehouse/api/warehouseServices.ts
+++ b/src/modules/overseas-warehouse/api/warehouseServices.ts
@@ -33,7 +33,7 @@ export interface RelabelInventoryCandidate {
destinationFbaWarehouseCode?: string; destinationFbaWarehouseName?: string; skuCode?: string; productNameZh?: string
availablePackages: number | string; version: number
positions: Array<{ positionId: string; palletNo?: string; batchNo?: string; warehouseLocationCode?: string; availablePackages: number }>
- sourceType?: 'INBOUND' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string
+ sourceType?: 'INBOUND' | 'RETURN_CARTON' | 'RETURN_PARCEL'; returnCartonId?: string; cartonMark?: string
skuItems?: Array<{ skuCode: string; newSkuCode?: string; quantity: number; remark?: string }>; relabelledPackages?: number
}
diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs
index 651ddc1..90e1872 100644
--- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs
+++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs
@@ -5,6 +5,45 @@ import test from 'node:test'
const view = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'utf8')
+function submitHarness(failure) {
+ const source = view.slice(view.indexOf('async function submit()'), view.indexOf('\nfunction statusType('))
+ const state = { requests: [], loads: 0, routes: [], errors: [], successes: [] }
+ const form = { overseasWarehouseId: 'WH-1', warehouseContactName: 'Contact', warehouseContactPhone: '123', customerReferenceNo: 'REF-NEW', remark: 'edited', needRelabel: true, items: [{ skuCode: 'SKU-NEW', productNameZh: 'Product', expectedQuantity: 25 }] }
+ const editingReturnNo = { value: 'RT-TEST' }, showCreate = { value: true }, creating = { value: false }
+ const submit = new Function('form', 'duplicateSkuIds', 'creating', 'editingReturnNo', 'showCreate', 'props', 'ElMessage', 'updateReturnOrder', 'createReturnOrder', 'createIdempotencyKey', 'resetForm', 'router', 'load', `${source}; return submit`)(
+ form, { value: new Set() }, creating, editingReturnNo, showCreate, { createPage: false, detailPage: false },
+ { warning() {}, success(message) { state.successes.push(message) }, error(message) { state.errors.push(message) } },
+ async (returnNo, payload) => { state.requests.push({ returnNo, payload }); if (failure) throw failure },
+ async () => assert.fail('Editing must use update'), () => 'edit-test', () => {},
+ { async push(route) { state.routes.push(route) } }, async () => { state.loads++ },
+ )
+ return { submit, state, editingReturnNo, showCreate, creating }
+}
+
+test('编辑退货保存提交修改后的字段,并重新读取当前列表', async () => {
+ const { submit, state, editingReturnNo, showCreate, creating } = submitHarness()
+ await submit()
+ assert.equal(state.requests[0].returnNo, 'RT-TEST')
+ assert.equal(state.requests[0].payload.customerReferenceNo, 'REF-NEW')
+ assert.equal(state.requests[0].payload.items[0].expectedQuantity, 25)
+ assert.equal(state.loads, 1)
+ assert.deepEqual(state.routes, [])
+ assert.equal(showCreate.value, false)
+ assert.equal(editingReturnNo.value, '')
+ assert.equal(creating.value, false)
+})
+
+test('编辑退货保存失败显示接口错误并保留编辑内容供重试', async () => {
+ const { submit, state, editingReturnNo, showCreate, creating } = submitHarness(new Error('只有待审核退货预报可以修改'))
+ await submit()
+ assert.deepEqual(state.errors, ['只有待审核退货预报可以修改'])
+ assert.deepEqual(state.successes, [])
+ assert.equal(state.loads, 0)
+ assert.equal(showCreate.value, true)
+ assert.equal(editingReturnNo.value, 'RT-TEST')
+ assert.equal(creating.value, false)
+})
+
test('退货签收照片通过客户鉴权下载接口加载并释放对象 URL', () => {
assert.match(api, /\/return-orders\/\$\{encodeURIComponent\(returnNo\)\}\/photos\/files\/\$\{encodeURIComponent\(String\(fileId\)\)\}\/download/)
assert.match(api, /apiBlobRequest/)
@@ -81,7 +120,7 @@ const labelHtml = mark => `
${labelStyle}
function printHarness() {
const source = view.slice(view.indexOf('async function printReturnLabel('), view.indexOf('\nfunction openDetail('))
- .replace('receipt: ReturnReceipt', 'receipt')
+ .replace('receipt: ReturnReceipt | ReturnReceipt[]', 'receipt')
const calls = { opened: 0, printed: 0, html: '', warnings: [], errors: [], fetched: [] }
const popup = {
document: { open() {}, write(html) { calls.html = html }, close() {} },
@@ -119,7 +158,7 @@ test('退货多箱打印保留统一模板尺寸和每箱条码,并按箱分
assert.equal(calls.printed, 1)
assert.deepEqual(calls.fetched, [])
assert.deepEqual(calls.errors, [])
- assert.match(view, /打印全部箱唛(\{\{ receipt\.labels\?\.length \|\| receipt\.boxCount \|\| 1 \}\} 箱)/)
+ assert.match(view, /打印全部箱唛(\{\{ printableBoxCount \}\} 箱)/)
})
test('缺少内嵌模板时读取鉴权箱唛模板,待修复箱记录禁止打印', async () => {
@@ -136,6 +175,22 @@ test('缺少内嵌模板时读取鉴权箱唛模板,待修复箱记录禁止
assert.deepEqual(calls.errors, [])
})
+test('两托各两箱合并为一次四页打印,每箱条码保留且模板样式不重复', async () => {
+ const { print, calls } = printHarness()
+ const receipts = [1, 2].map(pallet => ({
+ cartonMark: `PALLET-${pallet}`, boxCount: 2,
+ labels: [1, 2].map(box => ({ cartonMark: `SKU-${pallet}#2-${box}`, printableHtml: labelHtml(`SKU-${pallet}#2-${box}`) })),
+ printableHtml: `batch-${pallet}`,
+ }))
+ await print(receipts)
+ assert.equal(calls.opened, 1)
+ assert.equal(calls.printed, 1)
+ assert.equal((calls.html.match(//g) || []).length, 4)
+ assert.equal(calls.html.split(labelStyle).length - 1, 1)
+ for (const receipt of receipts) for (const label of receipt.labels) assert.ok(calls.html.includes(`data-barcode="${label.cartonMark}"`))
+ assert.deepEqual(calls.errors, [])
+})
+
test('待审核退货可编辑,完成入库后从独立新增页发起贴标', () => {
assert.match(view, /row\.status === 'PENDING_REVIEW'.*key: 'edit'/)
assert.match(view, /if \(editingReturnNo\.value\) await updateReturnOrder\(editingReturnNo\.value, payload\)/)
diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue
index 0d7535a..87de609 100644
--- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue
+++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue
@@ -24,6 +24,8 @@ const creating = ref(false)
const showCreate = ref(false)
const editingReturnNo = ref('')
const selected = ref()
+const printableReceipts = computed(() => selected.value?.receipts.filter(item => item.unitType !== 'PARCEL' && (item.cartonMark || item.labels?.length)) || [])
+const printableBoxCount = computed(() => printableReceipts.value.reduce((total, receipt) => total + (receipt.labels?.length || receipt.boxCount || 1), 0))
const photoUrls = reactive>({})
const rows = ref([])
const total = ref(0)
@@ -175,18 +177,21 @@ function previewPhoto(fileId: string | number) {
const url = photoUrls[String(fileId)]
if (url) window.open(url, '_blank', 'noopener')
}
-async function printReturnLabel(receipt: ReturnReceipt) {
- if (receipt.labelRepairRequired) { ElMessage.warning('签收箱数与当前箱记录不一致,请联系仓库核实并修复后打印'); return }
- if (!receipt.cartonMark) return
+async function printReturnLabel(receipt: ReturnReceipt | ReturnReceipt[]) {
+ const receipts = Array.isArray(receipt) ? receipt : [receipt]
+ if (receipts.some(item => item.labelRepairRequired)) { ElMessage.warning('签收箱数与当前箱记录不一致,请联系仓库核实并修复后打印'); return }
+ if (!receipts.length) return
const popup = window.open('', '_blank')
if (!popup) { ElMessage.warning('请允许浏览器打开打印窗口'); return }
popup.opener = null
try {
- const labels = receipt.labels?.length
- ? receipt.labels
- : [{ cartonMark: receipt.cartonMark, printableHtml: receipt.printableHtml || (await getReturnInventoryLabel(receipt.cartonMark)).printableHtml }]
+ const groups = await Promise.all(receipts.map(async item => item.labels?.length
+ ? await Promise.all(item.labels.map(async label => ({ ...label, printableHtml: label.printableHtml || (await getReturnInventoryLabel(label.cartonMark)).printableHtml })))
+ : item.cartonMark ? [{ cartonMark: item.cartonMark, printableHtml: item.printableHtml || (await getReturnInventoryLabel(item.cartonMark)).printableHtml }] : []))
+ const labels = [...new Map(groups.flat().map(label => [label.cartonMark, label])).values()]
+ if (!labels.length) { popup.close(); ElMessage.warning('暂无可打印的箱唛'); return }
const printStyles = [...new Set(labels.flatMap(label => label.printableHtml.match(/${labels.map((label) => {
+ const printableHtml = (receipts.length === 1 ? receipts[0]?.printableHtml : undefined) || `退货箱唛${printStyles}${labels.map((label) => {
const html = label.printableHtml || ''
const bodyStart = html.indexOf('', bodyStart) + 1
@@ -221,7 +226,12 @@ async function submit() {
const payload = { overseasWarehouseId: form.overseasWarehouseId, warehouseContactName: form.warehouseContactName.trim(), warehouseContactPhone: form.warehouseContactPhone.trim(), customerReferenceNo: form.customerReferenceNo || undefined, remark: form.remark || undefined, needRelabel: form.needRelabel, idempotencyKey: createIdempotencyKey(), items: items.map(item => ({ ...item, productNameZh: item.productNameZh || undefined })) }
if (editingReturnNo.value) await updateReturnOrder(editingReturnNo.value, payload)
else await createReturnOrder(payload)
- ElMessage.success(editingReturnNo.value ? '退货预报已更新' : '退货预报已创建'); showCreate.value = false; editingReturnNo.value = ''; resetForm(); await router.push({ name: 'overseas-return-relabel' })
+ const wasEditing = Boolean(editingReturnNo.value)
+ ElMessage.success(wasEditing ? '退货预报已更新' : '退货预报已创建'); showCreate.value = false; editingReturnNo.value = ''; resetForm()
+ if (props.createPage || props.detailPage) await router.push({ name: 'overseas-return-relabel' })
+ else await load()
+ } catch (error) {
+ ElMessage.error(error instanceof Error ? error.message : '保存退货预报失败')
} finally { creating.value = false }
}
function statusType(status: string, needRelabel = false) {
@@ -368,10 +378,11 @@ onBeforeUnmount(clearPhotoUrls)
- 实物明细({{ selected.pallets?.length || 0 }} 个托盘/箱)打印全部箱唛({{ receipt.labels?.length || receipt.boxCount || 1 }} 箱)
+ 实物明细({{ selected.pallets?.length || 0 }} 个托盘/箱)打印全部箱唛({{ printableBoxCount }} 箱)
- {{ pallet.unitType === 'BOX' ? '箱子' : '托盘' }} {{ pallet.unitCode || pallet.palletCode }}{{ pallet.skuTypes }} 种 SKU · {{ pallet.totalQuantity }} 件 · {{ pallet.totalBoxCount }} 箱
+ {{ pallet.unitType === 'BOX' ? '箱子' : '托盘' }} {{ pallet.unitCode || pallet.palletCode }}{{ pallet.skuTypes }} 种 SKU · {{ pallet.totalQuantity }} 件 · {{ pallet.totalBoxCount }} 箱 · 体积 {{ pallet.volumeCbm == null ? '—' : `${pallet.volumeCbm} m³` }}
+ 长 {{ pallet.length ?? '—' }} cm · 宽 {{ pallet.width ?? '—' }} cm · 高 {{ pallet.height ?? '—' }} cm · 重量 {{ pallet.weight ?? '—' }} kg
{{ row.productNameZh || '—' }}
@@ -462,6 +473,7 @@ onBeforeUnmount(clearPhotoUrls)
.return-detail-section :deep(.el-table .el-tag) { font-size:12px; }
.return-pallet-list { padding:0 14px 10px; }
.return-pallet-summary { margin-left:12px; color:#7c899c; font-size:12px; font-weight:400; }
+.return-pallet-measurements { margin-bottom:8px; color:#7c899c; font-size:12px; }
.return-photo-grid { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:12px; padding:0 16px 16px; }
.return-photo { min-width:0; overflow:hidden; padding:0; border:1px solid #dfe6ef; border-radius:6px; color:#526176; background:#fff; cursor:pointer; }
.return-photo img,.return-photo>span { display:flex; width:100%; height:120px; align-items:center; justify-content:center; object-fit:cover; background:#f4f7fb; }
diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue
index de8ff08..c7640d5 100644
--- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue
+++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue
@@ -26,16 +26,14 @@ import {
} from '@/modules/first-leg/api/firstLeg'
import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination'
import { createIdempotencyKey } from '@/utils/idempotencyKey'
-import { createReturnRelabel, uploadReturnRelabelFile } from '../api/returnOrders'
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'edit' | 'detail'
type LabelingMode = 'carton' | 'sku' | 'carton_and_sku'
type RelabelTemplateType = 'carton' | 'sku' | 'mixed'
-interface CargoRow { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON'; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
+interface CargoRow { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL'; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string }
-interface ReturnRelabelSkuRow { skuCode: string; productNameZh: string; availableQuantity: number; selected: boolean; quantity: number }
interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string }
@@ -189,16 +187,21 @@ const selectedDiscardOrderId = ref('')
const discardCandidatesLoading = ref(false)
const relabelCandidates = ref([])
const selectedRelabelInboundOrderId = ref('')
+const orderInfoCollapsed = ref(false)
const relabelCandidatesLoading = ref(false)
const transferInventoryCandidates = ref([])
const selectedTransferInventory = ref('')
// Snowflake IDs exceed JavaScript's safe integer range. Keep them as strings
// all the way to JSON so the backend receives the exact source order ID.
const selectedTransferSourceOrderIds = ref([])
-const returnRelabelSkuRows = ref([])
const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:'))
const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '')
-const requiresBothTotalFiles = computed(() => (props.service === 'labeling' || (props.service === 'transfer' && form.relabel)) && !isReturnRelabelSource.value && labelingMode.value === 'carton_and_sku')
+const hideReturnSkuCartonMark = computed(() => isReturnRelabelSource.value && labelingMode.value === 'sku')
+const selectedReturnCandidates = computed(() => relabelCandidates.value.filter(item => Boolean(item.returnCartonId) && item.inboundOrderNo === selectedReturnOrderNo.value))
+const returnBoxCandidates = computed(() => selectedReturnCandidates.value.filter(item => item.sourceType === 'RETURN_CARTON'))
+const returnLooseCandidates = computed(() => selectedReturnCandidates.value.filter(item => item.sourceType === 'RETURN_PARCEL'))
+const returnSkuOnly = computed(() => isReturnRelabelSource.value && returnLooseCandidates.value.length > 0 && returnBoxCandidates.value.length === 0)
+const requiresBothTotalFiles = computed(() => (props.service === 'labeling' || (props.service === 'transfer' && form.relabel)) && labelingMode.value === 'carton_and_sku')
function inventoryQuantity(value: number | string | null | undefined) {
const quantity = Number(value)
return Number.isFinite(quantity) && quantity > 0 ? quantity : 0
@@ -207,7 +210,7 @@ const relabelInboundOptions = computed(() => {
const grouped = new Map()
for (const candidate of relabelCandidates.value) {
const returnOrderNo = candidate.inboundOrderNo || String(candidate.returnCartonId || '')
- const value = candidate.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(candidate.inboundOrderId)
+ const value = candidate.returnCartonId ? `RETURN_ORDER:${returnOrderNo}` : String(candidate.inboundOrderId)
const current = grouped.get(value)
if (current) {
current.availablePackages += inventoryQuantity(candidate.availablePackages)
@@ -215,11 +218,11 @@ const relabelInboundOptions = computed(() => {
}
else grouped.set(value, {
value,
- inboundOrderNo: candidate.sourceType === 'RETURN_CARTON' ? `退货单 ${candidate.inboundOrderNo || '—'}` : (candidate.inboundOrderNo || '—'),
- inboundCode: candidate.sourceType === 'RETURN_CARTON' ? '已签收退货库存' : (candidate.inboundCode || '—'),
+ inboundOrderNo: candidate.returnCartonId ? `退货单 ${candidate.inboundOrderNo || '—'}` : (candidate.inboundOrderNo || '—'),
+ inboundCode: candidate.returnCartonId ? '已签收退货库存' : (candidate.inboundCode || '—'),
availablePackages: inventoryQuantity(candidate.availablePackages),
relabelledPackages: inventoryQuantity(candidate.relabelledPackages),
- quantityUnit: candidate.sourceType === 'RETURN_CARTON' ? '件' : '箱',
+ quantityUnit: candidate.returnCartonId ? '件' : '箱',
})
}
return [...grouped.values()]
@@ -263,7 +266,10 @@ watch(() => cargoRows.value.map((row) => row.sku), (skus) => {
if (sourceSku) row.sku = sourceSku
})
})
-watch(labelingMode, (value) => { if (value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow() })
+watch([returnSkuOnly, labelingMode], ([skuOnly, value]) => {
+ if (skuOnly && value !== 'sku') { labelingMode.value = 'sku'; return }
+ if (value !== 'carton' && !skuRelabelRows.value.length) addSkuRelabelRow()
+})
watch(() => form.destination, (value) => {
if (props.service !== 'labeling') return
for (const row of cargoRows.value) row.destinationWarehouseId = value
@@ -293,6 +299,7 @@ onMounted(() => {
onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos() })
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '', reviewStart: '', reviewEnd: '', completedStart: '', completedEnd: '', measuredStart: '', measuredEnd: '', outboundStart: '', outboundEnd: '' }) }
function resetForm() {
+ orderInfoCollapsed.value = false
Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', privateDestinationAddress: '', privateDestinationPostalCode: '', privateDestinationContact: '', privateDestinationPhone: '', address: '', contact: '', phone: '', pickupContact: '', pickupPhone: '', relabelContact: '', relabelPhone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01 })
cargoRows.value = []
transferRelabelRows.value = []
@@ -490,7 +497,7 @@ function clearTransferRelabelRows() {
function addSkuRelabelRow() {
const source = cargoRows.value[Math.min(skuRelabelRows.value.length, Math.max(0, cargoRows.value.length - 1))]
skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: source?.id || null,
- cartonMark: '', originalSku: source?.sku || '', newSku: '', quantity: 1,
+ cartonMark: hideReturnSkuCartonMark.value ? source?.cartonMark || '' : '', originalSku: source?.sku || '', newSku: '', quantity: 1,
fbaCartonMark: '', destinationWarehouseId: form.destination, remark: '' })
}
function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) }
@@ -532,6 +539,16 @@ function chooseCreateTotalFile(scope: 'CARTON_TOTAL' | 'SKU_TOTAL') {
function buildSkuRelabelItems(): RelabelCreate['items'] {
const groups = new Map()
for (const row of skuRelabelRows.value) {
+ if (hideReturnSkuCartonMark.value) {
+ const source = cargoRows.value.find(item => item.id === row.sourceRowId)
+ const candidate = selectedReturnCandidates.value.find(item => String(item.returnCartonId) === source?.returnCartonId)
+ if (candidate?.skuItems?.some(item => item.skuCode === row.originalSku.trim())) row.cartonMark = source?.cartonMark || ''
+ else {
+ const matches = selectedReturnCandidates.value.filter(item => item.skuItems?.some(sku => sku.skuCode === row.originalSku.trim()))
+ if (matches.length !== 1) throw new Error(`请从上方库存明细选择 SKU ${row.originalSku} 的来源库存`)
+ row.cartonMark = matches[0].cartonMark || ''
+ }
+ }
const cartonMark = row.cartonMark.trim().toUpperCase()
if (!cartonMark) throw new Error('请填写每个箱子的我方箱唛')
const rows = groups.get(cartonMark) || []
@@ -546,7 +563,20 @@ function buildSkuRelabelItems(): RelabelCreate['items'] {
throw new Error('填写的箱唛数量超过当前入仓单可用库存箱数')
}
return [...groups.entries()].map(([_, rows], groupIndex) => {
- const source = cargoRows.value.find((item) => item.id === rows[0].sourceRowId) || anchors[groupIndex]
+ const source = isReturnRelabelSource.value
+ ? cargoRows.value.find(item => item.cartonMark === rows[0].cartonMark.trim())
+ : cargoRows.value.find((item) => item.id === rows[0].sourceRowId) || anchors[groupIndex]
+ if (isReturnRelabelSource.value && !source) throw new Error('请选择该退货单实际可用的来源箱号')
+ if (isReturnRelabelSource.value) {
+ const candidate = selectedReturnCandidates.value.find(item => String(item.returnCartonId) === source?.returnCartonId)
+ if (candidate?.sourceType === 'RETURN_PARCEL' && labelingMode.value !== 'sku') throw new Error('散件库存只能贴 SKU,请先合箱后申请箱唛或混贴')
+ const requested = new Map()
+ for (const row of rows) {
+ const quantity = (requested.get(row.originalSku) || 0) + row.quantity
+ requested.set(row.originalSku, quantity)
+ if (quantity > Number(candidate?.skuItems?.find(item => item.skuCode === row.originalSku)?.quantity || 0)) throw new Error(`SKU ${row.originalSku} 超过来源箱可用数量`)
+ }
+ }
if (!source?.returnCartonId && (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId)) {
throw new Error('来源库存已变化,请重新选择有库存的历史入仓单')
}
@@ -949,48 +979,18 @@ function listRowActions(candidate: object): TableAction[] {
if (canConfirmRelabel.value && props.service === 'labeling' && item.__status === 'CUSTOMER_CONFIRM_PENDING' && !item.__confirmedAt) actions.push({ key: 'confirm-relabel', label: '确认换标', run: () => confirmRelabelFromList(item['0']) })
return actions
}
-async function submitReturnRelabel() {
- const items = returnRelabelSkuRows.value.filter(item => item.selected)
- .map(item => ({ skuCode: item.skuCode, quantity: Number(item.quantity) }))
- if (!selectedReturnOrderNo.value || !items.length) { ElMessage.warning('请至少选择一个需要贴标的 SKU'); return }
- if (items.some(item => item.quantity < 1 || item.quantity > (returnRelabelSkuRows.value.find(row => row.skuCode === item.skuCode)?.availableQuantity || 0))) {
- ElMessage.warning('本次贴标数量必须大于 0,且不能超过该 SKU 的库存总数'); return
- }
- const destination = fbaWarehouses.value.find(warehouse => warehouse.warehouseCode === form.destination)
- if (!destination) { ElMessage.warning('请选择 FBA 目的仓'); return }
- const selectedFile = cartonTotalFile.value || skuTotalFile.value
- const fileScope = cartonTotalFile.value ? 'CARTON_TOTAL' as const : 'SKU_TOTAL' as const
- if (!selectedFile) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return }
- try { await ElMessageBox.confirm(`确认提交退货单 ${selectedReturnOrderNo.value} 的贴标申请吗?`, '提交确认', { confirmButtonText: '提交', cancelButtonText: '取消' }) } catch { return }
- saving.value = true
- try {
- const created = await createReturnRelabel(selectedReturnOrderNo.value, {
- items,
- fileScope,
- destinationFbaWarehouseId: String(destination.id),
- destinationFbaWarehouseCode: destination.warehouseCode,
- cartonRemark: form.remark.trim() || undefined,
- idempotencyKey: createIdempotencyKey(),
- })
- await uploadReturnRelabelFile(created.relabelNo, fileScope, selectedFile)
- ElMessage.success(`贴标申请 ${created.relabelNo} 已提交,等待审核`)
- await router.push({ name: 'overseas-labeling' })
- } catch (error) { ElMessage.error(error instanceof Error ? error.message : '退货贴标申请提交失败,请重试') }
- finally { saving.value = false }
-}
watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => {
if (nextSize !== previousSize && nextPage !== 1) { page.value = 1; return }
void load()
})
async function submit(action: 'draft' | 'submit' = 'submit') {
- if (isReturnRelabelSource.value) { await submitReturnRelabel(); return }
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value
const invalidCargo = props.service === 'disposal'
? !selectedDiscardOrderId.value || cargoRows.value.every((item) => item.quantity < 1)
|| cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0))
: (props.service === 'labeling' || props.service === 'relabel') && (relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value !== 'carton'
- ? skuRelabelRows.value.some((item) => !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1
+ ? skuRelabelRows.value.some((item) => (!hideReturnSkuCartonMark.value && !item.cartonMark) || !item.originalSku || !item.newSku || item.quantity < 1
|| (labelingMode.value === 'carton_and_sku' && !item.fbaCartonMark))
: cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name
|| (props.service !== 'labeling' && item.quantity < 1))))
@@ -1019,7 +1019,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.shipmentNo || !item.cartonMark || !item.fbaCartonMark)) {
ElMessage.warning('请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整'); return
}
- if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.sourceInventoryPositionId)) {
+ if (props.service === 'labeling' && labelingMode.value === 'carton' && cartonDetailRows.value.some((item) => !item.returnCartonId && !item.sourceInventoryPositionId)) {
ElMessage.warning('请选择有库存的历史入仓单'); return
}
if (props.service === 'disposal' && !form.reason) {
@@ -1399,34 +1399,17 @@ function selectRelabelInventory(inboundOrderId: string) {
const rows: CargoRow[] = []
const selectedCandidates = relabelCandidates.value.filter((item) => {
const returnOrderNo = item.inboundOrderNo || String(item.returnCartonId || '')
- return (item.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(item.inboundOrderId)) === inboundOrderId
+ return (item.returnCartonId ? `RETURN_ORDER:${returnOrderNo}` : String(item.inboundOrderId)) === inboundOrderId
})
- returnRelabelSkuRows.value = []
if (inboundOrderId.startsWith('RETURN_ORDER:')) {
- const grouped = new Map()
- for (const candidate of selectedCandidates) {
- const skuItems = candidate.skuItems?.length
- ? candidate.skuItems
- : candidate.skuCode ? [{ skuCode: candidate.skuCode, quantity: inventoryQuantity(candidate.availablePackages) }] : []
- for (const item of skuItems) {
- const skuCode = item.skuCode.trim()
- if (!skuCode) continue
- const key = skuCode.toUpperCase()
- const current = grouped.get(key)
- const quantity = inventoryQuantity(item.quantity)
- if (current) current.availableQuantity += quantity
- else grouped.set(key, {
- skuCode,
- productNameZh: candidate.skuCode?.trim().toUpperCase() === key ? (candidate.productNameZh || '') : '',
- availableQuantity: quantity,
- selected: false,
- // 可贴库存和本次申请数量是两个概念;重复换标时默认不应自动带入上次/首个箱子的数量。
- quantity: 0,
- })
- }
- }
- returnRelabelSkuRows.value = [...grouped.values()]
labelingMode.value = 'sku'
+ for (const candidate of selectedCandidates) rows.push({
+ id: ++cargoSequence, sku: candidate.skuItems?.[0]?.skuCode || candidate.skuCode || '',
+ name: candidate.productNameZh || '退货库存', included: false, quantity: 1, availableQuantity: 1,
+ returnCartonId: String(candidate.returnCartonId), sourceInboundOrderNo: candidate.inboundOrderNo,
+ fnsku: '', shipmentNo: candidate.inboundOrderNo || '', newSkuCode: '', shippingMark: '', customerCartonMarks: '',
+ cartonMark: candidate.cartonMark || '', fbaCartonMark: '', destinationWarehouseId: '', remark: '',
+ })
} else for (const candidate of selectedCandidates) {
for (const position of candidate.positions) rows.push(...Array.from({ length: Math.min(inventoryQuantity(position.availablePackages), 500) }, () => ({
id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物',
@@ -1440,7 +1423,7 @@ function selectRelabelInventory(inboundOrderId: string) {
})))
}
cargoRows.value = rows.slice(0, 500)
- if (cargoRows.value.length) cargoRows.value[0].included = true
+ if (cargoRows.value.length && !inboundOrderId.startsWith('RETURN_ORDER:')) cargoRows.value[0].included = true
skuRelabelRows.value = []
importedRelabelBindings.value = []
shippingLabelFile.value = null
@@ -1448,6 +1431,22 @@ function selectRelabelInventory(inboundOrderId: string) {
skuTotalFile.value = null
if (!isReturnRelabelSource.value && labelingMode.value !== 'carton') addSkuRelabelRow()
}
+function addReturnSkuBinding(source: RelabelInventoryCandidate, skuCode: string) {
+ const sku = source.skuItems?.find(item => item.skuCode === skuCode)
+ if (!sku) return
+ const row = skuRelabelRows.value.find(item => item.cartonMark === source.cartonMark && item.originalSku === skuCode)
+ if (row) return
+ const sourceRow = cargoRows.value.find(item => item.returnCartonId === String(source.returnCartonId))
+ skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: sourceRow?.id || null, cartonMark: source.cartonMark || '', originalSku: skuCode, newSku: '', quantity: Math.max(1, Number(sku.quantity)), fbaCartonMark: '', destinationWarehouseId: '', remark: '' })
+}
+function addReturnBoxBinding(source: RelabelInventoryCandidate) {
+ if (labelingMode.value !== 'carton') {
+ for (const sku of source.skuItems || []) addReturnSkuBinding(source, sku.skuCode)
+ return
+ }
+ if (cartonDetailRows.value.some(item => item.cartonMark === source.cartonMark)) return
+ cargoRows.value.push({ id: ++cargoSequence, included: true, sku: source.skuItems?.map(item => item.skuCode).join(',') || '', name: source.productNameZh || '退货箱', shipmentNo: selectedReturnOrderNo.value, cartonMark: source.cartonMark || '', fbaCartonMark: '', remark: '', sourceInventoryBalanceId: undefined, sourceInventoryPositionId: undefined, sourceInboundOrderId: undefined, returnCartonId: String(source.returnCartonId), quantity: 1, fnsku: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', destinationWarehouseId: '' })
+}
@@ -1467,8 +1466,17 @@ function selectRelabelInventory(inboundOrderId: string) {
{{ mode === 'edit' ? `编辑转运草稿 ${editingTransferNo}` : config.createTitle }}
返回列表