From dcdaf1438c1fdc9e0aa5badd60a7300ed2271301 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Mon, 19 Oct 2026 15:59:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/discardDocuments.test.mjs | 38 +++++++++++++++++ .../utils/discardDocuments.ts | 38 +++++++++++++++++ .../views/WarehouseServiceView.vue | 42 +++++++------------ .../views/warehouseDisposalForm.test.mjs | 5 ++- 4 files changed, 93 insertions(+), 30 deletions(-) create mode 100644 src/modules/overseas-warehouse/utils/discardDocuments.test.mjs create mode 100644 src/modules/overseas-warehouse/utils/discardDocuments.ts diff --git a/src/modules/overseas-warehouse/utils/discardDocuments.test.mjs b/src/modules/overseas-warehouse/utils/discardDocuments.test.mjs new file mode 100644 index 0000000..042799f --- /dev/null +++ b/src/modules/overseas-warehouse/utils/discardDocuments.test.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' +import ts from 'typescript' + +const compiled = ts.transpileModule(readFileSync(new URL('./discardDocuments.ts', import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText +const { groupDiscardDocuments } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`) + +const returned = (id, warehouse = 'WH', document = 'TT-BYWL-261019152551') => ({ + orderId: `return:${id}`, returnCartonId: id, sourceType: 'RETURN_CARTON', overseasWarehouseId: warehouse, + inboundCode: document, inboundOrderNo: document, availablePackages: 5, cartonSelectionSupported: true, + cartons: [{ cartonId: id, cartonMark: `BOX-${id}`, skus: [{ skuCode: 'A', availablePackages: 5 }] }], skus: [{ skuCode: 'A', availablePackages: 5 }], +}) + +test('同单两箱合并成一个选项,可用量为两箱而非十件,保留原始提交身份', () => { + const sources = [returned('1'), returned('2')] + const result = groupDiscardDocuments(sources) + assert.equal(result.length, 1) + assert.equal(result[0].availablePackages, 2) + assert.deepEqual(result[0].sources, sources) + assert.equal(result[0].cartonSelectionSupported, true) +}) + +test('不同单据或操作仓独立展示,混装SKU不重复计算物理箱', () => { + assert.equal(groupDiscardDocuments([returned('1'), returned('2', 'OTHER'), returned('3', 'WH', 'TT-OTHER')]).length, 3) + const source = { ...returned('1'), sourceType: 'INVENTORY', orderId: 'inventory:1', inventoryPositionId: '1', availablePackages: 10 } + const second = { ...source, orderId: 'inventory:2', inventoryPositionId: '2', cartons: source.cartons } + assert.equal(groupDiscardDocuments([source, second])[0].availablePackages, 1) +}) + +test('缺少箱级记录时合并仓位可用箱数并保持全部废弃明细', () => { + const first = { ...returned('1'), sourceType: 'INVENTORY', orderId: 'inventory:1', cartonSelectionSupported: false, availablePackages: 3 } + const second = { ...first, orderId: 'inventory:2', availablePackages: 4 } + const result = groupDiscardDocuments([first, second])[0] + assert.equal(result.availablePackages, 7) + assert.equal(result.cartonSelectionSupported, false) + assert.equal(result.sources.length, 2) +}) diff --git a/src/modules/overseas-warehouse/utils/discardDocuments.ts b/src/modules/overseas-warehouse/utils/discardDocuments.ts new file mode 100644 index 0000000..8e876b8 --- /dev/null +++ b/src/modules/overseas-warehouse/utils/discardDocuments.ts @@ -0,0 +1,38 @@ +import type { DiscardCandidate } from '../api/warehouseServices' + +export interface DiscardDocument extends DiscardCandidate { + sources: DiscardCandidate[] +} + +/** 下拉按来源单据和操作仓汇总;原始库存身份保留给选箱与提交。 */ +export function groupDiscardDocuments(candidates: DiscardCandidate[]): DiscardDocument[] { + const groups = new Map() + for (const candidate of candidates) { + const documentNo = candidate.inboundOrderNo || candidate.inboundCode + const key = JSON.stringify([candidate.overseasWarehouseId, candidate.sourceType, documentNo || candidate.orderId]) + let document = groups.get(key) + if (!document) { + document = { ...candidate, orderId: key, sources: [], availablePackages: 0 } + groups.set(key, document) + } + document.sources.push(candidate) + } + for (const document of groups.values()) { + const boxes = new Set() + let untrackedPackages = 0 + for (const source of document.sources) { + if (source.sourceType === 'RETURN_CARTON' && source.returnCartonId) { + // 退货候选的 availablePackages 是箱内 SKU 件数,一条退货箱只计一箱。 + boxes.add(`RETURN_CARTON:${source.returnCartonId}`) + } else if (source.cartonSelectionSupported && source.cartons?.length) { + for (const box of source.cartons) boxes.add(`${source.sourceType}:${box.cartonId}`) + } else { + untrackedPackages += source.availablePackages + } + } + document.availablePackages = boxes.size + untrackedPackages + document.cartonSelectionSupported = document.sources.every(source => source.cartonSelectionSupported) + document.cartonSelectionBlockReason = document.sources.find(source => !source.cartonSelectionSupported)?.cartonSelectionBlockReason + } + return [...groups.values()] +} diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index fd9d1d2..f7d5af4 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -12,6 +12,7 @@ import { prepareTransferCartonLabels } from '../utils/transferCartonPrinting' import CompletionConfirmationDialog from '../components/CompletionConfirmationDialog.vue' import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue' import { returnRelabelCandidate } from '../utils/returnRelabelCandidates' +import { groupDiscardDocuments } from '../utils/discardDocuments' import { createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel, getTransferCartonLabel, submitTransfer, updateTransfer, increaseTransferForecastCartons, getTransferSourceCartons, type TransferSourceCarton, @@ -73,8 +74,8 @@ async function refreshAfterCompletion(no: string) { } const canCancelDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:cancel')) const configs: Record = { - labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '关联业务单号', '海外操作仓', 'SKU 数(种)', '总箱数/包裹', '换标类型', '状态', '审核时间', '贴标完成时间', '贴标出库时间', '创建时间'] }, - transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '预计箱数', '海外仓', 'FBA 目的仓', '入仓方式', '是否换标', '换标单号', '换标状态', '状态', '审核时间', '测量时间', '出库时间', '创建时间'] }, + labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '关联业务单号', '海外操作仓', 'SKU 数(种)', '总箱数/包裹', '换标类型', '状态', '创建时间'] }, + transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '预计箱数', '海外仓', 'FBA 目的仓', '入仓方式', '是否换标', '换标单号', '换标状态', '状态', '创建时间'] }, disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '废弃出库单号', '进仓编号', '操作仓', 'SKU 汇总', '废弃数量', '预估费用', '状态', '废弃完成时间', '创建时间'] }, relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '审核时间', '测量时间', '出库时间', '创建时间'] }, } @@ -259,21 +260,11 @@ const listQueryFields = computed(() => { if (props.service === 'transfer' || props.service === 'relabel') fields.push({ key: 'destination', label: '目的 FBA 仓', type: 'input', placeholder: '请输入仓代码' }) fields.push({ key: 'status', label: '订单状态', type: 'select', options: statusOptions.value }) fields.push({ key: 'createdDates', label: '创建时间', type: 'daterange' }) - if (props.service === 'labeling') fields.push( - { key: 'reviewDates', label: '审核时间', type: 'daterange' }, - { key: 'completedDates', label: '贴标完成时间', type: 'daterange' }, - { key: 'outboundDates', label: '贴标出库时间', type: 'daterange' }, - ) if (props.service === 'relabel') fields.push( { key: 'reviewDates', label: '审核时间', type: 'daterange' }, { key: 'measuredDates', label: '测量时间', type: 'daterange' }, { key: 'outboundDates', label: '出库时间', type: 'daterange' }, ) - if (props.service === 'transfer') fields.push( - { key: 'reviewDates', label: '审核时间', type: 'daterange' }, - { key: 'completedDates', label: '测量时间', type: 'daterange' }, - { key: 'outboundDates', label: '出库时间', type: 'daterange' }, - ) return fields }) const listColumns = computed(() => config.value.columns.map((label, index) => index === 0 @@ -284,13 +275,9 @@ const listColumns = computed(() => config.value.columns.map(( const discardSelectedBoxKeys = ref([]) const discardBoxSearch = ref('') const discardCandidates = ref([]) -const discardSelectedCandidate = computed(() => discardCandidates.value.find(item => item.orderId === selectedDiscardOrderId.value)) -const discardSelectionSources = computed(() => { - const current = discardSelectedCandidate.value - if (!current) return [] - if (current.sourceType !== 'RETURN_CARTON') return [current] - return discardCandidates.value.filter(item => item.sourceType === current.sourceType && item.inboundCode === current.inboundCode && item.overseasWarehouseId === current.overseasWarehouseId) -}) +const discardDocuments = computed(() => groupDiscardDocuments(discardCandidates.value)) +const discardSelectedCandidate = computed(() => discardDocuments.value.find(item => item.orderId === selectedDiscardOrderId.value)) +const discardSelectionSources = computed(() => discardSelectedCandidate.value?.sources || []) const discardSelectableBoxes = computed(() => discardSelectionSources.value.flatMap(source => (source.cartons || []).map(box => ({ ...box, key: `${source.sourceType}:${box.cartonId}`, source })))) const discardVisibleBoxes = computed(() => discardSelectableBoxes.value.filter(box => !discardBoxSearch.value.trim() || box.cartonMark.includes(discardBoxSearch.value.trim()) || box.skus.some(sku => sku.skuCode.includes(discardBoxSearch.value.trim())))) function toggleDiscardBox(key: string, selected: boolean) { @@ -992,17 +979,17 @@ function importSelectedBindingsToDetails() { function selectDiscardInbound(orderId: string) { discardSelectedBoxKeys.value = [] discardBoxSearch.value = '' - const candidate = discardCandidates.value.find((item) => item.orderId === orderId) + const candidate = discardDocuments.value.find((item) => item.orderId === orderId) if (!candidate) { cargoRows.value = []; return } form.inboundNo = candidate.inboundCode || candidate.inboundOrderNo form.warehouse = candidate.overseasWarehouseId if (candidate.cartonSelectionSupported) { selectAllDiscardBoxes(true); return } - cargoRows.value = candidate.skus.map((sku) => ({ + cargoRows.value = candidate.sources.flatMap(source => source.skus.map((sku) => ({ id: ++cargoSequence, sku: sku.skuCode, name: sku.productName, quantity: sku.availablePackages, - availableQuantity: sku.availablePackages, sourceOrderId: candidate.orderId, sourceType: candidate.sourceType, - returnCartonId: candidate.returnCartonId, sourceInventoryPositionId: candidate.inventoryPositionId, palletNo: candidate.palletNo, cartonMark: candidate.cartonMark || '', + availableQuantity: sku.availablePackages, sourceOrderId: source.orderId, sourceType: source.sourceType, + returnCartonId: source.returnCartonId, sourceInventoryPositionId: source.inventoryPositionId, palletNo: source.palletNo, cartonMark: source.cartonMark || '', fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '', - })) + }))) } function dateTime(value: string, end = false) { return value ? `${value}T${end ? '23:59:59' : '00:00:00'}` : undefined } function row(values: Array) { return Object.fromEntries(values.map((value, index) => [String(index), value == null || value === '' ? '—' : String(value)])) } @@ -1132,7 +1119,7 @@ async function load() { total.value = Number.isFinite(Number(result.total)) ? Number(result.total) : result.records.length submittedRows.value = result.records.map((item) => { const destinationWarehouses = warehouseDisplay(item.overseasWarehouseId) - return { ...row([item.relabelNo, (item.relatedBusinessOrderNos || []).join('、') || '—', destinationWarehouses, new Set(item.skuCodes.split(',').map(code => code.trim()).filter(Boolean)).size, item.totalPackageCount, (item.operationTypes || []).map(type => ({ CARTON_LABEL: '只换箱唛', SKU_LABEL: '只换 SKU', RETURN_BULK_SKU_LABEL: '只换 SKU', CARTON_AND_SKU: 'SKU 和箱唛都换' }[type] || type)).join('、') || '—', statusLabel(item.status), formatDateTime(item.reviewedAt), formatDateTime(item.relabelCompletedAt), formatDateTime(item.outboundAt), formatDateTime(item.createdAt)]), __status: item.status, __confirmedAt: item.customerConfirmedAt || '' } + return { ...row([item.relabelNo, (item.relatedBusinessOrderNos || []).join('、') || '—', destinationWarehouses, new Set(item.skuCodes.split(',').map(code => code.trim()).filter(Boolean)).size, item.totalPackageCount, (item.operationTypes || []).map(type => ({ CARTON_LABEL: '只换箱唛', SKU_LABEL: '只换 SKU', RETURN_BULK_SKU_LABEL: '只换 SKU', CARTON_AND_SKU: 'SKU 和箱唛都换' }[type] || type)).join('、') || '—', statusLabel(item.status), formatDateTime(item.createdAt)]), __status: item.status, __confirmedAt: item.customerConfirmedAt || '' } }) } else if (props.service === 'disposal') { const result = await listDiscards({ page: page.value, size: pageSize.value, discardNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, overseasWarehouseId: filters.warehouse || undefined, status: filters.status || undefined, createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true), completedFrom: dateTime(filters.completedStart), completedTo: dateTime(filters.completedEnd, true) }) @@ -1145,7 +1132,7 @@ async function load() { const operationWarehouse = warehouseDisplay(item.operatingWarehouseId) const relabelStatus = transferRelabelStatus(item) return props.service === 'transfer' - ? { ...row([item.transferNo, item.packageCount, operationWarehouse, item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是' : '否', item.relabelNo, relabelStatus.label, statusLabel(item.status), formatDateTime(item.reviewedAt), formatDateTime(item.measuredAt), formatDateTime(item.outboundAt), formatDateTime(item.createdAt)]), __status: item.status, __statusLabel: statusLabel(item.status), __relabelStatus: relabelStatus.code, __relabelStatusLabel: relabelStatus.label } + ? { ...row([item.transferNo, item.packageCount, operationWarehouse, item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是' : '否', item.relabelNo, relabelStatus.label, statusLabel(item.status), formatDateTime(item.createdAt)]), __status: item.status, __statusLabel: statusLabel(item.status), __relabelStatus: relabelStatus.code, __relabelStatusLabel: relabelStatus.label } : row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.reviewedAt), formatDateTime(item.measuredAt), formatDateTime(item.outboundAt), formatDateTime(item.createdAt)]) }) } @@ -1694,7 +1681,6 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
- @@ -1718,7 +1704,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
- + diff --git a/src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs b/src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs index b5be9fb..31fb31c 100644 --- a/src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs +++ b/src/modules/overseas-warehouse/views/warehouseDisposalForm.test.mjs @@ -14,9 +14,10 @@ test('货物废弃先选择海外操作仓,再加载并选择该仓入库单', test('废弃来源包含通用仓位,选择后带出全部库存且不把选择键提交为订单ID', () => { const body = view.match(/function selectDiscardInbound\(orderId: string\) \{([\s\S]*?)\n\}/)[1] - const select = new Function('discardSelectedBoxKeys', 'discardBoxSearch', 'selectAllDiscardBoxes', 'discardCandidates', 'cargoRows', 'form', 'cargoSequence', 'orderId', body) + const select = new Function('discardSelectedBoxKeys', 'discardBoxSearch', 'selectAllDiscardBoxes', 'discardDocuments', 'cargoRows', 'form', 'cargoSequence', 'orderId', body) const cargoRows = { value: [] }; const form = {} - select({ value: [] }, { value: '' }, () => {}, { value: [{ orderId: 'inventory:42', sourceType: 'INVENTORY', inventoryPositionId: '42', overseasWarehouseId: '99', inboundCode: 'WT-1', skus: [{ skuCode: 'A', productName: '商品', availablePackages: 100 }] }] }, cargoRows, form, 0, 'inventory:42') + const source = { orderId: 'inventory:42', sourceType: 'INVENTORY', inventoryPositionId: '42', overseasWarehouseId: '99', inboundCode: 'WT-1', skus: [{ skuCode: 'A', productName: '商品', availablePackages: 100 }] } + select({ value: [] }, { value: '' }, () => {}, { value: [{ ...source, orderId: 'document:WT-1', sources: [source] }] }, cargoRows, form, 0, 'document:WT-1') assert.equal(cargoRows.value[0].quantity, 100) assert.equal(cargoRows.value[0].sourceInventoryPositionId, '42') assert.equal(form.inboundNo, 'WT-1') From 666be8a80dc0453c1eb6d658f191544202b81f41 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Mon, 19 Oct 2026 16:40:30 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/ReturnOrdersView.test.mjs | 1 - .../views/ReturnOrdersView.vue | 45 +++++----- .../views/WarehouseServiceView.vue | 84 +++++++++---------- .../views/relabelAttachments.test.mjs | 2 +- .../views/returnRelabelSources.test.mjs | 1 - .../views/warehouseDisposalForm.test.mjs | 4 +- .../views/warehouseTransferForm.test.mjs | 8 +- 7 files changed, 66 insertions(+), 79 deletions(-) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index cdce72c..39b6251 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -62,7 +62,6 @@ test('新建退货预报选择海外仓后带出联系人和电话', () => { assert.match(view, /label="海外仓联系电话"/) assert.match(view, /:readonly="Boolean\(selectedOperationWarehouse\?\.contactName\)"/) assert.match(view, /:readonly="Boolean\(selectedOperationWarehouse\?\.contactPhone\)"/) - assert.match(view, /该海外仓未维护,请手动填写/) assert.match(view, /warehouseContactName: form\.warehouseContactName\.trim\(\)/) assert.match(view, /warehouseContactPhone: form\.warehouseContactPhone\.trim\(\)/) }) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index ecf1e38..586915e 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -298,25 +298,23 @@ onBeforeUnmount(clearPhotoUrls)