This commit is contained in:
2026-10-12 15:56:07 +08:00
parent 6cb9b91435
commit 06f91c8b91
4 changed files with 83 additions and 10 deletions
@@ -28,6 +28,7 @@ export interface RelabelDetail {
export interface DiscardSummary { id: string; discardNo: string; outboundNo?: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string; completedAt?: string } export interface DiscardSummary { id: string; discardNo: string; outboundNo?: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string; completedAt?: string }
export interface DiscardCandidate { orderId: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> } export interface DiscardCandidate { orderId: string; sourceType?: 'DROPSHIP' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> }
export interface RelabelInventoryCandidate { export interface RelabelInventoryCandidate {
stockQuantity?: number; inventoryStatus?: string; occupiedRelabelNo?: string; mergedIntoCartonMarks?: string[]; hasOutbound?: boolean
inventoryBalanceId: string; inboundOrderId: string; inboundOrderNo?: string; inboundCode?: string inventoryBalanceId: string; inboundOrderId: string; inboundOrderNo?: string; inboundCode?: string
overseasWarehouseId: string; overseasWarehouseCode?: string; overseasWarehouseName?: string overseasWarehouseId: string; overseasWarehouseCode?: string; overseasWarehouseName?: string
destinationFbaWarehouseCode?: string; destinationFbaWarehouseName?: string; skuCode?: string; productNameZh?: string destinationFbaWarehouseCode?: string; destinationFbaWarehouseName?: string; skuCode?: string; productNameZh?: string
@@ -199,13 +199,15 @@ const selectedTransferSourceOrderIds = ref<string[]>([])
const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:')) const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:'))
const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '') const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '')
const selectedReturnCandidates = computed(() => relabelCandidates.value.filter(item => Boolean(item.returnCartonId) && returnSourceNos(item).includes(selectedReturnOrderNo.value))) const selectedReturnCandidates = computed(() => relabelCandidates.value.filter(item => Boolean(item.returnCartonId) && returnSourceNos(item).includes(selectedReturnOrderNo.value)))
const returnBoxCandidates = computed(() => selectedReturnCandidates.value.filter(item => item.sourceType === 'RETURN_CARTON')) const availableReturnCandidates = computed(() => selectedReturnCandidates.value.filter(item => inventoryQuantity(item.availablePackages) > 0).map(item => ({ ...item, skuItems: (item.skuItems || []).filter(sku => inventoryQuantity(sku.quantity) > 0) })))
const returnLooseCandidates = computed(() => selectedReturnCandidates.value.filter(item => item.sourceType === 'RETURN_PARCEL')) const returnBoxCandidates = computed(() => availableReturnCandidates.value.filter(item => item.sourceType === 'RETURN_CARTON'))
const returnLooseCandidates = computed(() => availableReturnCandidates.value.filter(item => item.sourceType === 'RETURN_PARCEL'))
const requiresBothTotalFiles = computed(() => (props.service === 'labeling' || (props.service === 'transfer' && form.relabel)) && labelingMode.value === 'carton_and_sku') const requiresBothTotalFiles = computed(() => (props.service === 'labeling' || (props.service === 'transfer' && form.relabel)) && labelingMode.value === 'carton_and_sku')
function inventoryQuantity(value: number | string | null | undefined) { function inventoryQuantity(value: number | string | null | undefined) {
const quantity = Number(value) const quantity = Number(value)
return Number.isFinite(quantity) && quantity > 0 ? quantity : 0 return Number.isFinite(quantity) && quantity > 0 ? quantity : 0
} }
const relabelInventorySearch = ref('')
function returnSourceNos(candidate: RelabelInventoryCandidate): string[] { function returnSourceNos(candidate: RelabelInventoryCandidate): string[] {
return [...new Set((candidate.inboundOrderNo || '').split(',').map(value => value.trim()).filter(Boolean))] return [...new Set((candidate.inboundOrderNo || '').split(',').map(value => value.trim()).filter(Boolean))]
} }
@@ -213,24 +215,40 @@ function returnRelabelProgress(candidate: RelabelInventoryCandidate) {
const labels: Record<string, string> = { UNLABELLED: '未换标', BOUND: '处理中', PROCESSING: '处理中', PARTIALLY_COMPLETED: '部分完成', COMPLETED: '全部完成' } const labels: Record<string, string> = { UNLABELLED: '未换标', BOUND: '处理中', PROCESSING: '处理中', PARTIALLY_COMPLETED: '部分完成', COMPLETED: '全部完成' }
return [labels[candidate.relabelStatus || ''] || '—', '实际完成 ' + (candidate.relabelledQuantity ?? '—') + ' 件', '处理中占用 ' + (candidate.relabelReservedQuantity ?? '—') + ' 件'].join('|') return [labels[candidate.relabelStatus || ''] || '—', '实际完成 ' + (candidate.relabelledQuantity ?? '—') + ' 件', '处理中占用 ' + (candidate.relabelReservedQuantity ?? '—') + ' 件'].join('|')
} }
function returnInventoryState(candidate: RelabelInventoryCandidate) {
const labels: Record<string, string> = { AVAILABLE: '可选库存', RELABEL_PENDING: '换标占用', PACKING_RESERVED: '合箱计划预留', OUTBOUND_PENDING: '出库占用', DISCARD_PENDING: '废弃申请占用', OUTBOUNDED: '已出库', MERGED: '已合箱' }
return [...(candidate.mergedIntoCartonMarks?.length ? ['历史部分转入 ' + candidate.mergedIntoCartonMarks.join('、') + '(不代表当前库存位置)'] : []), labels[candidate.inventoryStatus || ''] || (inventoryQuantity(candidate.availablePackages) > 0 ? '可选库存' : '当前不可选'), candidate.occupiedRelabelNo].filter(Boolean).join(' · ')
}
const relabelInboundOptions = computed(() => { const relabelInboundOptions = computed(() => {
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件' }>() const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件'; stockQuantity: number | null; units: number; hasOutbound: boolean; reasons: string[] }>()
for (const candidate of relabelCandidates.value) { for (const candidate of relabelCandidates.value) {
const sources = candidate.returnCartonId ? returnSourceNos(candidate) : [String(candidate.inboundOrderId)] const sources = candidate.returnCartonId ? returnSourceNos(candidate) : [String(candidate.inboundOrderId)]
for (const sourceNo of sources) { for (const sourceNo of sources) {
const value = candidate.returnCartonId ? 'RETURN_ORDER:' + sourceNo : sourceNo const value = candidate.returnCartonId ? 'RETURN_ORDER:' + sourceNo : sourceNo
const current = grouped.get(value) const current = grouped.get(value)
if (current) current.availablePackages += inventoryQuantity(candidate.availablePackages) if (current) {
current.hasOutbound ||= candidate.hasOutbound === true || candidate.inventoryStatus === 'OUTBOUNDED'
current.availablePackages += inventoryQuantity(candidate.availablePackages)
current.units += candidate.returnCartonId && candidate.stockQuantity === 0 ? 0 : 1
current.stockQuantity = current.stockQuantity == null || candidate.stockQuantity == null ? null : current.stockQuantity + inventoryQuantity(candidate.stockQuantity)
if (candidate.returnCartonId) current.reasons.push(returnInventoryState(candidate))
}
else grouped.set(value, { else grouped.set(value, {
value, inboundOrderNo: candidate.returnCartonId ? '退货单 ' + sourceNo : (candidate.inboundOrderNo || '—'), value, inboundOrderNo: candidate.returnCartonId ? '退货单 ' + sourceNo : (candidate.inboundOrderNo || '—'),
inboundCode: candidate.returnCartonId ? '已签收退货库存(含合箱)' : (candidate.inboundCode || '—'), inboundCode: candidate.returnCartonId ? '已签收退货库存(含合箱)' : (candidate.inboundCode || '—'),
availablePackages: inventoryQuantity(candidate.availablePackages), availablePackages: inventoryQuantity(candidate.availablePackages),
quantityUnit: candidate.returnCartonId ? '件' : '箱', quantityUnit: candidate.returnCartonId ? '件' : '箱',
stockQuantity: candidate.stockQuantity == null ? null : inventoryQuantity(candidate.stockQuantity), units: candidate.returnCartonId && candidate.stockQuantity === 0 ? 0 : 1, hasOutbound: candidate.hasOutbound === true || candidate.inventoryStatus === 'OUTBOUNDED', reasons: candidate.returnCartonId ? [returnInventoryState(candidate)] : [],
}) })
} }
} }
return [...grouped.values()] return [...grouped.values()].map(option => ({ ...option, reasons: [...new Set(option.reasons)] }))
}) })
const visibleRelabelInboundOptions = computed(() => relabelInboundOptions.value.filter(option => option.availablePackages > 0))
function relabelOptionLabel(option: typeof relabelInboundOptions.value[number]) {
if (option.quantityUnit === '箱') return option.inboundOrderNo + '|' + option.inboundCode + '|可选 ' + option.availablePackages + ' 箱'
return option.inboundOrderNo + '|在仓 ' + option.units + ' 包裹/箱 · ' + (option.stockQuantity ?? '待查询') + ' 件' + (option.hasOutbound && Number(option.stockQuantity) > 0 ? '|部分出库' : '')
}
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
const cartonTemplateInput = ref<HTMLInputElement | null>(null) const cartonTemplateInput = ref<HTMLInputElement | null>(null)
@@ -1480,7 +1498,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<label v-if="props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" 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 === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
<label v-else-if="props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>废弃原因</span><el-select v-model="form.reason" placeholder="请选择"><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" /></el-select></label> <label v-else-if="props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>废弃原因</span><el-select v-model="form.reason" placeholder="请选择"><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" /></el-select></label>
<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-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 :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无可用库存" placeholder="请选择当前仍有可用库存的入仓单或退货单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|${option.inboundCode}|可用 ${option.availablePackages} ${option.quantityUnit}`" :value="option.value" /></el-select></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 === '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' && (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>
<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> <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>
@@ -1548,11 +1566,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div v-if="isReturnRelabelSource" class="warehouse-table-scroll"> <div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
<template v-if="returnLooseCandidates.length"> <template v-if="returnLooseCandidates.length">
<div class="warehouse-section-heading"><h3>可用包裹</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableSkusCollapsed" aria-controls="return-available-skus" @click="availableSkusCollapsed = !availableSkusCollapsed">{{ availableSkusCollapsed ? '展开明细' : '收起明细' }}</el-button></div> <div class="warehouse-section-heading"><h3>可用包裹</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableSkusCollapsed" aria-controls="return-available-skus" @click="availableSkusCollapsed = !availableSkusCollapsed">{{ availableSkusCollapsed ? '展开明细' : '收起明细' }}</el-button></div>
<table v-show="!availableSkusCollapsed" id="return-available-skus" class="warehouse-edit-table"><thead><tr><th>包裹识别码</th><th>来源退货单</th><th>SKU</th><th>可用数量(件)</th><th>换标进度</th><th>操作</th></tr></thead><tbody><template v-for="source in returnLooseCandidates" :key="source.returnCartonId"><tr v-for="sku in source.skuItems" :key="sku.skuCode"><td>{{ source.cartonMark }}</td><td>{{ source.inboundOrderNo }}</td><td>{{ sku.skuCode }}</td><td>{{ sku.quantity }}</td><td>{{ returnRelabelProgress(source) }}</td><td><el-button size="small" @click="labelingMode === 'sku' ? addReturnSkuBinding(source, sku.skuCode) : addReturnBoxBinding(source)">添加贴标</el-button></td></tr></template><tr v-if="!returnLooseCandidates.length"><td colspan="6">暂无可用包裹库存</td></tr></tbody></table> <table v-show="!availableSkusCollapsed" id="return-available-skus" class="warehouse-edit-table"><thead><tr><th>包裹识别码</th><th>来源退货单</th><th>SKU</th><th>可用数量(件)</th><th>操作</th></tr></thead><tbody><template v-for="source in returnLooseCandidates" :key="source.returnCartonId"><tr v-for="sku in source.skuItems" :key="sku.skuCode"><td>{{ source.cartonMark }}</td><td>{{ source.inboundOrderNo }}</td><td>{{ sku.skuCode }}</td><td>{{ sku.quantity }}</td><td><el-button size="small" :disabled="inventoryQuantity(source.availablePackages) <= 0" @click="labelingMode === 'sku' ? addReturnSkuBinding(source, sku.skuCode) : addReturnBoxBinding(source)">添加贴标</el-button></td></tr></template><tr v-if="!returnLooseCandidates.length"><td colspan="5">暂无可用包裹库存</td></tr></tbody></table>
</template> </template>
<template v-if="returnBoxCandidates.length"> <template v-if="returnBoxCandidates.length">
<div class="warehouse-section-heading"><h3>可用箱子({{ returnBoxCandidates.length }} 箱)</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableBoxesCollapsed" aria-controls="return-available-boxes" @click="availableBoxesCollapsed = !availableBoxesCollapsed">{{ availableBoxesCollapsed ? '展开明细' : '收起明细' }}</el-button></div> <div class="warehouse-section-heading"><h3>可用箱子({{ returnBoxCandidates.length }} 箱)</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableBoxesCollapsed" aria-controls="return-available-boxes" @click="availableBoxesCollapsed = !availableBoxesCollapsed">{{ availableBoxesCollapsed ? '展开明细' : '收起明细' }}</el-button></div>
<table v-show="!availableBoxesCollapsed" id="return-available-boxes" class="warehouse-edit-table"><thead><tr><th>仓库箱识别码</th><th>来源退货单</th><th>箱内 SKU</th><th>可用数量(件)</th><th>换标进度</th><th>操作</th></tr></thead><tbody><tr v-for="source in returnBoxCandidates" :key="source.returnCartonId"><td><strong>{{ source.cartonMark }}</strong></td><td>{{ source.inboundOrderNo }}</td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.skuCode }}</div></td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.quantity }}</div></td><td>{{ returnRelabelProgress(source) }}</td><td><el-button size="small" @click="addReturnBoxBinding(source)">添加贴标</el-button></td></tr><tr v-if="!returnBoxCandidates.length"><td colspan="6">暂无可用箱子库存</td></tr></tbody></table> <table v-show="!availableBoxesCollapsed" id="return-available-boxes" class="warehouse-edit-table"><thead><tr><th>仓库箱识别码</th><th>来源退货单</th><th>箱内 SKU</th><th>可用数量(件)</th><th>操作</th></tr></thead><tbody><tr v-for="source in returnBoxCandidates" :key="source.returnCartonId"><td><strong>{{ source.cartonMark }}</strong></td><td>{{ source.inboundOrderNo }}</td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.skuCode }}</div></td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.quantity }}</div></td><td><el-button size="small" :disabled="inventoryQuantity(source.availablePackages) <= 0" @click="addReturnBoxBinding(source)">添加贴标</el-button></td></tr><tr v-if="!returnBoxCandidates.length"><td colspan="5">暂无可用箱子库存</td></tr></tbody></table>
</template> </template>
</div> </div>
<div class="warehouse-labeling-mode"> <div class="warehouse-labeling-mode">
@@ -40,9 +40,9 @@ test('贴标申请移除操作项下方的解释文字,保留必要状态', ()
test('贴标申请可从当前客户仍有库存的入仓单或退货单选择来源', () => { test('贴标申请可从当前客户仍有库存的入仓单或退货单选择来源', () => {
assert.match(view, /海外操作仓(中转仓)/) assert.match(view, /海外操作仓(中转仓)/)
assert.match(view, /选择有库存的入仓单或退货单/) assert.match(view, /选择入仓单或退货单/)
assert.match(view, /quantityUnit: candidate\.returnCartonId \? '件' : '箱'/) assert.match(view, /quantityUnit: candidate\.returnCartonId \? '件' : '箱'/)
assert.match(view, /当前中转仓暂无可用库存/) assert.match(view, /暂无匹配的可选库存/)
assert.match(view, /relabelInboundOptions/) assert.match(view, /relabelInboundOptions/)
assert.match(view, /listRelabelInventoryCandidates\(form\.warehouse\)/) assert.match(view, /listRelabelInventoryCandidates\(form\.warehouse\)/)
assert.match(api, /relabel-applications\/inventory-candidates/) assert.match(api, /relabel-applications\/inventory-candidates/)
@@ -0,0 +1,54 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import ts from 'typescript'
const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8')
const source = view.slice(view.indexOf('function returnSourceNos'), view.indexOf('const loading = ref(false)'))
function setup(candidates) {
const output = ts.transpileModule(source + '\nexports.options = relabelInboundOptions; exports.visible = visibleRelabelInboundOptions; exports.label = relabelOptionLabel', { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText
const exports = {}, show = { value: false }, search = { value: '' }, selected = { value: '' }
new Function('exports', 'computed', 'relabelCandidates', 'showOccupiedRelabelInventory', 'relabelInventorySearch', 'selectedRelabelInboundOrderId', 'inventoryQuantity', output)(exports, fn => ({ get value() { return fn() } }), { value: candidates }, show, search, selected, value => Math.max(0, Number(value) || 0))
return { ...exports, show, search, selected }
}
test('occupied return keeps stock facts but remains hidden when searched', () => {
const context=setup([{ returnCartonId:'C', inboundOrderNo:'RT-1', availablePackages:0, stockQuantity:10, inventoryStatus:'RELABEL_PENDING', occupiedRelabelNo:'RL-1' }])
assert.equal(context.visible.value.length,0)
context.search.value='RT-1'; assert.equal(context.visible.value.length,0)
const label=context.label(context.options.value[0]); assert.match(label,/在仓 1 包裹\/箱 · 10 件/); assert.equal(label,'退货单 RT-1|在仓 1 包裹/箱 · 10 件'); assert.match(context.options.value[0].reasons.join(';'),/换标占用 · RL-1/)
context.search.value=''; assert.equal(context.visible.value.length,0)
})
test('merged empty source adds history reason without inflating physical stock count', () => {
const context=setup([{ returnCartonId:'OLD', inboundOrderNo:'RT-1', availablePackages:0, stockQuantity:0, inventoryStatus:'MERGED', mergedIntoCartonMarks:['NEW'] },{ returnCartonId:'NEW', inboundOrderNo:'RT-1', availablePackages:10, stockQuantity:10, inventoryStatus:'AVAILABLE' }])
assert.equal(context.options.value[0].units,1); assert.equal(context.options.value[0].stockQuantity,10); assert.equal(context.options.value[0].availablePackages,10)
assert.match(context.options.value[0].reasons.join(';'),/历史部分转入 NEW(不代表当前库存位置)/)
})
test('missing stock projection stays unknown and outbound history does not count in-warehouse units', () => {
const context=setup([{returnCartonId:'C',inboundOrderNo:'RT-1',availablePackages:0},{returnCartonId:'D',inboundOrderNo:'RT-2',availablePackages:0,stockQuantity:0,inventoryStatus:'OUTBOUNDED'}])
assert.match(context.label(context.options.value[0]),/待查询/)
assert.equal(context.options.value[1].units,0);assert.match(context.options.value[1].reasons.join(';'),/已出库/)
})
test('fully outbound orders stay hidden even in history search; mixed remaining stock marks partial outbound', () => {
const context=setup([{returnCartonId:'D',inboundOrderNo:'RT-D',availablePackages:0,stockQuantity:0,inventoryStatus:'OUTBOUNDED'}, {returnCartonId:'A',inboundOrderNo:'RT-P',availablePackages:0,stockQuantity:0,inventoryStatus:'OUTBOUNDED'}, {returnCartonId:'B',inboundOrderNo:'RT-P',availablePackages:2,stockQuantity:2,inventoryStatus:'AVAILABLE'}])
context.show.value=true
assert.deepEqual(context.visible.value.map(item=>item.inboundOrderNo),['退货单 RT-P'])
context.search.value='RT-D';assert.equal(context.visible.value.some(item=>item.inboundOrderNo==='退货单 RT-D'),false)
assert.equal(context.label(context.options.value[1]),'退货单 RT-P|在仓 1 包裹/箱 · 2 件|部分出库')
})
test('partially shipped contents within one physical box show partial outbound', () => {
const context=setup([{returnCartonId:'B',inboundOrderNo:'RT-P',availablePackages:3,stockQuantity:3,inventoryStatus:'AVAILABLE',hasOutbound:true}])
assert.match(context.label(context.options.value[0]),/3 件|部分出库$/)
})
test('available box and parcel lists remove zero-quantity sources and zero SKU rows', () => {
const block=view.slice(view.indexOf('const availableReturnCandidates'),view.indexOf('const requiresBothTotalFiles'))
const output=ts.transpileModule(block+'\nexports.boxes=returnBoxCandidates; exports.parcels=returnLooseCandidates',{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.CommonJS}}).outputText
const exports={}
const sources=[{sourceType:'RETURN_CARTON',cartonMark:'EMPTY',availablePackages:0,skuItems:[{skuCode:'A',quantity:0}]},{sourceType:'RETURN_CARTON',cartonMark:'BOX',availablePackages:3,skuItems:[{skuCode:'A',quantity:0},{skuCode:'B',quantity:3}]},{sourceType:'RETURN_PARCEL',cartonMark:'P0',availablePackages:0},{sourceType:'RETURN_PARCEL',cartonMark:'P1',availablePackages:2,skuItems:[{skuCode:'C',quantity:2}]}]
new Function('exports','computed','selectedReturnCandidates','inventoryQuantity',output)(exports,fn=>({get value(){return fn()}}),{value:sources},value=>Math.max(0,Number(value)||0))
assert.deepEqual(exports.boxes.value.map(item=>item.cartonMark),['BOX'])
assert.deepEqual(exports.boxes.value[0].skuItems,[{skuCode:'B',quantity:3}])
assert.deepEqual(exports.parcels.value.map(item=>item.cartonMark),['P1'])
assert.doesNotMatch(view,/显示占用及历史单据|showOccupiedRelabelInventory/)
})