优化
This commit is contained in:
@@ -4,14 +4,22 @@ import type { PageResult } from './warehouseServices'
|
||||
export interface ReturnInventoryItem {
|
||||
overseasWarehouseId: string; cartonMark: string; sourceBatchNo?: string; status: string; skuCode: string
|
||||
productNameZh?: string; quantity: number; cartonSkuTypes: number; cartonTotalQuantity: number; receivedAt: string
|
||||
relabelledQuantity: number; remainingRelabelQuantity: number
|
||||
}
|
||||
|
||||
export interface ReturnInventorySummaryItem {
|
||||
overseasWarehouseId: string; skuCode: string; productNameZh?: string
|
||||
availableQuantity: number; cartonQuantity: number; updatedAt: string
|
||||
totalQuantity: number; availableQuantity: number; relabelLockedQuantity: number; cartonQuantity: number; updatedAt: string
|
||||
}
|
||||
|
||||
export interface ReturnInventoryCartonItem { skuCode: string; productNameZh?: string; quantity: number }
|
||||
export interface ReturnSkuConversion {
|
||||
relabelNo: string; cartonMark: string; oldSkuCode: string; newSkuCode: string; quantity: number
|
||||
status: 'APPLIED' | 'CONFIRMED' | 'REVERSED'; completedAt: string
|
||||
}
|
||||
export interface ReturnInventoryCartonItem {
|
||||
skuCode: string; productNameZh?: string; quantity: number; availableQuantity: number
|
||||
relabelledQuantity: number; remainingRelabelQuantity: number
|
||||
}
|
||||
|
||||
export interface ReturnInventoryCartonSummary {
|
||||
cartonMark: string; overseasWarehouseId: string; sourceBatchNo?: string; sourceReturnNo?: string; relabelStatus?: string; relabelledQuantity?: number; relabelReservedQuantity?: number; status: string
|
||||
@@ -23,6 +31,7 @@ export function listReturnInventoryCartons(params: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
export interface ReturnInventoryCartonDetail {
|
||||
skuConversions: ReturnSkuConversion[]
|
||||
cartonMark: string; overseasWarehouseId: string; sourceBatchNo?: string; sourceReturnNo?: string; relabelStatus?: string; relabelledQuantity?: number; relabelReservedQuantity?: number; status: string
|
||||
skuTypes: number; totalQuantity: number; length?: number; width?: number; height?: number; weight?: number; receivedAt: string; items: ReturnInventoryCartonItem[]
|
||||
}
|
||||
|
||||
@@ -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 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 {
|
||||
skuConversions?: import('./returnInventory').ReturnSkuConversion[]
|
||||
stockQuantity?: number; inventoryStatus?: string; occupiedRelabelNo?: string; mergedIntoCartonMarks?: string[]; hasOutbound?: boolean
|
||||
inventoryBalanceId: string; inboundOrderId: string; inboundOrderNo?: string; inboundCode?: string
|
||||
overseasWarehouseId: string; overseasWarehouseCode?: string; overseasWarehouseName?: string
|
||||
|
||||
@@ -90,14 +90,31 @@ const summaryColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 110, fixed: 'left' },
|
||||
{ key: 'warehouse', label: '海外仓', slotName: 'warehouse', minWidth: 190 },
|
||||
{ prop: 'skuCode', label: 'SKU', minWidth: 190 }, { prop: 'productNameZh', label: '中文品名', minWidth: 190 },
|
||||
{ prop: 'availableQuantity', label: '可用库存', width: 110, align: 'right' }, { prop: 'cartonQuantity', label: '所在箱数', width: 110, align: 'right' },
|
||||
{ prop: 'totalQuantity', label: '在仓总库存(件)', width: 150, align: 'right' },
|
||||
{ prop: 'availableQuantity', label: '可用库存', width: 110, align: 'right' },
|
||||
{ prop: 'relabelLockedQuantity', label: '换标占用(件)', width: 140, align: 'right' },
|
||||
{ prop: 'cartonQuantity', label: '所在箱数', width: 110, align: 'right' },
|
||||
{ key: 'time', label: '库存更新时间', slotName: 'time', width: 180 },
|
||||
]
|
||||
const cartonItemColumns: DataTableColumn[] = [
|
||||
{ prop: 'skuCode', label: 'SKU', minWidth: 200 },
|
||||
{ prop: 'productNameZh', label: '中文品名', minWidth: 240 },
|
||||
{ prop: 'quantity', label: '数量', width: 110, align: 'right', headerAlign: 'right' },
|
||||
{ prop: 'availableQuantity', label: '可用数量(件)', width: 150, align: 'right' },
|
||||
{ prop: 'relabelledQuantity', label: '已换标数量(件)', width: 160, align: 'right' },
|
||||
{ prop: 'remainingRelabelQuantity', label: '剩余可换(件)', width: 150, align: 'right' },
|
||||
]
|
||||
const conversionColumns: DataTableColumn[] = [
|
||||
{ prop: 'cartonMark', label: '包裹 / 箱识别码', minWidth: 220 },
|
||||
{ prop: 'relabelNo', label: '换标单号', minWidth: 200 },
|
||||
{ prop: 'oldSkuCode', label: '旧 SKU', minWidth: 180 },
|
||||
{ prop: 'newSkuCode', label: '新 SKU', minWidth: 180 },
|
||||
{ prop: 'quantity', label: '实际换标数量(件)', width: 170, align: 'right' },
|
||||
{ key: 'conversionStatus', label: '状态', slotName: 'conversionStatus', width: 140 },
|
||||
]
|
||||
function conversionStatusLabel(status: string) {
|
||||
return ({ APPLIED: '已换标,待确认', CONFIRMED: '已确认', REVERSED: '已退回冲回' } as Record<string, string>)[status] || '—'
|
||||
}
|
||||
const cartonSummaryColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 150, fixed: 'left' }, { prop: 'cartonMark', label: '箱号', minWidth: 220 },
|
||||
{ key: 'warehouse', label: '海外仓', slotName: 'warehouse', minWidth: 190 }, { prop: 'skuTypes', label: 'SKU 种类', width: 110, align: 'right' },
|
||||
@@ -111,6 +128,8 @@ const cartonColumns: DataTableColumn[] = [
|
||||
{ key: 'warehouse', label: '海外仓', slotName: 'warehouse', minWidth: 190 }, { prop: 'skuCode', label: 'SKU', minWidth: 180 },
|
||||
{ prop: 'productNameZh', label: '中文品名', minWidth: 180 }, { prop: 'quantity', label: '本箱数量', width: 110, align: 'right' },
|
||||
{ prop: 'cartonSkuTypes', label: 'SKU 种类', width: 110, align: 'right' }, { prop: 'cartonTotalQuantity', label: '总件数', width: 100, align: 'right' },
|
||||
{ prop: 'relabelledQuantity', label: '已换标数量(件)', width: 160, align: 'right' },
|
||||
{ prop: 'remainingRelabelQuantity', label: '剩余可换(件)', width: 150, align: 'right' },
|
||||
{ key: 'status', label: '箱状态', slotName: 'status', width: 100 }, { prop: 'sourceBatchNo', label: '签收批次', minWidth: 180 },
|
||||
{ key: 'time', label: '生成时间', slotName: 'time', width: 180 },
|
||||
]
|
||||
@@ -273,6 +292,7 @@ onMounted(() => { if (props.detailPage) void Promise.all([loadCartonDetail(), lo
|
||||
<header class="carton-detail-header"><button type="button" class="carton-detail-back" @click="returnToCartons"><ArrowLeft :size="16" />返回列表</button><div><strong>{{ cartonDetail.cartonMark }}</strong><span>{{ warehouseLabel(cartonDetail.overseasWarehouseId) }} · 退货箱明细</span></div><div class="carton-detail-actions"><el-tag effect="light">{{ cartonStatusLabel(cartonDetail.status) }}</el-tag><el-button type="primary" @click="printLabel(cartonDetail.cartonMark)"><Printer :size="16" />打印箱唛</el-button></div></header>
|
||||
<section class="carton-detail-section"><h3>箱号概览</h3><dl class="carton-detail-grid"><div><dt>来源退货单</dt><dd>{{ cartonDetail.sourceReturnNo || '—' }}</dd></div><div><dt>签收批次</dt><dd>{{ cartonDetail.sourceBatchNo || '—' }}</dd></div><div><dt>SKU 种类</dt><dd>{{ cartonDetail.skuTypes }}</dd></div><div><dt>箱内总件数</dt><dd>{{ cartonDetail.totalQuantity }}</dd></div><div><dt>箱规</dt><dd>{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}</dd></div><div><dt>重量</dt><dd>{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}</dd></div><div><dt>换标进度</dt><dd>{{ returnRelabelStatusLabel(cartonDetail.relabelStatus) }}</dd></div><div><dt>实际完成(件)</dt><dd>{{ cartonDetail.relabelledQuantity ?? '—' }}</dd></div><div><dt>处理中占用(件)</dt><dd>{{ cartonDetail.relabelReservedQuantity ?? '—' }}</dd></div><div><dt>签收时间</dt><dd>{{ formatInventoryTime(cartonDetail.receivedAt) }}</dd></div></dl></section>
|
||||
<section class="carton-detail-section"><h3>箱内 SKU</h3><DataTable :data="cartonDetail.items" :columns="cartonItemColumns" row-key="skuCode" empty-text="暂无箱内 SKU" /></section>
|
||||
<section class="carton-detail-section"><h3>SKU 换标明细</h3><DataTable :data="cartonDetail.skuConversions" :columns="conversionColumns" empty-text="暂无 SKU 换标记录"><template #conversionStatus="{ row }">{{ conversionStatusLabel(row.status) }}</template></DataTable></section>
|
||||
</template>
|
||||
<div v-else class="carton-detail-error" role="alert"><strong>{{ detailError || '箱号详情加载失败' }}</strong><el-button @click="loadCartonDetail">重新加载</el-button></div>
|
||||
</article>
|
||||
|
||||
@@ -1562,15 +1562,15 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
</section>
|
||||
</div>
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"><input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||||
<p v-if="isReturnRelabelSource" class="warehouse-list-notice">仓库识别码用于定位实物,不代表商品已换标。部分数量换标须由仓库合箱为独立新箱,源包裹/箱余量保留,下游按新箱出库;整包裹可未确认仓库识别标签即出库。</p>
|
||||
<p v-if="isReturnRelabelSource" class="warehouse-list-notice">仓库识别码用于定位实物,不代表商品已换标。SKU 换标完成后扣减旧 SKU、增加新 SKU,包裹/箱总件数不变;已换标件数不可重复申请,剩余未换标件数可继续选择。客户确认前新 SKU 保持占用。</p>
|
||||
<div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
|
||||
<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>
|
||||
<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>
|
||||
<table v-show="!availableSkusCollapsed" id="return-available-skus" class="warehouse-edit-table"><thead><tr><th>包裹识别码</th><th>来源退货单</th><th>SKU</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><div v-for="conversion in (source.skuConversions || []).filter(item => item.status !== 'REVERSED')" :key="`${conversion.relabelNo}-${conversion.oldSkuCode}-${conversion.newSkuCode}`">{{ conversion.oldSkuCode }} → {{ conversion.newSkuCode }}:已换 {{ conversion.quantity }} 件{{ conversion.status === 'APPLIED' ? '(待确认)' : '' }}</div><span v-if="!source.skuConversions?.some(item => item.status !== 'REVERSED')">尚未换标</span></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="6">暂无可用包裹库存</td></tr></tbody></table>
|
||||
</template>
|
||||
<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>
|
||||
<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>
|
||||
<table v-show="!availableBoxesCollapsed" id="return-available-boxes" class="warehouse-edit-table"><thead><tr><th>仓库箱识别码</th><th>来源退货单</th><th>箱内 SKU</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="conversion in (source.skuConversions || []).filter(item => item.status !== 'REVERSED')" :key="`${conversion.relabelNo}-${conversion.oldSkuCode}-${conversion.newSkuCode}`">{{ conversion.oldSkuCode }} → {{ conversion.newSkuCode }}:已换 {{ conversion.quantity }} 件{{ conversion.status === 'APPLIED' ? '(待确认)' : '' }}</div><span v-if="!source.skuConversions?.some(item => item.status !== 'REVERSED')">尚未换标</span></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="6">暂无可用箱子库存</td></tr></tbody></table>
|
||||
</template>
|
||||
</div>
|
||||
<div class="warehouse-labeling-mode">
|
||||
|
||||
@@ -18,11 +18,11 @@ test('实际完成和处理中占用分别展示,缺失投影保持未知', ()
|
||||
assert.equal(exports.progress({}), '—|实际完成 — 件|处理中占用 — 件')
|
||||
assert.equal(exports.progress({ relabelStatus: 'PROCESSING', relabelledQuantity: 0, relabelReservedQuantity: 4 }), '处理中|实际完成 0 件|处理中占用 4 件')
|
||||
})
|
||||
test('退货 SKU 申请按实物箱选择,来源标识只读,部分数量独立合箱', () => {
|
||||
test('退货 SKU 申请按实物箱选择,来源标识只读,部分数量转换库存', () => {
|
||||
assert.match(view, /item.id === rows\[0\].sourceRowId/)
|
||||
assert.match(view, /row.sourceRowId !== source\?\.id/)
|
||||
assert.match(view, /:readonly="isReturnRelabelSource"/)
|
||||
assert.match(view, /部分数量换标须由仓库合箱为独立新箱/)
|
||||
assert.match(view, /SKU 换标完成后扣减旧 SKU、增加新 SKU/)
|
||||
assert.match(view, /labelingMode.value === 'sku' \? !skuTotalFile.value : !cartonTotalFile.value/)
|
||||
})
|
||||
|
||||
@@ -63,3 +63,16 @@ test('包裹箱唛添加保持一个实物并去重,三种类型不再禁用',
|
||||
assert.doesNotMatch(view, /returnSkuOnly|散件库存只能贴 SKU/)
|
||||
assert.match(view, /labelingMode === 'sku' \? addReturnSkuBinding\(source, sku.skuCode\) : addReturnBoxBinding\(source\)/)
|
||||
})
|
||||
|
||||
test('部分换标明细显示实物来源、旧新SKU、实际件数和待确认状态', () => {
|
||||
for (const id of ['return-available-skus', 'return-available-boxes']) {
|
||||
const start = view.indexOf('id="' + id + '"')
|
||||
const table = view.slice(start, view.indexOf('</table>', start))
|
||||
assert.match(table, /SKU 换标结果/)
|
||||
assert.match(table, /剩余可换(件)/)
|
||||
assert.match(table, /conversion.oldSkuCode/)
|
||||
assert.match(table, /conversion.newSkuCode/)
|
||||
assert.match(table, /conversion.quantity/)
|
||||
assert.match(table, /item.status !== 'REVERSED'/)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user