发布:将已验 TEST 功能更新至既有生产灰度(2026-10-23) #15
@@ -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' }), '')
|
||||
|
||||
@@ -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<ReturnReceipt, 'receiptNo' | 'unitType'>[], cartons: ReturnReceiptCarton[]) {
|
||||
const boxReceiptNos = new Set(receipts.filter(receipt => receipt.unitType === 'BOX' || receipt.unitType === 'PALLET').map(receipt => receipt.receiptNo))
|
||||
|
||||
@@ -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(/<section class="return-label">/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(/<section class="return-label">/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')
|
||||
|
||||
@@ -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<ReturnOrderDetail>()
|
||||
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<Record<string, string>>({})
|
||||
const rows = ref<ReturnOrderSummary[]>([])
|
||||
@@ -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(/<style\b[^>]*>[\s\S]*?<\/style>/gi) || []))].join('')
|
||||
const printableHtml = (receipts.length === 1 ? receipts[0]?.printableHtml : undefined) || `<!doctype html><html><head><meta charset="utf-8"><title>退货箱唛</title>${printStyles}<style>.return-label{break-after:page}.return-label:last-child{break-after:auto}.return-label .label{page-break-after:auto}</style></head><body>${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)
|
||||
</el-table>
|
||||
</section>
|
||||
<section v-if="selected.receipts?.some(item => item.unitType === 'PARCEL')" class="return-detail-section">
|
||||
<h3 class="section-heading-with-action"><span>包裹签收明细</span><el-button text :aria-expanded="parcelDetailsExpanded" aria-controls="return-parcel-details" @click="parcelDetailsExpanded = !parcelDetailsExpanded">{{ parcelDetailsExpanded ? '收起明细' : '展开明细' }}</el-button></h3>
|
||||
<h3 class="section-heading-with-action"><span>包裹签收明细</span><span class="return-label-actions"><el-button type="primary" size="small" @click="printReturnLabel(parcelReceipts)">打印全部包裹唛头({{ parcelReceipts.length }} 个)</el-button><el-button text :aria-expanded="parcelDetailsExpanded" aria-controls="return-parcel-details" @click="parcelDetailsExpanded = !parcelDetailsExpanded">{{ parcelDetailsExpanded ? '收起明细' : '展开明细' }}</el-button></span></h3>
|
||||
<div v-show="parcelDetailsExpanded" id="return-parcel-details">
|
||||
<details v-for="receipt in (selected.receipts || []).filter(item => item.unitType === 'PARCEL')" :key="`${receipt.receiptNo}-items`" class="return-parcel-items" open>
|
||||
<details v-for="receipt in parcelReceipts" :key="`${receipt.receiptNo}-items`" class="return-parcel-items" open>
|
||||
<summary class="return-parcel-items__head"><strong>包裹 {{ receipt.unitCode || receipt.trackingNo || receipt.receiptNo }}</strong><span class="return-parcel-items__summary">{{ receipt.receivedQuantity }} 件<ChevronDown :size="16" aria-hidden="true" /></span></summary>
|
||||
<div class="return-parcel-items__list"><div v-for="item in receipt.items" :key="item.skuCode" class="return-parcel-item"><span class="return-parcel-item__sku">{{ item.skuCode }}</span><span class="return-parcel-item__name">{{ item.productNameZh || '中文品名未填写' }}</span><strong>{{ item.quantity }} 件</strong></div></div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="selected.pallets?.length" class="return-detail-section">
|
||||
<h3 class="section-heading-with-action"><span>实物明细({{ selected.pallets?.length || 0 }} 个托盘/箱)</span><span v-if="printableReceipts.length" class="return-label-actions"><el-button type="primary" size="small" @click="printReturnLabel(printableReceipts)">打印全部箱唛({{ printableBoxCount }} 箱)</el-button></span></h3>
|
||||
<el-collapse v-if="selected.pallets?.length" class="return-pallet-list">
|
||||
<el-collapse-item v-for="pallet in selected.pallets" :key="pallet.palletCode" :name="pallet.palletCode">
|
||||
<section v-if="physicalDetails.length" class="return-detail-section">
|
||||
<h3 class="section-heading-with-action"><span>实物明细({{ physicalDetails.length }} 个托盘/箱)</span><span v-if="printableReceipts.length" class="return-label-actions"><el-button type="primary" size="small" @click="printReturnLabel(printableReceipts)">打印全部箱唛({{ printableBoxCount }} 箱)</el-button></span></h3>
|
||||
<el-collapse v-if="physicalDetails.length" class="return-pallet-list">
|
||||
<el-collapse-item v-for="pallet in physicalDetails" :key="pallet.palletCode" :name="pallet.palletCode">
|
||||
<template #title><strong>{{ pallet.unitType === 'BOX' ? '箱子' : '托盘' }} {{ pallet.unitCode || pallet.palletCode }}</strong><span class="return-pallet-summary">{{ pallet.skuTypes }} 种 SKU · {{ pallet.totalQuantity }} 件 · {{ pallet.totalBoxCount }} 箱 · 体积 {{ formatReturnVolume(pallet.volumeCbm) }} m³</span></template>
|
||||
<div class="return-pallet-measurements">长 {{ pallet.length ?? '—' }} cm · 宽 {{ pallet.width ?? '—' }} cm · 高 {{ pallet.height ?? '—' }} cm · 重量 {{ pallet.weight ?? '—' }} kg</div>
|
||||
<el-table :data="pallet.items" size="small" border>
|
||||
@@ -408,13 +415,14 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<el-empty v-else description="暂无打托记录" :image-size="64" />
|
||||
</section>
|
||||
<section v-if="signedBoxDetails.length" class="return-detail-section">
|
||||
<h3 class="section-heading-with-action"><span>签收箱号明细({{ signedBoxDetails.length }} 箱)</span><span class="return-label-actions"><el-button type="primary" size="small" @click="showCartons = true">查看箱号明细</el-button></span></h3>
|
||||
<h3 class="section-heading-with-action"><span>签收箱号明细({{ signedBoxDetails.length }} 箱)</span><span class="return-label-actions"><el-button v-if="!physicalDetails.length && printableReceipts.length" type="primary" size="small" @click="printReturnLabel(printableReceipts)">打印全部箱唛({{ printableBoxCount }} 箱)</el-button><el-button type="primary" size="small" @click="showCartons = true">查看箱号明细</el-button></span></h3>
|
||||
<el-dialog v-model="showCartons" title="签收箱号明细" width="1120px" align-center destroy-on-close>
|
||||
<div class="return-cartons-dialog-body">
|
||||
<el-collapse v-if="signedBoxDetails.length" class="return-pallet-list">
|
||||
<el-collapse-item v-for="box in signedBoxDetails" :key="box.cartonMark" :name="box.cartonMark">
|
||||
<template #title><strong>{{ box.cartonMark }}</strong><span class="return-pallet-summary">{{ box.countStatus === 'CONFIRMED' ? `已确认 · ${box.items.length} 种 SKU · ${box.items.reduce((sum, item) => sum + Number(item.quantity), 0)} 件` : box.countStatus === 'PENDING' ? '待清点' : '未留存箱内签收明细' }}</span></template>
|
||||
<div class="return-pallet-measurements">签收单 {{ box.receiptNo }}<template v-if="box.palletCode"> · 托盘 {{ box.palletCode }}</template><template v-if="box.countedAt"> · 确认时间 {{ formatDateTime(box.countedAt) }}</template></div>
|
||||
<div v-if="box.measurements" class="return-pallet-measurements">长 {{ box.measurements.length ?? '—' }} cm · 宽 {{ box.measurements.width ?? '—' }} cm · 高 {{ box.measurements.height ?? '—' }} cm · 重量 {{ box.measurements.weight ?? '—' }} kg · 体积 {{ formatReturnVolume(box.measurements.volumeCbm) }} m³</div>
|
||||
<el-table v-if="box.countStatus === 'CONFIRMED'" :data="box.items" size="small" border>
|
||||
<el-table-column prop="skuCode" label="SKU" min-width="220" />
|
||||
<el-table-column prop="productNameZh" label="中文品名" min-width="180" />
|
||||
|
||||
@@ -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) {
|
||||
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></label>
|
||||
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="搜索单号或选择可用库存" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" /></el-select><small class="warehouse-hint">在仓数量包含被占用货物;本次可选数量才可加入申请。合箱后的库存按当前包裹/箱展示,来源退货单保留。</small></label>
|
||||
<label v-if="props.service === 'transfer'"><span>库存转运来源(可选)</span><el-select v-model="selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="不选则按新货转运;选择后免 PDA 测量" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint">库存转运可直接进入目的仓、换标和排车流程,PDA 可拆托、重打或同目的仓合托。</small></label>
|
||||
<label v-if="props.service === 'labeling' && (isReturnRelabelSource || labelingMode !== 'sku')"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>入仓方式</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group></div>
|
||||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="pickup-address-field"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="请输入仓库上门提货的详细地址" /></label>
|
||||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="请输入提货联系人" maxlength="128" /></label>
|
||||
@@ -1591,7 +1596,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<div v-else><el-button type="primary" size="small" @click="addSkuRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="skuRelabelRows = []">清空</el-button></div>
|
||||
</div>
|
||||
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>仓库唛头 *</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" :readonly="isReturnRelabelSource" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td><td><button type="button" class="warehouse-delete" title="移出本次明细" @click="removeCargoRow(row.id)"><Trash2 :size="16" />移出</button></td></tr><tr v-if="cartonDetailRows.length === 0"><td colspan="8"><div class="warehouse-empty">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
|
||||
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-sku-label-table"><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th v-if="labelingMode === 'carton_and_sku'">新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-input v-model="row.cartonMark" :readonly="isReturnRelabelSource" placeholder="请输入仓库唛头" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" :readonly="isReturnRelabelSource" placeholder="请输入旧 SKU" @change="applyExistingSkuBinding(row)" /></td><td><el-input v-model="row.newSku" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除" @click="removeSkuRelabelRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="skuRelabelRows.length === 0"><td :colspan="labelingMode === 'carton_and_sku' ? 7 : 6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||||
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-sku-label-table"><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th v-if="labelingMode === 'carton_and_sku'">新 FBA 箱唛 *</th><th v-if="labelingMode === 'carton_and_sku'">FBA 货件编号 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-input v-model="row.cartonMark" :readonly="isReturnRelabelSource" placeholder="请输入仓库唛头" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" :readonly="isReturnRelabelSource" placeholder="请输入旧 SKU" @change="applyExistingSkuBinding(row)" /></td><td><el-input v-model="row.newSku" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.shipmentNo" placeholder="请输入 FBA 货件编号" /></td><td><el-input v-model="row.remark" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除" @click="removeSkuRelabelRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="skuRelabelRows.length === 0"><td :colspan="labelingMode === 'carton_and_sku' ? 8 : 6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="warehouse-section-heading" :class="{ 'warehouse-step-heading': props.service === 'transfer' }"><div><h3><span v-if="props.service === 'transfer'" class="warehouse-step-number">1</span>货物明细</h3></div><div v-if="props.service !== 'disposal'" class="warehouse-heading-actions"><el-button v-if="props.service === 'transfer'" size="small" :disabled="!cargoRows.some((row) => row.sku.trim() && row.quantity > 0)" @click="printTransferDraftCartonMarks"><Printer :size="15" />打印唛头</el-button><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []">清空</el-button></div></div>
|
||||
|
||||
@@ -30,7 +30,7 @@ test('整包裹支持SKU和混贴提交,实物数为1且各SKU数量仍按件'
|
||||
const builder = view.slice(view.indexOf('function buildSkuRelabelItems'), view.indexOf('function addRelabelCargoRow'))
|
||||
const compiledBuilder = ts.transpileModule(builder, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
const cargo = { id: 1, returnCartonId: 'PARCEL-ID', cartonMark: 'RP-1', sku: 'SKU-A', availableQuantity: 1 }
|
||||
const rows = [{ sourceRowId: 1, cartonMark: 'RP-1', originalSku: 'SKU-A', newSku: 'NEW-A', quantity: 3, fbaCartonMark: 'FBA-NEW' }, { sourceRowId: 1, cartonMark: 'RP-1', originalSku: 'SKU-B', newSku: 'NEW-B', quantity: 2, fbaCartonMark: 'FBA-NEW' }]
|
||||
const rows = [{ sourceRowId: 1, cartonMark: 'RP-1', originalSku: 'SKU-A', newSku: 'NEW-A', quantity: 3, fbaCartonMark: 'FBA-NEW', shipmentNo: 'FBA-SHIP-NEW' }, { sourceRowId: 1, cartonMark: 'RP-1', originalSku: 'SKU-B', newSku: 'NEW-B', quantity: 2, fbaCartonMark: 'FBA-NEW', shipmentNo: 'FBA-SHIP-NEW' }]
|
||||
const candidate = { returnCartonId: 'PARCEL-ID', sourceType: 'RETURN_PARCEL', skuItems: [{ skuCode: 'SKU-A', quantity: 3 }, { skuCode: 'SKU-B', quantity: 2 }] }
|
||||
for (const mode of ['sku', 'carton_and_sku']) {
|
||||
const build = new Function('skuRelabelRows', 'cargoRows', 'isReturnRelabelSource', 'selectedReturnCandidates', 'labelingMode', 'form', compiledBuilder + '; return buildSkuRelabelItems')({ value: rows }, { value: [cargo] }, { value: true }, { value: [candidate] }, { value: mode }, { destination: 'FBA-WH' })
|
||||
@@ -40,7 +40,19 @@ test('整包裹支持SKU和混贴提交,实物数为1且各SKU数量仍按件'
|
||||
assert.equal(item.totalPackageCount, 1)
|
||||
assert.equal(item.notifiedPackageCount, 1)
|
||||
assert.deepEqual(item.skuItems.map(sku => sku.quantity), [3, 2])
|
||||
if (mode === 'carton_and_sku') assert.equal(item.fbaCartonMark, 'FBA-NEW')
|
||||
if (mode === 'carton_and_sku') {
|
||||
assert.equal(item.fbaCartonMark, 'FBA-NEW')
|
||||
assert.equal(item.destinationFbaWarehouseCode, 'FBA-WH')
|
||||
assert.equal(item.shipmentNo, 'FBA-SHIP-NEW')
|
||||
rows[1].shipmentNo = 'OTHER-SHIPMENT'
|
||||
assert.throws(build, /相同的 FBA 货件编号/)
|
||||
rows[1].shipmentNo = 'FBA-SHIP-NEW'
|
||||
} else {
|
||||
assert.equal(item.fbaCartonMark, undefined)
|
||||
assert.equal(item.destinationFbaWarehouseCode, undefined)
|
||||
const buildWithoutDestination = new Function('skuRelabelRows', 'cargoRows', 'isReturnRelabelSource', 'selectedReturnCandidates', 'labelingMode', 'form', compiledBuilder + '; return buildSkuRelabelItems')({ value: rows }, { value: [cargo] }, { value: true }, { value: [candidate] }, { value: mode }, { destination: '' })
|
||||
assert.equal(buildWithoutDestination()[0].operationType, 'SKU_LABEL')
|
||||
}
|
||||
rows[0].quantity = 4
|
||||
assert.throws(build, /超过来源箱可用数量/)
|
||||
rows[0].quantity = 3
|
||||
|
||||
Reference in New Issue
Block a user