diff --git a/src/modules/overseas-warehouse/api/returnInventory.ts b/src/modules/overseas-warehouse/api/returnInventory.ts index 8daf616..4a0ecd7 100644 --- a/src/modules/overseas-warehouse/api/returnInventory.ts +++ b/src/modules/overseas-warehouse/api/returnInventory.ts @@ -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) { } 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[] } diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index 3a78ddb..b9c35da 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -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 diff --git a/src/modules/overseas-warehouse/views/ReturnInventoryView.vue b/src/modules/overseas-warehouse/views/ReturnInventoryView.vue index 7a936de..6148b41 100644 --- a/src/modules/overseas-warehouse/views/ReturnInventoryView.vue +++ b/src/modules/overseas-warehouse/views/ReturnInventoryView.vue @@ -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)[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
{{ cartonDetail.cartonMark }}{{ warehouseLabel(cartonDetail.overseasWarehouseId) }} · 退货箱明细
{{ cartonStatusLabel(cartonDetail.status) }}打印箱唛

箱号概览

来源退货单
{{ cartonDetail.sourceReturnNo || '—' }}
签收批次
{{ cartonDetail.sourceBatchNo || '—' }}
SKU 种类
{{ cartonDetail.skuTypes }}
箱内总件数
{{ cartonDetail.totalQuantity }}
箱规
{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}
重量
{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}
换标进度
{{ returnRelabelStatusLabel(cartonDetail.relabelStatus) }}
实际完成(件)
{{ cartonDetail.relabelledQuantity ?? '—' }}
处理中占用(件)
{{ cartonDetail.relabelReservedQuantity ?? '—' }}
签收时间
{{ formatInventoryTime(cartonDetail.receivedAt) }}

箱内 SKU

+

SKU 换标明细

diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 915bf02..0117fcb 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -1562,15 +1562,15 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { -

仓库识别码用于定位实物,不代表商品已换标。部分数量换标须由仓库合箱为独立新箱,源包裹/箱余量保留,下游按新箱出库;整包裹可未确认仓库识别标签即出库。

+

仓库识别码用于定位实物,不代表商品已换标。SKU 换标完成后扣减旧 SKU、增加新 SKU,包裹/箱总件数不变;已换标件数不可重复申请,剩余未换标件数可继续选择。客户确认前新 SKU 保持占用。

diff --git a/src/modules/overseas-warehouse/views/returnRelabelSources.test.mjs b/src/modules/overseas-warehouse/views/returnRelabelSources.test.mjs index 481f117..ef3546c 100644 --- a/src/modules/overseas-warehouse/views/returnRelabelSources.test.mjs +++ b/src/modules/overseas-warehouse/views/returnRelabelSources.test.mjs @@ -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('', 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'/) + } +})