diff --git a/src/modules/overseas-warehouse/utils/returnReceiptDisplay.test.mjs b/src/modules/overseas-warehouse/utils/returnReceiptDisplay.test.mjs index 34d1957..8e6d370 100644 --- a/src/modules/overseas-warehouse/utils/returnReceiptDisplay.test.mjs +++ b/src/modules/overseas-warehouse/utils/returnReceiptDisplay.test.mjs @@ -21,6 +21,16 @@ test('volume rounds upward to exactly three decimal places without increasing ex } }) +test('箱子在签收箱号明细展示一次,真实托盘和缺少箱号明细的实物仍保留', () => { + const boxes = Array.from({ length: 6 }, (_, index) => ({ cartonMark: `RC-${index}` })) + const units = boxes.map(box => ({ unitType: 'BOX', unitCode: box.cartonMark, palletCode: 'PLT-SHARED' })) + assert.deepEqual(exports.receiptPhysicalDetails(units, boxes), []) + const pallet = { unitType: 'PALLET', unitCode: 'PLT-SHARED', palletCode: 'PLT-SHARED' } + const unmatchedBox = { unitType: 'BOX', unitCode: 'RC-UNKNOWN', palletCode: 'PLT-SHARED' } + assert.deepEqual(exports.receiptPhysicalDetails([...units, pallet, unmatchedBox], boxes), [pallet, unmatchedBox]) + assert.deepEqual(exports.receiptPhysicalDetails(units, []), units) +}) + test('parcel waybills appear as tracking numbers while generated parcel codes remain physical codes', () => { assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'sf561626' }), 'sf561626') assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK0123456789ABCDEF' }), '') diff --git a/src/modules/overseas-warehouse/utils/returnReceiptDisplay.ts b/src/modules/overseas-warehouse/utils/returnReceiptDisplay.ts index 4dcddc2..48e90eb 100644 --- a/src/modules/overseas-warehouse/utils/returnReceiptDisplay.ts +++ b/src/modules/overseas-warehouse/utils/returnReceiptDisplay.ts @@ -1,4 +1,9 @@ -import type { ReturnReceipt, ReturnReceiptCarton } from '../api/returnOrders' +import type { ReturnPallet, ReturnReceipt, ReturnReceiptCarton } from '../api/returnOrders' + +export function receiptPhysicalDetails(pallets: ReturnPallet[], boxes: ReturnReceiptCarton[]) { + const signedBoxMarks = new Set(boxes.map(box => box.cartonMark)) + return pallets.filter(unit => unit.unitType !== 'BOX' || !signedBoxMarks.has(unit.unitCode || unit.palletCode)) +} export function receiptBoxDetails(receipts: Pick[], cartons: ReturnReceiptCarton[]) { const boxReceiptNos = new Set(receipts.filter(receipt => receipt.unitType === 'BOX' || receipt.unitType === 'PALLET').map(receipt => receipt.receiptNo)) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index 5cd3765..3def722 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -144,6 +144,28 @@ function printHarness() { return { print, calls } } +test('包裹唛头可按实物识别码打印,不把快递单号或商品件数当作唛头', async () => { + const { print, calls } = printHarness() + await print({ unitType: 'PARCEL', unitCode: 'PK-PARCEL-1', trackingNo: 'SF-WAYBILL', receivedQuantity: 20 }) + assert.deepEqual(calls.fetched, ['PK-PARCEL-1']) + assert.ok(calls.html.includes('data-barcode="PK-PARCEL-1"')) + assert.equal((calls.html.match(/
/g) || []).length, 1) + assert.equal(calls.printed, 1) + assert.deepEqual(calls.errors, []) +}) + +test('批量包裹唛头按包裹分页、去重并保留服务端条码模板', async () => { + const { print, calls } = printHarness() + const parcels = ['PK-PARCEL-1', 'PK-PARCEL-2'].map(mark => ({ unitType: 'PARCEL', unitCode: mark, + receivedQuantity: 3, labels: [{ cartonMark: mark, printableHtml: labelHtml(mark) }] })) + await print([...parcels, parcels[0]]) + assert.equal((calls.html.match(/
/g) || []).length, 2) + assert.ok(calls.html.includes('data-barcode="PK-PARCEL-1"')) + assert.ok(calls.html.includes('data-barcode="PK-PARCEL-2"')) + assert.deepEqual(calls.fetched, []) + assert.equal(calls.printed, 1) +}) + test('退货箱唛打印保留服务端整份模板,不另行覆盖纸张或条码', async () => { const { print, calls } = printHarness() const printableHtml = labelHtml('RETURN-TEST-1') diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index 99afafb..34380cf 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -11,7 +11,7 @@ import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-l import { createIdempotencyKey } from '@/utils/idempotencyKey' import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders' import { getReturnInventoryLabel } from '../api/returnInventory' -import { formatReturnVolume, receiptBoxDetails, receiptTrackingNo } from '../utils/returnReceiptDisplay' +import { formatReturnVolume, receiptBoxDetails, receiptPhysicalDetails, receiptTrackingNo } from '../utils/returnReceiptDisplay' const props = withDefaults(defineProps<{ createPage?: boolean; detailPage?: boolean; returnNo?: string }>(), { createPage: false, detailPage: false, returnNo: '' }) const router = useRouter() @@ -28,8 +28,13 @@ const selected = ref() const showCartons = ref(false) const parcelDetailsExpanded = ref(true) const receiptBatchesExpanded = ref(true) -const signedBoxDetails = computed(() => receiptBoxDetails(selected.value?.receipts || [], selected.value?.cartons || [])) +const signedBoxDetails = computed(() => receiptBoxDetails(selected.value?.receipts || [], selected.value?.cartons || []).map(box => ({ + ...box, + measurements: selected.value?.pallets.find(unit => unit.unitType === 'BOX' && (unit.unitCode || unit.palletCode) === box.cartonMark), +}))) +const physicalDetails = computed(() => receiptPhysicalDetails(selected.value?.pallets || [], signedBoxDetails.value)) const printableReceipts = computed(() => selected.value?.receipts.filter(item => item.unitType !== 'PARCEL' && (item.cartonMark || item.labels?.length)) || []) +const parcelReceipts = computed(() => selected.value?.receipts.filter(item => item.unitType === 'PARCEL') || []) const printableBoxCount = computed(() => printableReceipts.value.reduce((total, receipt) => total + (receipt.labels?.length || receipt.boxCount || 1), 0)) const photoUrls = reactive>({}) const rows = ref([]) @@ -190,11 +195,13 @@ async function printReturnLabel(receipt: ReturnReceipt | ReturnReceipt[]) { if (!popup) { ElMessage.warning('请允许浏览器打开打印窗口'); return } popup.opener = null try { - 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 groups = await Promise.all(receipts.map(async item => { + if (item.labels?.length) return await Promise.all(item.labels.map(async label => ({ ...label, printableHtml: label.printableHtml || (await getReturnInventoryLabel(label.cartonMark)).printableHtml }))) + const mark = item.cartonMark || (item.unitType === 'PARCEL' ? item.unitCode : undefined) + return mark ? [{ cartonMark: mark, printableHtml: item.printableHtml || (await getReturnInventoryLabel(mark)).printableHtml }] : [] + })) const labels = [...new Map(groups.flat().map(label => [label.cartonMark, label])).values()] - if (!labels.length) { popup.close(); ElMessage.warning('暂无可打印的箱唛'); return } + if (!labels.length) { popup.close(); ElMessage.warning('暂无可打印的唛头'); return } const printStyles = [...new Set(labels.flatMap(label => label.printableHtml.match(/]*>[\s\S]*?<\/style>/gi) || []))].join('') const printableHtml = (receipts.length === 1 ? receipts[0]?.printableHtml : undefined) || `退货箱唛${printStyles}${labels.map((label) => { const html = label.printableHtml || '' @@ -211,7 +218,7 @@ async function printReturnLabel(receipt: ReturnReceipt | ReturnReceipt[]) { popup.print() } catch (error) { popup.close() - ElMessage.error(error instanceof Error ? error.message : '箱唛加载失败') + ElMessage.error(error instanceof Error ? error.message : '唛头加载失败') } } function openDetail(returnNo: string) { void router.push({ name: 'overseas-return-relabel-detail', params: { returnNo } }) } @@ -383,18 +390,18 @@ onBeforeUnmount(clearPhotoUrls)
-

包裹签收明细{{ parcelDetailsExpanded ? '收起明细' : '展开明细' }}

+

包裹签收明细打印全部包裹唛头({{ parcelReceipts.length }} 个){{ parcelDetailsExpanded ? '收起明细' : '展开明细' }}

-
+
包裹 {{ receipt.unitCode || receipt.trackingNo || receipt.receiptNo }}{{ receipt.receivedQuantity }} 件
{{ item.skuCode }}{{ item.productNameZh || '中文品名未填写' }}{{ item.quantity }} 件
-
-

实物明细({{ selected.pallets?.length || 0 }} 个托盘/箱)打印全部箱唛({{ printableBoxCount }} 箱)

- - +
+

实物明细({{ physicalDetails.length }} 个托盘/箱)打印全部箱唛({{ printableBoxCount }} 箱)

+ +
长 {{ pallet.length ?? '—' }} cm · 宽 {{ pallet.width ?? '—' }} cm · 高 {{ pallet.height ?? '—' }} cm · 重量 {{ pallet.weight ?? '—' }} kg
@@ -408,13 +415,14 @@ onBeforeUnmount(clearPhotoUrls)
-

签收箱号明细({{ signedBoxDetails.length }} 箱)查看箱号明细

+

签收箱号明细({{ signedBoxDetails.length }} 箱)打印全部箱唛({{ printableBoxCount }} 箱)查看箱号明细

签收单 {{ box.receiptNo }}
+
长 {{ box.measurements.length ?? '—' }} cm · 宽 {{ box.measurements.width ?? '—' }} cm · 高 {{ box.measurements.height ?? '—' }} cm · 重量 {{ box.measurements.weight ?? '—' }} kg · 体积 {{ formatReturnVolume(box.measurements.volumeCbm) }} m³
diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 0117fcb..3430059 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -32,7 +32,7 @@ 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' | '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 SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; shipmentNo: string; destinationWarehouseId: string; remark: string } interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string } interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] } interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string } @@ -522,7 +522,7 @@ 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, - fbaCartonMark: '', destinationWarehouseId: form.destination, remark: '' }) + fbaCartonMark: '', shipmentNo: '', destinationWarehouseId: form.destination, remark: '' }) } function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) } function applyExistingSkuBinding(row: SkuRelabelRow) { @@ -539,6 +539,7 @@ function applyExistingCartonBinding(row: SkuRelabelRow) { && item.cartonMark.trim().toUpperCase() === cartonMark && item.fbaCartonMark.trim()) if (!mapping) return if (!row.fbaCartonMark.trim()) row.fbaCartonMark = mapping.fbaCartonMark + if (!row.shipmentNo.trim()) row.shipmentNo = mapping.shipmentNo if (!row.destinationWarehouseId) row.destinationWarehouseId = mapping.destinationWarehouseId } function selectShippingLabel(event: Event) { shippingLabelFile.value = (event.target as HTMLInputElement).files?.[0] || null } @@ -597,10 +598,14 @@ function buildSkuRelabelItems(): RelabelCreate['items'] { const cartonMark = rows[0].cartonMark.trim() const changeCarton = labelingMode.value === 'carton_and_sku' const fbaCartonMark = rows[0].fbaCartonMark.trim() + const shipmentNo = rows[0].shipmentNo.trim() if (changeCarton && (!fbaCartonMark || !form.destination || rows.some((row) => row.fbaCartonMark.trim() !== fbaCartonMark))) { throw new Error('同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛,并选择整票 FBA 目的仓') } + if (changeCarton && (!shipmentNo || rows.some(row => row.shipmentNo.trim() !== shipmentNo))) { + throw new Error('同一我方箱唛的组合换标必须填写相同的 FBA 货件编号') + } return { sourceInventoryBalanceId: source.sourceInventoryBalanceId, sourceInventoryPositionId: source.sourceInventoryPositionId, @@ -610,7 +615,7 @@ function buildSkuRelabelItems(): RelabelCreate['items'] { productNameZh: source.name, totalPackageCount: 1, notifiedPackageCount: 1, - shipmentNo: source.shipmentNo || cartonMark, + shipmentNo: changeCarton ? shipmentNo : source.shipmentNo || cartonMark, trackingCode: form.trackingNo || undefined, newSkuCode: rows[0].newSku, labelRequirement: form.requirement || undefined, @@ -818,7 +823,7 @@ function importSelectedBindingsToDetails() { } labelingMode.value = 'carton' } else { - const importedRows = selected.map(({ templateType: _, selected: __, shipmentNo, ...item }) => item) + const importedRows = selected.map(({ templateType: _, selected: __, ...item }) => item) for (const item of importedRows) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => !(row.cartonMark.trim().toUpperCase() === item.cartonMark.trim().toUpperCase() && row.originalSku.trim().toUpperCase() === item.originalSku.trim().toUpperCase())) @@ -1005,7 +1010,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') { || 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 - || (labelingMode.value === 'carton_and_sku' && !item.fbaCartonMark)) + || (labelingMode.value === 'carton_and_sku' && (!item.fbaCartonMark || !item.shipmentNo.trim()))) : cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || (props.service !== 'labeling' && item.quantity < 1)))) if ((props.service === 'relabel' && !form.inboundNo) || !primaryWarehouse || invalidCargo) { @@ -1452,7 +1457,7 @@ function addReturnSkuBinding(source: RelabelInventoryCandidate, skuCode: string) 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: '' }) + skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: sourceRow?.id || null, cartonMark: source.cartonMark || '', originalSku: skuCode, newSku: '', quantity: Math.max(1, Number(sku.quantity)), fbaCartonMark: '', shipmentNo: '', destinationWarehouseId: '', remark: '' }) } function addReturnBoxBinding(source: RelabelInventoryCandidate) { if (labelingMode.value !== 'carton') { @@ -1500,7 +1505,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { - +
*入仓方式仓库上门提货客户送货到仓
@@ -1591,7 +1596,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
添加 SKU 明细清空
商品 SKU中文品名 *FBA 货件编号 *箱数仓库唛头 *新 FBA 箱唛 *备注操作
1 箱
暂无本次换标明细,请上传关系后导入或手动添加
-
仓库唛头 *旧 SKU *新 SKU *换标数量(件)*新 FBA 箱唛 *备注操作
暂无绑定明细,请点击“添加 SKU 明细”
+
仓库唛头 *旧 SKU *新 SKU *换标数量(件)*新 FBA 箱唛 *FBA 货件编号 *备注操作
暂无绑定明细,请点击“添加 SKU 明细”