From 9b58c12012c073f7c45cba723198b1c9a59654d2 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 16 Oct 2026 11:42:59 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/warehouseServices.ts | 7 +- .../CompletionConfirmationDialog.test.mjs | 30 ++++++- .../CompletionConfirmationDialog.vue | 55 +++++++++--- .../utils/returnReceiptDisplay.test.mjs | 2 + .../utils/returnReceiptDisplay.ts | 2 +- .../views/WarehouseServiceView.vue | 83 +++++++++++++------ .../views/inventoryTransferForm.test.mjs | 41 +++++++++ .../views/warehouseTransferForm.test.mjs | 6 +- 8 files changed, 180 insertions(+), 46 deletions(-) create mode 100644 src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index 2c86df0..e488686 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -45,7 +45,7 @@ export interface TransferCreate { inboundCode?: string; sourceLastlegOrderIds?: string[]; operatingWarehouseId: string; fbaWarehouseCode?: string; destinationType?: 'NONE' | 'FBA' | 'PRIVATE' destinationWarehouseId?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string - deliveryAddress: string; packageCount: number; volumeCbm: number; deliveryMethod: string; estimatedArrivalAt: string; estimatedShipAt?: string + deliveryAddress?: string; packageCount: number; volumeCbm: number; deliveryMethod?: string; estimatedArrivalAt?: string; estimatedShipAt?: string fbaShipmentNo?: string; trackingNo?: string; fbaBoxNos?: string; remark?: string; needRelabel: boolean; idempotencyKey: string labelDeadline?: string; pickupContactName?: string; pickupContactPhone?: string; relabelContactName?: string; relabelContactPhone?: string; relabelChargeAmount?: number; relabelDeliveryMethod?: RelabelDeliveryMethod items: Array<{ skuCode?: string; productNameZh?: string; quantity: number; packageCount: number; shippingMark?: string }> @@ -142,3 +142,8 @@ export interface DiscardPhoto { fileId: string; palletNo: string; fileName: stri export function downloadDiscardPhoto(discardNo: string, fileId: string) { return apiBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/photos/files/${encodeURIComponent(fileId)}/download` }) } + +export interface TransferSourceCarton { cartonMark: string; productNameZh?: string; quantity?: number; destinationWarehouseCode?: string; destinationWarehouseName?: string; items: Array<{skuCode?: string; quantity?: number}> } +export function getTransferSourceCartons(operatingWarehouseId: string, inboundCode: string) { + return apiRequest({method:'GET',url:'/merchant/v1/last-leg/warehouse-transfers/inventory-source-cartons',params:{operatingWarehouseId,inboundCode}}) +} diff --git a/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.test.mjs b/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.test.mjs index bc8c078..5ab1bd9 100644 --- a/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.test.mjs +++ b/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.test.mjs @@ -14,6 +14,9 @@ function harness(kind = 'disposal', overrides = {}) { const api = { getDiscard: async () => ({ summary: { status: 'PDA_COMPLETED' }, photos: [{ fileId: '101' }] }), getRelabel: async () => ({ application: { status: 'CUSTOMER_CONFIRM_PENDING' }, completionEvidence: { photoFileIds: ['101'] } }), + getTransfer: async () => ({ status: 'WAIT_CUSTOMER_CONFIRMATION', measurementPhotos: [{ fileId: '101' }], cartons: [{ cartonMark: 'BOX-1', status: 'COUNTED', weight: 5 }], pallets: [{ palletNo: 'PLT-1', grossWeightKg: 5 }] }), + downloadTransferMeasurementPhoto: async () => ({}), + confirmTransferMeasurement: async no => calls.push(['transfer', no]), downloadDiscardPhoto: async () => ({}), downloadRelabelCompletionPhoto: async () => ({}), confirmDiscardCompletion: async no => calls.push(['discard', no]), confirmRelabelCompletion: async no => calls.push(['relabel', no]), ...overrides, @@ -24,11 +27,11 @@ function harness(kind = 'disposal', overrides = {}) { useAuthStore: () => ({ hasPermission: () => true }), ElMessage: { success() {}, error() {} }, URL: { createObjectURL: () => 'blob:photo', revokeObjectURL: url => revoked.push(url) }, ...api, } - const state = new Function(...Object.keys(env), js + '\nreturn { loadEvidence, loadPhoto, submit, clear, checked, canConfirm, urls, failed, error, busy };')(...Object.values(env)) + const state = new Function(...Object.keys(env), js + '\nreturn { loadEvidence, loadPhoto, submit, clear, checked, canConfirm, urls, failed, error, busy, measurement };')(...Object.values(env)) return { ...state, props, calls, events, revoked } } -for (const kind of ['disposal', 'labeling']) { +for (const kind of ['disposal', 'labeling', 'transfer']) { test(`${kind}: opening loads photos without confirming; only reviewed evidence can be submitted`, async () => { const state = harness(kind) await state.loadEvidence() @@ -37,12 +40,33 @@ for (const kind of ['disposal', 'labeling']) { state.checked.value = true assert.equal(state.canConfirm.value, true) await state.submit() - assert.deepEqual(state.calls, [[kind === 'disposal' ? 'discard' : 'relabel', 'CD/1']]) + assert.deepEqual(state.calls, [[kind === 'transfer' ? 'transfer' : kind === 'disposal' ? 'discard' : 'relabel', 'CD/1']]) assert.deepEqual(state.events, [['confirmed', 'CD/1'], ['update:modelValue', false]]) state.clear(); assert.deepEqual(state.revoked, ['blob:photo']) }) } +test('transfer confirmation reads current measurements and blocks missing, failed or outdated evidence', async () => { + const state = harness('transfer', { downloadTransferMeasurementPhoto: async () => { throw Error('offline') } }) + await state.loadEvidence() + assert.equal(state.measurement.value.pallets[0].grossWeightKg, 5) + assert.equal(state.measurement.value.cartons[0].cartonMark, 'BOX-1') + state.checked.value = true + await state.submit() + assert.equal(state.canConfirm.value, false) + assert.deepEqual(state.calls, []) + for (const detail of [ + { status: 'WAIT_CUSTOMER_CONFIRMATION', measurementPhotos: [] }, + { status: 'COMPLETED', measurementPhotos: [{ fileId: '101' }] }, + { status: 'WAIT_CUSTOMER_CONFIRMATION', customerConfirmedAt: '2026-10-16', measurementPhotos: [{ fileId: '101' }] }, + ]) { + const invalid = harness('transfer', { getTransfer: async () => detail }) + await invalid.loadEvidence(); invalid.checked.value = true; await invalid.submit() + assert.equal(invalid.canConfirm.value, false) + assert.deepEqual(invalid.calls, []) + } +}) + test('photo failure blocks confirmation and supports retry', async () => { let reject = true const state = harness('disposal', { downloadDiscardPhoto: async () => { if (reject) throw Error('offline'); return {} } }) diff --git a/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.vue b/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.vue index 057cd26..e2fe982 100644 --- a/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.vue +++ b/src/modules/overseas-warehouse/components/CompletionConfirmationDialog.vue @@ -2,12 +2,20 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue' import { ElMessage } from 'element-plus' import { useAuthStore } from '@/modules/auth/public' -import { confirmDiscardCompletion, confirmRelabelCompletion, downloadDiscardPhoto, downloadRelabelCompletionPhoto, getDiscard, getRelabel, type DiscardPhoto } from '../api/warehouseServices' +import { confirmDiscardCompletion, confirmRelabelCompletion, confirmTransferMeasurement, downloadDiscardPhoto, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, getDiscard, getRelabel, getTransfer, type DiscardPhoto } from '../api/warehouseServices' -const props = defineProps<{ modelValue: boolean; kind: 'disposal' | 'labeling'; documentNo: string }>() +const props = defineProps<{ modelValue: boolean; kind: 'disposal' | 'labeling' | 'transfer'; documentNo: string }>() const emit = defineEmits<{ 'update:modelValue': [value: boolean]; confirmed: [documentNo: string] }>() const auth = useAuthStore() -const title = computed(() => props.kind === 'disposal' ? '废弃确认' : '贴标完成确认') +const title = computed(() => props.kind === 'transfer' ? '确认测量数据' : props.kind === 'disposal' ? '废弃确认' : '贴标完成确认') +interface MeasurementEvidence { + status?: string + customerConfirmedAt?: string + cartons?: Array<{ cartonMark: string; status: string; length?: number; width?: number; height?: number; weight?: number }> + pallets?: Array<{ palletNo?: string; sequenceNo?: number; cartonCount?: number; lengthCm?: number; widthCm?: number; heightCm?: number; grossWeightKg?: number; volumeCbm?: number }> + measurementPhotos?: Array<{ fileId: string | number }> +} +const measurement = ref(null) const loading = ref(false) const busy = ref(false) const error = ref('') @@ -21,18 +29,19 @@ let generation = 0 const previewUrls = computed(() => fileIds.value.map(id => urls.value[id]).filter(Boolean)) const canConfirm = computed(() => eligible.value && checked.value && !loading.value && !busy.value && !error.value && fileIds.value.length > 0 && fileIds.value.every(id => Boolean(urls.value[id])) - && auth.hasPermission(`merchant:overseas-warehouse:${props.kind === 'disposal' ? 'discard' : 'relabel'}:confirm`)) + && auth.hasPermission(`merchant:overseas-warehouse:${props.kind === 'transfer' ? 'transfer' : props.kind === 'disposal' ? 'discard' : 'relabel'}:confirm`)) function clear() { generation++ Object.values(urls.value).forEach(URL.revokeObjectURL) urls.value = {}; failed.value = {}; photoLoading.value = {}; fileIds.value = []; checked.value = false; eligible.value = false + measurement.value = null } async function loadPhoto(fileId: string) { if (photoLoading.value[fileId]) return const currentGeneration = generation; const no = props.documentNo; const kind = props.kind photoLoading.value[fileId] = true; failed.value[fileId] = false try { - const blob = kind === 'disposal' ? await downloadDiscardPhoto(no, fileId) : await downloadRelabelCompletionPhoto(no, fileId) + const blob = kind === 'transfer' ? await downloadTransferMeasurementPhoto(no, fileId) : kind === 'disposal' ? await downloadDiscardPhoto(no, fileId) : await downloadRelabelCompletionPhoto(no, fileId) if (generation !== currentGeneration || !props.modelValue) return if (urls.value[fileId]) URL.revokeObjectURL(urls.value[fileId]) urls.value[fileId] = URL.createObjectURL(blob) @@ -43,7 +52,13 @@ async function loadEvidence() { clear(); error.value = ''; loading.value = true const currentGeneration = generation try { - if (props.kind === 'disposal') { + if (props.kind === 'transfer') { + const detail = await getTransfer(props.documentNo) as MeasurementEvidence + if (generation !== currentGeneration) return + measurement.value = detail + eligible.value = detail.status === 'WAIT_CUSTOMER_CONFIRMATION' && !detail.customerConfirmedAt + fileIds.value = (detail.measurementPhotos || []).map(photo => String(photo.fileId)) + } else if (props.kind === 'disposal') { const detail = await getDiscard(props.documentNo) as { summary?: { status?: string }; photos?: DiscardPhoto[] } if (generation !== currentGeneration) return eligible.value = detail.summary?.status === 'PDA_COMPLETED' @@ -56,7 +71,7 @@ async function loadEvidence() { } fileIds.value = [...new Set(fileIds.value)] if (!eligible.value) error.value = '当前单据已确认或尚未完成 PDA 作业,请刷新列表核对。' - else if (!fileIds.value.length) error.value = '暂无 PDA 完工作业照片,暂不能确认。' + else if (!fileIds.value.length) error.value = props.kind === 'transfer' ? '暂无 PDA 测量打托照片,暂不能确认。' : '暂无 PDA 完工作业照片,暂不能确认。' await Promise.all(fileIds.value.map(loadPhoto)) } catch (e) { if (generation === currentGeneration) error.value = e instanceof Error ? e.message : '作业照片加载失败' } finally { if (generation === currentGeneration) loading.value = false } @@ -65,9 +80,10 @@ async function submit() { if (!canConfirm.value) return busy.value = true try { - if (props.kind === 'disposal') await confirmDiscardCompletion(props.documentNo) + if (props.kind === 'transfer') await confirmTransferMeasurement(props.documentNo) + else if (props.kind === 'disposal') await confirmDiscardCompletion(props.documentNo) else await confirmRelabelCompletion(props.documentNo) - ElMessage.success(props.kind === 'disposal' ? '废弃已确认,单据已完结' : '贴标已确认,订单进入后续出库流程') + ElMessage.success(props.kind === 'transfer' ? '仓库实测数据已确认' : props.kind === 'disposal' ? '废弃已确认,单据已完结' : '贴标已确认,订单进入后续出库流程') emit('confirmed', props.documentNo) emit('update:modelValue', false) } catch (e) { ElMessage.error(e instanceof Error ? e.message : '确认失败,请重试') } @@ -80,7 +96,22 @@ onBeforeUnmount(clear) diff --git a/src/modules/overseas-warehouse/utils/returnReceiptDisplay.test.mjs b/src/modules/overseas-warehouse/utils/returnReceiptDisplay.test.mjs index 842cf08..b9d6bc3 100644 --- a/src/modules/overseas-warehouse/utils/returnReceiptDisplay.test.mjs +++ b/src/modules/overseas-warehouse/utils/returnReceiptDisplay.test.mjs @@ -39,6 +39,8 @@ test('箱子在签收箱号明细展示一次,真实托盘和缺少箱号明 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' }), '') + assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK-FUW3Q9N0B8RL' }), '') + assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK-FUW3Q9N0B8RL', trackingNo: ' SF123 ' }), 'SF123') assert.equal(exports.receiptTrackingNo({ unitType: 'PARCEL', unitCode: 'PK0123456789ABCDEF', trackingNo: ' SF123 ' }), 'SF123') assert.equal(exports.receiptTrackingNo({ unitType: 'PALLET', unitCode: 'PLT123' }), '') }) diff --git a/src/modules/overseas-warehouse/utils/returnReceiptDisplay.ts b/src/modules/overseas-warehouse/utils/returnReceiptDisplay.ts index 007cffd..25a137d 100644 --- a/src/modules/overseas-warehouse/utils/returnReceiptDisplay.ts +++ b/src/modules/overseas-warehouse/utils/returnReceiptDisplay.ts @@ -18,7 +18,7 @@ export function receiptTrackingNo(receipt: Pick auth.hasPermission('merchant:overseas- const canConfirmDiscard = computed(() => auth.hasPermission('merchant:overseas-warehouse:discard:confirm')) const completionConfirmOpen = ref(false) const completionConfirmNo = ref('') -const completionConfirmKind = ref<'disposal' | 'labeling'>('disposal') -function openCompletionConfirmation(no: string, kind: 'disposal' | 'labeling') { +const completionConfirmKind = ref<'disposal' | 'labeling' | 'transfer'>('disposal') +function openCompletionConfirmation(no: string, kind: 'disposal' | 'labeling' | 'transfer') { if (!no) return completionConfirmNo.value = no; completionConfirmKind.value = kind; completionConfirmOpen.value = true } @@ -294,6 +294,22 @@ const availableSkusCollapsed = ref(false) const relabelCandidatesLoading = ref(false) const transferInventoryCandidates = ref([]) const selectedTransferInventory = ref('') +const inventoryTransfer = computed(() => props.service === 'transfer' && Boolean(selectedTransferInventory.value)) +const transferSourceCartons = ref([]) +const transferSourceLoading = ref(false) +const transferSourceError = ref('') +let transferSourceRequest = 0 +async function loadTransferSourceCartons() { + const request = ++transferSourceRequest + transferSourceCartons.value = []; transferSourceError.value = '' + if (!inventoryTransfer.value) return + transferSourceLoading.value = true + try { + const cartons = await getTransferSourceCartons(form.warehouse, selectedTransferInventory.value) + if (request === transferSourceRequest) transferSourceCartons.value = cartons + } catch (error) { if (request === transferSourceRequest) transferSourceError.value = error instanceof Error ? error.message : '原单箱明细加载失败' } + finally { if (request === transferSourceRequest) transferSourceLoading.value = false } +} // Snowflake IDs exceed JavaScript's safe integer range. Keep them as strings // all the way to JSON so the backend receives the exact source order ID. const selectedTransferSourceOrderIds = ref([]) @@ -545,6 +561,8 @@ async function openEdit(transferNo: string) { const sourceInboundCode = String(detail.inboundCode || '') if (transferInventoryOptions.value.some((option) => option.value === sourceInboundCode)) { selectedTransferInventory.value = sourceInboundCode + await loadTransferSourceCartons() + if (transferDestinationType.value === 'NONE') transferDestinationType.value = 'FBA' form.inboundNo = sourceInboundCode selectedTransferSourceOrderIds.value = (detail.sourceOrders || []) .map((item) => String(item.lastlegOrderId || '')).filter(Boolean) @@ -1058,7 +1076,7 @@ function transferRelabelStatusTagType(status: string): StatusTagType { function deliveryMethodLabel(value: string) { const labels: Record = { TRUCK: '卡车派送', EXPRESS: '快递派送', AMAZON_PICKUP: '亚马逊自提', CUSTOMER_PICKUP: '客户自提', - WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓', + INVENTORY_TRANSFER: '库存转运', WAREHOUSE_PICKUP: '仓库上门提货', CUSTOMER_DELIVERY: '客户送货到仓', // Keep the legacy value readable for historical applications created before the four-way split. PICKUP: '仓库自提', } @@ -1160,7 +1178,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') { if (props.service === 'relabel' && (!form.destination || !form.address || !form.deliveryMethod || !form.estimatedArrivalAt || !form.fbaShipmentNo || form.volumeCbm <= 0)) { ElMessage.warning('请填写完整的派送信息、体积、预计到达时间和 FBA 货件号'); return } - if (props.service === 'transfer' && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || (form.deliveryMethod === 'WAREHOUSE_PICKUP' && (!form.pickupContact || !form.pickupPhone)))) { + if (props.service === 'transfer' && !inventoryTransfer.value && (!form.warehouse || !form.deliveryMethod || !form.address || !form.estimatedArrivalAt || (form.deliveryMethod === 'WAREHOUSE_PICKUP' && (!form.pickupContact || !form.pickupPhone)))) { const missing = [!form.warehouse && '海外仓', !form.deliveryMethod && '送货方式', !form.address && '地址', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupContact && '提货联系人', form.deliveryMethod === 'WAREHOUSE_PICKUP' && !form.pickupPhone && '联系人电话', !form.estimatedArrivalAt && '预计入仓时间'].filter(Boolean).join('、') ElMessage.warning(`请填写:${missing || '必填信息'}`); return } @@ -1168,6 +1186,8 @@ async function submit(action: 'draft' | 'submit' = 'submit') { !Number.isSafeInteger(row.quantity) || row.quantity < 1 || !Number.isSafeInteger(row.expectedPackageCount) || Number(row.expectedPackageCount) < 1))) { ElMessage.warning('请填写每个 SKU 的数量(件)和预计箱数,均须为正整数'); return } + if (inventoryTransfer.value && (transferSourceLoading.value || transferSourceError.value)) { ElMessage.warning('请等待原单箱明细加载成功后再保存'); return } + if (inventoryTransfer.value && transferDestinationType.value === 'NONE') { ElMessage.warning('库存转运请选择 FBA 仓或私人仓'); return } if (props.service === 'transfer' && transferDestinationType.value === 'FBA' && !form.destination) { ElMessage.warning('请选择 FBA 目的仓'); return } @@ -1181,6 +1201,9 @@ async function submit(action: 'draft' | 'submit' = 'submit') { if (action === 'submit' && !requiresBothTotalFiles.value && !cartonTotalFile.value && !skuTotalFile.value) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return } const relabelRows = transferRelabelRows.value const allMarks = relabelRows.flatMap(transferCustomerMarks) + if (inventoryTransfer.value && allMarks.some(mark => !transferSourceCartons.value.some(box => box.cartonMark === mark))) { + ElMessage.error('当前箱子不存在或不属于当前订单'); return + } if (!relabelRows.length || relabelRows.some((row) => transferCustomerMarks(row).length !== row.quantity)) { ElMessage.warning('每个换标明细的客户箱唛数量必须与换标数量一致'); return } if (new Set(allMarks.map((mark) => mark.toUpperCase())).size !== allMarks.length) { ElMessage.warning('客户箱唛号不能重复'); return } if (allMarks.join(',').length > 2000 || relabelRows.some((row) => transferCustomerMarks(row).join(',').length > 255)) { ElMessage.warning('客户箱唛内容过长,请减少单次提交箱数或缩短箱唛号'); return } @@ -1232,7 +1255,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') { const transferRemark = [form.remark, form.relabel && form.requirement ? `换标要求:${form.requirement}` : ''].filter(Boolean).join(';') const operationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU' const selectedFba = fbaWarehouses.value.find((warehouse) => warehouse.warehouseCode === form.destination) - const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: form.address, packageCount, volumeCbm: 0, deliveryMethod: form.deliveryMethod, estimatedArrivalAt: form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, quantity: item.quantity, packageCount: Number(item.expectedPackageCount), shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined } + const payload: TransferCreate = { operatingWarehouseId: primaryWarehouse, destinationType: transferDestinationType.value, fbaWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseId: transferDestinationType.value === 'FBA' ? String(selectedFba?.id || '') || undefined : undefined, destinationWarehouseCode: transferDestinationType.value === 'FBA' ? form.destination || undefined : undefined, destinationWarehouseName: transferDestinationType.value === 'FBA' ? selectedFba?.warehouseName : undefined, privateDestinationAddress: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationAddress : undefined, privateDestinationPostalCode: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPostalCode : undefined, privateDestinationContactName: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationContact : undefined, privateDestinationContactPhone: transferDestinationType.value === 'PRIVATE' ? form.privateDestinationPhone : undefined, deliveryAddress: inventoryTransfer.value ? undefined : form.address, packageCount, volumeCbm: 0, deliveryMethod: inventoryTransfer.value ? undefined : form.deliveryMethod, estimatedArrivalAt: inventoryTransfer.value ? undefined : form.estimatedArrivalAt, estimatedShipAt: form.estimatedShipAt || undefined, fbaShipmentNo: transferDestinationType.value === 'FBA' ? form.fbaShipmentNo || undefined : undefined, trackingNo: !inventoryTransfer.value && form.deliveryMethod === 'CUSTOMER_DELIVERY' ? form.trackingNo || undefined : undefined, fbaBoxNos: customerCartonMarks.length ? customerCartonMarks.join(',') : undefined, remark: transferRemark || undefined, needRelabel: props.service === 'relabel' || form.relabel, labelDeadline: form.relabel ? form.labelDeadline : undefined, pickupContactName: !inventoryTransfer.value && form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupContact : undefined, pickupContactPhone: !inventoryTransfer.value && form.deliveryMethod === 'WAREHOUSE_PICKUP' ? form.pickupPhone : undefined, relabelContactName: form.relabel ? form.relabelContact : undefined, relabelContactPhone: form.relabel ? form.relabelPhone : undefined, relabelDeliveryMethod: form.relabel && labelingMode.value !== 'sku' ? form.relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, quantity: item.quantity, packageCount: Number(item.expectedPackageCount), shippingMark: transferCustomerMarks(item).join(',') || undefined })), relabelItems: form.relabel ? transferRelabelRows.value.map((item) => ({ operationType, customerCartonMarks: transferCustomerMarks(item), originalSku: item.sku, newSku: item.newSkuCode || undefined, quantity: item.quantity, fbaCartonMark: item.fbaCartonMark || undefined, destinationWarehouseCode: transferRelabelDefaultDestination.value || undefined, remark: item.remark || undefined })) : undefined } if (props.service === 'transfer' && selectedTransferInventory.value) { ;(payload as TransferCreate).inboundCode = selectedTransferInventory.value ;(payload as TransferCreate).sourceLastlegOrderIds = selectedTransferSourceOrderIds.value @@ -1297,9 +1320,7 @@ async function loadDetailPage() { async function confirmTransferMeasurements() { const transferNo = String(selectedDetail.value?.transferNo || selectedDetail.value?.['0'] || '') if (!transferNo) return - try { await ElMessageBox.confirm('请核对 PDA 回传的箱数、尺寸、重量和打托数据。', '确认测量数据', { confirmButtonText: '确认数据', cancelButtonText: '取消' }) } catch { return } - try { selectedDetail.value = await confirmTransferMeasurement(transferNo) as ServiceDetail; ElMessage.success('仓库实测数据已确认') } - catch (error) { ElMessage.error(error instanceof Error ? error.message : '确认失败') } + openCompletionConfirmation(transferNo, 'transfer') } async function printTransferCartons(transferNo: string) { if (props.service !== 'transfer') return @@ -1320,9 +1341,7 @@ function clearCompletionPhotos() { for (const key of Object.keys(completionPhotoErrors)) delete completionPhotoErrors[key] } async function confirmTransferFromList(transferNo: string) { - try { await ElMessageBox.confirm('请核对 PDA 回传的箱数、尺寸、重量和打托数据。', '确认测量数据', { confirmButtonText: '确认数据', cancelButtonText: '取消' }) } catch { return } - try { await confirmTransferMeasurement(transferNo); ElMessage.success('仓库实测数据已确认'); await load() } - catch (error) { ElMessage.error(error instanceof Error ? error.message : '确认失败') } + openCompletionConfirmation(transferNo, 'transfer') } async function loadCompletionPhoto(fileId: string) { const relabelNo = selectedRelabelDetail.value?.application.relabelNo @@ -1476,7 +1495,10 @@ const transferInventoryOptions = computed(() => { async function selectTransferInventory(value: string) { selectedTransferInventory.value = value || '' selectedTransferSourceOrderIds.value = [] - if (!value) return + transferSourceCartons.value = []; transferSourceError.value = ''; ++transferSourceRequest + if (!value) { form.inboundNo = ''; cargoRows.value = [createCargoRow()]; transferSourceLoading.value = false; return } + if (transferDestinationType.value === 'NONE') transferDestinationType.value = 'FBA' + void loadTransferSourceCartons() form.inboundNo = value // The inventory-candidates response already contains the merchant-visible // SKU/name/package data. Do not call the platform-only last-leg orders API @@ -1489,14 +1511,14 @@ async function selectTransferInventory(value: string) { const sku = String(candidate.skuCode || '') const current = grouped.get(sku) const quantity = inventoryQuantity(candidate.availablePackages) - if (current) current.quantity += quantity - else grouped.set(sku, { ...createCargoRow(), sku, name: candidate.productNameZh || '', quantity, availableQuantity: quantity }) + if (current) { current.quantity += quantity; current.expectedPackageCount = Number(current.expectedPackageCount || 0) + quantity } + else grouped.set(sku, { ...createCargoRow(), sku, name: candidate.productNameZh || '', quantity, expectedPackageCount: quantity, availableQuantity: quantity }) } cargoRows.value = [...grouped.values()] try { const historical = await listRelabelHistoricalOrders(form.warehouse, value) const sourceIds = historical.map((item) => String(item.id || '')).filter(Boolean) - selectedTransferSourceOrderIds.value = sourceIds + if (selectedTransferInventory.value === value) selectedTransferSourceOrderIds.value = sourceIds } catch (error) { ElMessage.error(error instanceof Error ? error.message : '库存来源订单加载失败') } @@ -1615,11 +1637,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { -
*入仓方式仓库上门提货客户送货到仓
- - - -
+
*入仓方式仓库上门提货客户送货到仓
+ + + +
海外仓收货信息{{ operationWarehouseAddress || '该海外仓暂未维护收货地址' }}
收货联系人{{ selectedOperationWarehouse?.warehouse.contactName || '未维护' }}
@@ -1636,9 +1658,9 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { - + -
目的去向暂未确定FBA 仓私人仓
+
目的去向暂未确定FBA 仓私人仓
@@ -1719,8 +1741,17 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { -

1货物明细

打印唛头添加行清空
-
商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}中文品名{{ props.service === 'transfer' ? '' : ' *' }}可用箱数FNSKU / 箱唛SKU 数量(件)*{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *操作
{{ row.availableQuantity }} 箱
{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}
+
+

原单箱明细 共 {{ transferSourceCartons.length }} 箱 · 只读

+ 重新加载 +
+ + +
箱号商品 SKU中文品名箱内数量(件)原目的仓
{{ box.cartonMark }}{{ box.items.map(item => item.skuCode || '—').join(' / ') }}{{ box.productNameZh || '—' }}{{ box.quantity ?? '未记录' }}{{ [box.destinationWarehouseCode, box.destinationWarehouseName].filter(Boolean).join(' · ') || '未指定' }}
{{ transferSourceError ? '加载失败,请重试' : '原库存单据未记录逐箱明细,不虚构箱号;按原库存汇总转运' }}
+
商品 SKU中文品名库存箱数原目的仓
{{ row.sku || '—' }}{{ row.name || '—' }}{{ row.expectedPackageCount }}{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}
+
+

1货物明细

打印唛头添加行清空
+
商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}中文品名{{ props.service === 'transfer' ? '' : ' *' }}可用箱数FNSKU / 箱唛SKU 数量(件)*{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *操作
{{ row.availableQuantity }} 箱
{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}
diff --git a/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs b/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs new file mode 100644 index 0000000..6025146 --- /dev/null +++ b/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs @@ -0,0 +1,41 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { transpileModule, ScriptTarget } from 'typescript' + +const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8') +function setup() { + const body = view.slice(view.indexOf('async function selectTransferInventory('), view.indexOf('function selectTransferInboundMethod(')) + const code = transpileModule(body, { compilerOptions: { target: ScriptTarget.ES2022 } }).outputText + const ctx = { selectedTransferInventory: {value:''}, selectedTransferSourceOrderIds:{value:[]}, + transferSourceCartons:{value:[{cartonMark:'OLD'}]}, transferSourceError:{value:'OLD'}, transferSourceLoading:{value:true}, + transferDestinationType:{value:'NONE'}, form:{warehouse:'WH',inboundNo:''}, + transferInventoryCandidates:{value:[{inboundCode:'IN',skuCode:'SKU',productNameZh:'货品',availablePackages:2}, + {inboundCode:'IN',skuCode:'SKU',availablePackages:3},{inboundCode:'OTHER',skuCode:'OTHER',availablePackages:99}]}, + cargoRows:{value:[]}, inventoryQuantity:Number, createCargoRow:()=>({expectedPackageCount:1}), + loadTransferSourceCartons:async()=>{}, listRelabelHistoricalOrders:async()=>[{id:'1234567890123456789'}], + ElMessage:{error(message){throw Error(message)}} } + const select = new Function(...Object.keys(ctx), 'let transferSourceRequest=0;'+code+'; return selectTransferInventory')(...Object.values(ctx)) + return {ctx,select} +} +test('inventory source populates full immutable cargo counts without mixing other inbound orders',async()=>{ + const {ctx,select}=setup(); await select('IN') + assert.equal(ctx.transferDestinationType.value,'FBA') + assert.equal(ctx.cargoRows.value.length,1) + assert.equal(ctx.cargoRows.value[0].expectedPackageCount,5) + assert.equal(ctx.cargoRows.value[0].quantity,5) + assert.deepEqual(ctx.selectedTransferSourceOrderIds.value,['1234567890123456789']) + await select('') + assert.equal(ctx.form.inboundNo,'') + assert.deepEqual(ctx.selectedTransferSourceOrderIds.value,[]) + assert.deepEqual(ctx.transferSourceCartons.value,[]) +}) +test('inventory view hides inbound fields and editing actions and shows original carton destination',()=>{ + assert.match(view,/v-if="props.service === 'transfer' && !inventoryTransfer"[^>]*>\*<\/i>入仓方式/) + assert.match(view,//) + const panel=view.slice(view.indexOf('
',view.indexOf('
}) test('上门提货不提交物流号,需要换标时随转运单登记申请', () => { - assert.match(view, /trackingNo:\s*form\.deliveryMethod === 'CUSTOMER_DELIVERY'\s*\?[^:]+:\s*undefined/) + assert.match(view, /trackingNo:\s*!inventoryTransfer\.value && form\.deliveryMethod === 'CUSTOMER_DELIVERY'\s*\?[^:]+:\s*undefined/) assert.match(view, /relabelItems: form\.relabel/) assert.match(view, /form\.deliveryMethod === 'CUSTOMER_DELIVERY'.*物流跟踪号/s) assert.match(view, /提货联系人/) @@ -157,8 +157,8 @@ test('上门提货不提交物流号,需要换标时随转运单登记申请', assert.match(view, /form\.relabelContact/) assert.match(view, /form\.relabelPhone/) assert.match(view, /选择海外仓后自动带出,也可手动填写/) - assert.match(view, /pickupContactName: form\.deliveryMethod === 'WAREHOUSE_PICKUP' \? form\.pickupContact : undefined/) - assert.match(view, /pickupContactPhone: form\.deliveryMethod === 'WAREHOUSE_PICKUP' \? form\.pickupPhone : undefined/) + assert.match(view, /pickupContactName: !inventoryTransfer\.value && form\.deliveryMethod === 'WAREHOUSE_PICKUP' \? form\.pickupContact : undefined/) + assert.match(view, /pickupContactPhone: !inventoryTransfer\.value && form\.deliveryMethod === 'WAREHOUSE_PICKUP' \? form\.pickupPhone : undefined/) assert.match(view, /relabelContactName: form\.relabel \? form\.relabelContact/) assert.match(api, /pickupContactName\?: string; pickupContactPhone\?: string/) }) From bb674eb3a0adf4bb5a5eeb3e3b4f31aa28e5975e Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 16 Oct 2026 13:58:08 +0800 Subject: [PATCH 2/4] =?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/WarehouseServiceView.vue | 39 ++++++++++++++----- .../views/inventoryTransferForm.test.mjs | 21 ++++++++++ .../views/warehouseTransferForm.test.mjs | 2 +- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 81691bf..773e3f9 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -1432,7 +1432,11 @@ async function loadTransferInventoryCandidates(warehouseId: string) { if (!warehouseId) return relabelCandidatesLoading.value = true try { - transferInventoryCandidates.value = await listRelabelInventoryCandidates(warehouseId) + // 共用接口包含退货历史及占用中的箱子,转运只展示当前可整箱使用的来源。 + transferInventoryCandidates.value = (await listRelabelInventoryCandidates(warehouseId)).filter(item => + item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL' + ? Number(item.cartonAvailablePackages || 0) > 0 + : Number(item.availablePackages || 0) > 0) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '当前海外仓库存来源加载失败') } finally { relabelCandidatesLoading.value = false } @@ -1483,12 +1487,18 @@ async function resubmitRejectedRelabel() { const transferInventoryOptions = computed(() => { const grouped = new Map() for (const item of transferInventoryCandidates.value) { - const value = String(item.inboundCode || '') + // 退货库存没有普通入仓码,使用带前缀的退货来源编码,避免提交时被当作普通入仓单查询。 + const returnNo = String(item.inboundOrderNo || '').trim() + const value = item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL' + ? (returnNo ? `RETURN_ORDER:${returnNo}` : '') + : String(item.inboundCode || '') if (!value) continue - const packages = inventoryQuantity(item.availablePackages) + const packages = item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL' + ? inventoryQuantity(item.cartonAvailablePackages || 0) + : inventoryQuantity(item.availablePackages) const current = grouped.get(value) if (current) current.packages += packages - else grouped.set(value, { value, label: `${item.inboundOrderNo || value}|${value}`, packages }) + else grouped.set(value, { value, label: value.startsWith('RETURN_ORDER:') ? `退货单|${returnNo}` : `${item.inboundOrderNo || value}|${value}`, packages }) } return [...grouped.values()] }) @@ -1505,17 +1515,26 @@ async function selectTransferInventory(value: string) { // here: merchants are not authorized for that endpoint (A0403), and the // transfer service can derive source orders from inboundCode when IDs are // omitted. Aggregate candidates for the selected inbound into cargo rows. - const candidates = transferInventoryCandidates.value.filter((item) => String(item.inboundCode || '') === value) + const isReturnSource = value.startsWith('RETURN_ORDER:') + const returnNo = isReturnSource ? value.slice('RETURN_ORDER:'.length) : '' + const candidates = transferInventoryCandidates.value.filter((item) => isReturnSource + ? (item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL') && String(item.inboundOrderNo || '') === returnNo + : String(item.inboundCode || '') === value) const grouped = new Map() for (const candidate of candidates) { - const sku = String(candidate.skuCode || '') - const current = grouped.get(sku) - const quantity = inventoryQuantity(candidate.availablePackages) - if (current) { current.quantity += quantity; current.expectedPackageCount = Number(current.expectedPackageCount || 0) + quantity } - else grouped.set(sku, { ...createCargoRow(), sku, name: candidate.productNameZh || '', quantity, expectedPackageCount: quantity, availableQuantity: quantity }) + const candidateRows = isReturnSource + ? (candidate.cartonSkuItems?.length ? candidate.cartonSkuItems : candidate.skuItems || []).map(item => ({ sku: item.skuCode, name: candidate.productNameZh || '', quantity: inventoryQuantity(item.quantity), packageCount: inventoryQuantity(candidate.cartonAvailablePackages || 0) })) + : [{ sku: String(candidate.skuCode || ''), name: candidate.productNameZh || '', quantity: inventoryQuantity(candidate.availablePackages), packageCount: inventoryQuantity(candidate.availablePackages) }] + for (const row of candidateRows) { + if (!row.sku) continue + const current = grouped.get(row.sku) + if (current) { current.quantity += row.quantity; current.expectedPackageCount = Number(current.expectedPackageCount || 0) + row.packageCount; current.availableQuantity = Number(current.availableQuantity || 0) + row.quantity } + else grouped.set(row.sku, { ...createCargoRow(), sku: row.sku, name: row.name, quantity: row.quantity, expectedPackageCount: row.packageCount, availableQuantity: row.quantity }) + } } cargoRows.value = [...grouped.values()] try { + if (isReturnSource) return const historical = await listRelabelHistoricalOrders(form.warehouse, value) const sourceIds = historical.map((item) => String(item.id || '')).filter(Boolean) if (selectedTransferInventory.value === value) selectedTransferSourceOrderIds.value = sourceIds diff --git a/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs b/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs index 6025146..cf0d64a 100644 --- a/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs +++ b/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs @@ -39,3 +39,24 @@ test('inventory view hides inbound fields and editing actions and shows original assert.match(view,/!inventoryTransfer\.value && \(!form.warehouse/) assert.match(view,/estimatedArrivalAt: inventoryTransfer.value \? undefined : form.estimatedArrivalAt/) }) + +test('仓储转运来源下拉包含退货单并使用独立来源编码', () => { + assert.match(view, /RETURN_ORDER:\$\{returnNo\}/) + assert.match(view, /退货单|\$\{returnNo\}/) + assert.match(view, /item\.sourceType === 'RETURN_CARTON' \|\| item\.sourceType === 'RETURN_PARCEL'/) + assert.match(view, /isReturnSource[\s\S]*item\.sourceType === 'RETURN_CARTON'/) +}) + +test('退货来源带出实物 SKU 数量并跳过普通历史订单查询', async () => { + const { ctx, select } = setup() + ctx.transferInventoryCandidates.value = [{ sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', + cartonAvailablePackages: 1, skuItems: [{ skuCode: 'SKU', quantity: 1 }], + cartonSkuItems: [{ skuCode: 'SKU', quantity: 6 }] }, + { sourceType: 'RETURN_CARTON', inboundOrderNo: 'OTHER', cartonAvailablePackages: 1, + cartonSkuItems: [{ skuCode: 'FOREIGN', quantity: 99 }] }] + await select('RETURN_ORDER:RET-1') + assert.equal(ctx.cargoRows.value.length, 1) + assert.equal(ctx.cargoRows.value[0].quantity, 6) + assert.equal(ctx.cargoRows.value[0].expectedPackageCount, 1) + assert.deepEqual(ctx.selectedTransferSourceOrderIds.value, []) +}) diff --git a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs index 9b67d49..6892ed3 100644 --- a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs +++ b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs @@ -341,7 +341,7 @@ test('仓储转运可在新建页直接提交,也可保存草稿后编辑提 test('编辑仓储转运时加载库存转运来源候选', () => { assert.match(view, /async function loadTransferInventoryCandidates\(warehouseId: string\)/) assert.match(view, /if \(props\.service === 'transfer' && form\.warehouse\) \{[\s\S]*await loadTransferInventoryCandidates\(form\.warehouse\)/) - assert.match(view, /transferInventoryCandidates\.value = await listRelabelInventoryCandidates\(warehouseId\)/) + assert.match(view, /transferInventoryCandidates\.value = \(await listRelabelInventoryCandidates\(warehouseId\)\)\.filter/) assert.match(view, /transferInventoryOptions\.value\.some\(\(option\) => option\.value === sourceInboundCode\)/) assert.match(view, /selectedTransferInventory\.value = sourceInboundCode/) assert.match(view, /detail\.sourceOrders \|\| \[\]/) From 3d73f1c82d82dbe166d12fc4bfd100a4c2d3f38c Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 16 Oct 2026 14:22:00 +0800 Subject: [PATCH 3/4] =?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/WarehouseServiceView.vue | 32 +++++++++++++++---- .../views/inventoryTransferForm.test.mjs | 24 ++++++++++++++ .../views/warehouseTransferForm.test.mjs | 2 +- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 773e3f9..21cccf8 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -305,6 +305,24 @@ async function loadTransferSourceCartons() { if (!inventoryTransfer.value) return transferSourceLoading.value = true try { + // 退货候选本身就是逐箱库存,使用与下拉一致的实物箱数据,不能按普通入仓快照回查。 + if (selectedTransferInventory.value.startsWith('RETURN_ORDER:')) { + const returnNo = selectedTransferInventory.value.slice('RETURN_ORDER:'.length) + transferSourceCartons.value = transferInventoryCandidates.value + .filter(item => (item.sourceType === 'RETURN_CARTON' || item.sourceType === 'RETURN_PARCEL') + && String(item.inboundOrderNo || '').trim() === returnNo && Number(item.cartonAvailablePackages || 0) > 0) + .filter(item => Boolean(item.cartonMark)) + .map(item => { + const contents = item.cartonSkuItems?.length ? item.cartonSkuItems : item.skuItems || [] + return { cartonMark: item.cartonMark!, productNameZh: item.productNameZh, + quantity: contents.reduce((total, sku) => total + Number(sku.quantity || 0), 0), + destinationWarehouseCode: item.destinationFbaWarehouseCode, + destinationWarehouseName: item.destinationFbaWarehouseName, + items: contents.map(sku => ({ skuCode: sku.skuCode, quantity: Number(sku.quantity || 0) })) } + }) + if (!transferSourceCartons.value.length) throw new Error('当前退货单没有可转运箱,请刷新库存后重试') + return + } const cartons = await getTransferSourceCartons(form.warehouse, selectedTransferInventory.value) if (request === transferSourceRequest) transferSourceCartons.value = cartons } catch (error) { if (request === transferSourceRequest) transferSourceError.value = error instanceof Error ? error.message : '原单箱明细加载失败' } @@ -1653,7 +1671,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) { - +
*入仓方式仓库上门提货客户送货到仓
@@ -1775,7 +1793,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {

2换标申请

-
+
换标类型只换箱唛只换 SKU 标签SKU 和箱唛都换
@@ -1792,11 +1810,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
-
待导入绑定关系

可只勾选本次需要换标的部分,也可以全选后一次导入。

全选导入到明细
选择关系类型客户箱唛号旧 SKU新 SKU数量新 FBA 箱唛
{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}{{ item.cartonMark }}{{ item.originalSku || '—' }}{{ item.newSku || '—' }}{{ item.quantity }}{{ item.fbaCartonMark || '—' }}
+
待导入绑定关系

可只勾选本次需要换标的部分,也可以全选后一次导入。

全选导入到明细
选择关系类型仓库唛头旧 SKU新 SKU数量新 FBA 箱唛
{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}{{ item.cartonMark }}{{ item.originalSku || '—' }}{{ item.newSku || '—' }}{{ item.quantity }}{{ item.fbaCartonMark || '—' }}
4绑定明细
添加 SKU 明细清空
-
客户箱唛号 *旧 SKU新 FBA 箱唛 *备注操作
暂无绑定明细,请点击“添加换标明细”
-
客户箱唛号 *旧 SKU *新 SKU *换标数量(件)*备注操作
暂无绑定明细,请点击“添加 SKU 明细”
-
客户箱唛号 *旧 SKU *新 SKU *换标数量(件)*新 FBA 箱唛 *备注操作
暂无绑定明细,请点击“添加 SKU 明细”
+
仓库唛头 *旧 SKU新 FBA 箱唛 *备注操作
暂无绑定明细,请点击“添加换标明细”
+
仓库唛头 *旧 SKU *新 SKU *换标数量(件)*备注操作
暂无绑定明细,请点击“添加 SKU 明细”
+
仓库唛头 *旧 SKU *新 SKU *换标数量(件)*新 FBA 箱唛 *备注操作
暂无绑定明细,请点击“添加 SKU 明细”
重置保存草稿提交订单提交{{ config.title }}
@@ -1860,7 +1878,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {

换标箱级去向

-
换标类型客户箱唛号旧 SKU新 SKU新 FBA 箱唛实际 FBA 目的仓数量备注
{{ binding.operationType === 'CARTON_AND_SKU' ? 'SKU 和箱唛都换' : binding.operationType === 'SKU_LABEL' ? '只换 SKU' : '只换箱唛' }}{{ binding.customerCartonMark || '—' }}{{ binding.originalSku || '—' }}{{ binding.newSku || '—' }}{{ binding.fbaCartonMark || '—' }}{{ binding.operationType === 'SKU_LABEL' ? '沿用原箱去向' : warehouseDisplay(binding.destinationWarehouseCode) }}{{ binding.quantity ?? '—' }}{{ binding.remark || '—' }}
暂无换标绑定明细
+
换标类型仓库唛头旧 SKU新 SKU新 FBA 箱唛实际 FBA 目的仓数量备注
{{ binding.operationType === 'CARTON_AND_SKU' ? 'SKU 和箱唛都换' : binding.operationType === 'SKU_LABEL' ? '只换 SKU' : '只换箱唛' }}{{ binding.customerCartonMark || '—' }}{{ binding.originalSku || '—' }}{{ binding.newSku || '—' }}{{ binding.fbaCartonMark || '—' }}{{ binding.operationType === 'SKU_LABEL' ? '沿用原箱去向' : warehouseDisplay(binding.destinationWarehouseCode) }}{{ binding.quantity ?? '—' }}{{ binding.remark || '—' }}
暂无换标绑定明细

换标完成与出库

diff --git a/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs b/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs index cf0d64a..c7220c2 100644 --- a/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs +++ b/src/modules/overseas-warehouse/views/inventoryTransferForm.test.mjs @@ -4,6 +4,30 @@ import { readFileSync } from 'node:fs' import { transpileModule, ScriptTarget } from 'typescript' const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8') + +test('退货箱明细与来源下拉使用同一份逐箱库存,不请求普通入仓箱接口', async () => { + const body = view.slice(view.indexOf('async function loadTransferSourceCartons('), view.indexOf('// Snowflake IDs')) + const code = transpileModule(body, { compilerOptions: { target: ScriptTarget.ES2022 } }).outputText + const ctx = { transferSourceCartons: { value: [] }, transferSourceError: { value: '' }, + transferSourceLoading: { value: false }, inventoryTransfer: { value: true }, + selectedTransferInventory: { value: 'RETURN_ORDER:RET-1' }, form: { warehouse: 'WH' }, + transferInventoryCandidates: { value: [ + { sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', cartonMark: 'BOX-1', cartonAvailablePackages: 1, + cartonSkuItems: [{ skuCode: 'A', quantity: 3 }, { skuCode: 'B', quantity: 2 }] }, + { sourceType: 'RETURN_PARCEL', inboundOrderNo: 'RET-1', cartonMark: 'BOX-2', cartonAvailablePackages: 1, + cartonSkuItems: [{ skuCode: 'A', quantity: 1 }] }, + { sourceType: 'RETURN_CARTON', inboundOrderNo: 'OTHER', cartonMark: 'FOREIGN', cartonAvailablePackages: 1 }, + { sourceType: 'RETURN_CARTON', inboundOrderNo: 'RET-1', cartonMark: 'OCCUPIED', cartonAvailablePackages: 0 }, + ] }, getTransferSourceCartons: () => { throw Error('不应调用普通来源箱查询') } } + const load = new Function(...Object.keys(ctx), 'let transferSourceRequest=0;' + code + ';return loadTransferSourceCartons')(...Object.values(ctx)) + await load() + assert.deepEqual(ctx.transferSourceCartons.value.map(box => box.cartonMark), ['BOX-1', 'BOX-2']) + assert.equal(ctx.transferSourceCartons.value[0].quantity, 5) + assert.equal(ctx.transferSourceCartons.value[0].items.length, 2) + assert.equal(ctx.transferSourceError.value, '') + assert.equal(ctx.transferSourceLoading.value, false) +}) + function setup() { const body = view.slice(view.indexOf('async function selectTransferInventory('), view.indexOf('function selectTransferInboundMethod(')) const code = transpileModule(body, { compilerOptions: { target: ScriptTarget.ES2022 } }).outputText diff --git a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs index 6892ed3..c55636f 100644 --- a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs +++ b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs @@ -79,7 +79,7 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换 assert.doesNotMatch(transferConfig, /进仓编号/) assert.doesNotMatch(transferSubmit, /inboundCode|sourceLastlegOrderIds/) assert.match(view, /货物明细/) - assert.match(view, /客户箱唛号 \*/) + assert.match(view, /仓库唛头 \*/) assert.match(view, /downloadTransferCartonMarkTemplate/) assert.match(view, /importTransferCartonMarks/) assert.match(view, /SKU 关联<\/strong>/) From 3d3ca053a6d583bb1037fbd76bbbebecd81800e9 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 16 Oct 2026 16:19:31 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/warehouseServices.ts | 4 ++ .../views/WarehouseServiceView.vue | 72 ++++++++++--------- .../views/warehouseTransferForm.test.mjs | 17 ++--- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index e488686..f050848 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -76,6 +76,10 @@ export function updateTransfer(transferNo: string, payload: TransferCreate) { export function submitTransfer(transferNo: string) { return apiRequest>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/submit` }) } +/** 补充既有SKU的预报箱数,原箱数快照用于并发核对与安全重试。 */ +export function increaseTransferForecastCartons(transferNo: string, payload: { expectedPackageCount: number; transferItemId: string; expectedItemPackageCount: number; packageCount: number }) { + return apiRequest>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/increase-forecast-cartons`, data: payload }) +} export function listRelabels(params: Record) { return apiRequest>({ method: 'GET', url: '/merchant/v1/last-leg/relabel-applications', params }) } diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 21cccf8..15cea5b 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -11,7 +11,7 @@ import CompletionConfirmationDialog from '../components/CompletionConfirmationDi import { returnRelabelCandidate } from '../utils/returnRelabelCandidates' import { createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel, - getTransferCartonLabel, submitTransfer, updateTransfer, getTransferSourceCartons, type TransferSourceCarton, + getTransferCartonLabel, submitTransfer, updateTransfer, increaseTransferForecastCartons, getTransferSourceCartons, type TransferSourceCarton, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel, uploadRelabelFile, listDiscards, listDiscardCandidates, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard, @@ -148,6 +148,34 @@ interface ServiceDetail extends Record { cartons?: TransferCarton[]; pallets?: TransferPallet[]; measurementPhotos?: TransferMeasurementPhoto[]; customerConfirmedAt?: string } const selectedDetail = ref(null) +// 预报箱唛在提交后即可打印,与PDA实收清点明细分别展示。 +const showForecastCartons = ref(false) +const increaseForecastOpen = ref(false) +const increasingForecast = ref(false) +const forecastSnapshot = ref(null) +const increaseForecastForm = reactive({ itemId: '', additionalCount: 1 }) +const canIncreaseForecast = computed(() => canCreate.value && selectedDetail.value?.transferNo === selectedDetail.value?.inboundCode && ['PENDING_REVIEW', 'WAIT_PDA_RECEIVING'].includes(selectedDetail.value?.status || '')) +/** 保存原预报快照,防止弹窗打开后其他设备的修改被覆盖。 */ +function openIncreaseForecast() { + if (!canIncreaseForecast.value || !selectedDetail.value) return + forecastSnapshot.value = selectedDetail.value + increaseForecastForm.itemId = String(selectedDetail.value.items?.[0]?.id || '') + increaseForecastForm.additionalCount = 1; increaseForecastOpen.value = true +} +/** 只补箱唛,SKU预报件数不随包装箱数自动改变。 */ +async function saveIncreaseForecast() { + if (increasingForecast.value || !forecastSnapshot.value?.transferNo) return + const snapshot = forecastSnapshot.value + const item = snapshot.items?.find(row => String(row.id) === increaseForecastForm.itemId) + if (!item || !Number.isSafeInteger(increaseForecastForm.additionalCount) || increaseForecastForm.additionalCount < 1) { ElMessage.warning('请选择SKU并填写新增箱数'); return } + increasingForecast.value = true + try { + const result = await increaseTransferForecastCartons(String(snapshot.transferNo), { expectedPackageCount: Number(snapshot.packageCount), transferItemId: String(item.id), expectedItemPackageCount: Number(item.packageCount), packageCount: Number(item.packageCount) + increaseForecastForm.additionalCount }) + selectedDetail.value = result as ServiceDetail; increaseForecastOpen.value = false + ElMessage.success('预报箱数已增加,新增箱唛可打印;请通知PDA刷新后继续清点') + } catch (error) { ElMessage.error(error instanceof Error ? error.message : '增加预报箱数失败') } + finally { increasingForecast.value = false } +} const showTransferCartons = ref(false) function transferCartonItem(carton: TransferCarton) { return selectedDetail.value?.items?.find((item) => carton.transferItemId != null && String(item.id) === String(carton.transferItemId)) @@ -612,36 +640,6 @@ async function selectDiscardWarehouse() { } function createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, expectedPackageCount: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } } function addCargoRow() { cargoRows.value.push(createCargoRow()) } -function escapePrintHtml(value: string) { - return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''') -} -function printTransferDraftCartonMarks() { - if (props.service !== 'transfer') return - const validRows = cargoRows.value.filter((row) => row.sku.trim() && Number(row.expectedPackageCount) > 0) - if (!validRows.length || validRows.some((row) => !Number.isInteger(Number(row.expectedPackageCount)))) { - ElMessage.warning('请先填写商品 SKU 和对应的整数箱数'); return - } - const totalCartons = validRows.reduce((sum, row) => sum + Number(row.expectedPackageCount), 0) - if (totalCartons > 500) { ElMessage.warning('单次最多打印 500 张箱唛'); return } - const batch = new Date().toISOString().replace(/\D/g, '').slice(2, 14) - const labels: Array<{ mark: string; sku: string; index: number }> = [] - let cartonIndex = 0 - for (const [rowIndex, row] of validRows.entries()) { - let marks = transferCustomerMarks(row) - if (marks.length !== Number(row.expectedPackageCount)) { - marks = Array.from({ length: Number(row.expectedPackageCount) }, (_, index) => `CT-${batch}-${String(rowIndex + 1).padStart(2, '0')}-${String(index + 1).padStart(3, '0')}`) - row.customerCartonMarks = marks.join('\n') - } - for (const mark of marks) labels.push({ mark, sku: row.sku.trim(), index: ++cartonIndex }) - } - const printWindow = window.open('', '_blank') - if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return } - printWindow.opener = null - const sections = labels.map((label) => `
仓储转运客户箱唛
${escapePrintHtml(label.mark)}
SKU:${escapePrintHtml(label.sku)}
${label.index} / ${labels.length}
`).join('') - printWindow.document.open() - printWindow.document.write(`仓储转运客户箱唛${sections}`) - printWindow.document.close(); printWindow.focus(); printWindow.print() -} function addTransferRelabelRow() { const source = cargoRows.value[Math.min(transferRelabelRows.value.length, Math.max(0, cargoRows.value.length - 1))] transferRelabelRows.value.push({ ...createCargoRow(), sku: source?.sku || '' }) @@ -1787,7 +1785,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
商品 SKU中文品名库存箱数原目的仓
{{ row.sku || '—' }}{{ row.name || '—' }}{{ row.expectedPackageCount }}{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}
-

1货物明细

打印唛头添加行清空
+

1货物明细

添加行清空
商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}中文品名{{ props.service === 'transfer' ? '' : ' *' }}可用箱数FNSKU / 箱唛SKU 数量(件)*{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *操作
{{ row.availableQuantity }} 箱
{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}
@@ -1833,6 +1831,14 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {

本次废弃箱子

入仓码SKU品名申请数量所选箱唛
{{ item.inboundCode || '—' }}{{ item.skuCode || '—' }}{{ item.productName || '—' }}{{ item.discardPackages ?? '—' }}{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}

废弃现场照片

照片加载中…
{{ formatDateTime(photo.submittedAt) }}
暂无已提交的废弃现场照片
+
+

预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)

增加预报箱数打印预报箱唛查看预报箱唛
+ + +

保留原箱唛,仅补生成新增箱唛;SKU预报件数保持不变。

+ +
+

预报与实际签收

商品 SKU中文品名预报数量(件)预计箱数实际签收件数实际签收箱数
{{ item.skuCode || '—' }}{{ item.productNameZh || '—' }}{{ item.quantity ?? '—' }}{{ item.packageCount ?? '—' }}{{ receivedItemQuantity(item.id) }}{{ receivedItemCartons(item.id).length }}
暂无货物明细
@@ -1855,7 +1861,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
测量打托现场照片
照片 {{ index + 1 }} 加载中…
仓库尚未上传测量打托照片
-

签收箱号明细({{ countedTransferCartons().length }} 箱)

打印箱唛查看箱号明细
+

签收箱号明细({{ countedTransferCartons().length }} 箱)

查看箱号明细
暂无实际签收箱,等待 PDA 清点。
diff --git a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs index c55636f..96fe8dc 100644 --- a/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs +++ b/src/modules/overseas-warehouse/views/warehouseTransferForm.test.mjs @@ -102,15 +102,10 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换 assert.doesNotMatch(view, /FNSKU \/ 箱唛<\/th>/) }) -test('仓储转运货物明细可按 SKU 和箱数打印客户预贴箱唛', () => { - assert.match(view, /printTransferDraftCartonMarks/) - assert.match(view, /请先填写商品 SKU 和对应的整数箱数/) - assert.match(view, /Array\.from\(\{ length: Number\(row\.expectedPackageCount\) \}/) - assert.match(view, /row\.customerCartonMarks = marks\.join\('\\n'\)/) - assert.match(view, /仓储转运客户箱唛/) - assert.match(view, /@click="printTransferDraftCartonMarks"/) - assert.match(view, /打印唛头/) - assert.match(view, /单次最多打印 500 张箱唛/) +test('仓储转运提交后统一打印服务端预报箱唛', () => { + assert.doesNotMatch(view, /printTransferDraftCartonMarks|CT-\$\{batch\}/) + assert.match(view, /getTransferCartonLabel\(transferNo\)/) + assert.match(view, /打印预报箱唛/) }) test('仓储转运换标资料按步骤分组展示', () => { @@ -330,7 +325,7 @@ test('仓储转运可在新建页直接提交,也可保存草稿后编辑提 assert.match(view, /await updateTransfer\(editingTransferNo\.value, payload\)/) assert.match(view, /await submitTransfer\(transferNo\)/) assert.match(view, /
]*@submit\.prevent="submit\('submit'\)"/) - assert.match(view, /selectedDetail\?\.status !== 'DRAFT'[^>]*[\s\S]*?printTransferCartons[^>]*[\s\S]*?打印箱唛/) + assert.match(view, /selectedDetail\?\.status !== 'DRAFT'[^>]*[\s\S]*?printTransferCartons[^>]*[\s\S]*?打印预报箱唛/) assert.doesNotMatch(view, /row\.__status !== 'DRAFT'.*printTransferCartons\(row\['0'\]\)/s) assert.doesNotMatch(view, /selectedDetail\?\.status === 'DRAFT'[^>]*[\s\S]*?>提交订单/) assert.match(view, /native-type="button"[^>]*:loading="saving"[^>]*@click="submit\('draft'\)"/) @@ -407,7 +402,7 @@ test('预报实收同区对照、托盘卡片换行、签收按钮紧邻标题', assert.ok(section.includes('class="warehouse-transfer-pallet-sku"')) assert.doesNotMatch(section, /打印箱唛/) const receipt = view.slice(view.indexOf('warehouse-transfer-received-card', start)).split('