diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 0d8997e6..c9274a60 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -322,6 +322,8 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('仓库修改外箱(宽)') expect(source).toContain('仓库修改外箱(高)') expect(source).toContain('saveWarehouseOuterDimensions') + expect(source).not.toContain('请至少输入一项需要修改的外箱尺寸') + expect(source).toContain(".map((value) => value == null || value === '' ? null : Number(value))") expect(source).toContain('相关数值已重新计算') expect(source).toContain('runMoreAction($event, row as UnifiedOrderResource)') expect(source).toContain(':aria-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"') @@ -334,6 +336,11 @@ describe('UnifiedOrdersView contract', () => { expect(source).not.toContain("value.replaceAll('_', ' ')") }) + it('keeps the customer order package count independent from measured inbound totals', () => { + expect(source).toContain("{ source: '客户下单数据', count: customerTotals.count") + expect(source).not.toContain("{ source: '客户下单数据', count: summary?.totalPackageCount") + }) + it('uses the inbound measurement girth formula for cargo dimensions', () => { expect(source).toContain('formatNumber(length + 2 * (width + height))') expect(source).not.toContain('formatNumber(2 * (width + height))') @@ -358,14 +365,19 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain("updateMeasurementSectionMode('actual', $event)") expect(source).not.toContain("updateMeasurementSectionMode('outer', $event)") expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(2) + expect(source).toContain('@visible-change="handleOuterModeDropdownVisibleChange($event)"') expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !can\('platform:firstleg:inbound:measure:edit'\)"/g)).toHaveLength(2) expect(source).toContain('const previousMode = measurementSectionModes.outer') expect(source).toContain('if (!saved) measurementSectionModes.outer = previousMode') - expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode)') + expect(source).toContain('const selectedSameMode = outerModeAtDropdownOpen.value === measurementSectionModes.outer') + expect(source).toContain('await nextTick()') + expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode, ruleId)') expect(source).toContain('calculatedMeasurementRow(row, measurementSectionModes.customer, true)') expect(source).toContain('calculatedMeasurementRow(source, measurementSectionModes.outer)') - expect(source).toContain('calculatedMeasurementRow(row as unknown as Record, measurementSectionModes.actual)') + expect(source).toContain('calculatedMeasurementRow(row as unknown as Record, measurementSectionModes.actual, false, false)') expect(source).toContain("return Number(hasOuterOverride && override != null && override !== '' ? override : value)") + expect(source).toContain("const hasOuterOverride = useWarehouseOuterOverrides && !customerSource") + expect(source).not.toContain("!customerSource && mode !== 'ACTUAL'") expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)') expect(source).toContain('matchedSpecificationCoefficient(chargeRule, calculationSource)') expect(source).toContain('listCustomerChargeRules(customerId, tenantId)') @@ -381,20 +393,38 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain("ElMessage.warning('外箱规数据不完整,请先保存外箱规尺寸和计算规则')") expect(source).toContain("weightSource: 'OUTER'") expect(source).toContain("volumeSource: 'OUTER'") + expect(source).not.toContain('测量审核通过后才能确认并发送费用') + expect(source).not.toContain("reviewStatus !== 'APPROVED'") + expect(source).toContain('const hasActualMeasurementData = computed(() => (measurementDetail.value?.items?.length ?? 0) > 0)') + expect(source).toContain("暂无实际测量数据,不能确认并发送费用") + expect(source).toContain(':disabled="confirmFeeSending || measurementLoading || !hasActualMeasurementData || !canSendFeeForCurrentStatus"') expect(source).toContain('selectAndSaveOuterMeasurementMode($event)') - expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode)') + expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode, ruleId)') expect(source).toContain('coefficientMode: targetIds.has(String(item.id)) ? mode') + expect(source).toContain('outerLengthCm: item.outerLengthCm') + expect(source).toContain('outerWidthCm: item.outerWidthCm') + expect(source).toContain('outerHeightCm: item.outerHeightCm') + expect(source).toContain("String(item.chargeRuleId) === String(activeRule!.id)") + expect(source).toContain('item.calculatedOuterLengthCm, item.calculatedOuterWidthCm, item.calculatedOuterHeightCm') + expect(source).toContain('if (dimensions.every((dimension) => dimension == null)) return true') + expect(source).toContain('已保存并绑定计算规则,体积已重新计算') expect(source).not.toContain('feeConfirmVolume.value = Number(summary.measurement?.actualVolumeCbm)') const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange')) expect(sendFeeSource).not.toContain('saveInboundMeasurementDraft') expect(sendFeeSource).not.toContain('updateMeasurementCalculationMode') + expect(source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()'))).not.toContain('getLatestInboundFee') }) it('restores the saved outer-carton rule and server-calculated dimensions on re-entry', () => { - expect(source).toContain("mode is MeasurementCalculationMode => mode === 'ACTUAL' || mode === 'ADD' || mode === 'MULTIPLY'") - expect(source).toContain('measurementSectionModes.outer = savedModes.length === 1 ? savedModes[0]!') + expect(source).toContain('const savedModes = [...new Set(response.items.map((item) => item.coefficientMode)') + expect(source).toContain('measurementSectionModes.outer = savedModes.length === 1') + expect(source).toContain('? savedModes[0]!') + expect(source).toContain("configuredMode === 'ADD' ? 'ADD' : 'ACTUAL'") expect(source).toContain('row.calculatedOuterLengthCm') expect(source).toContain('measurementCalculationModeForRow(source) === measurementSectionModes.outer') + expect(source).toContain("String(row.calculationRuleVersion || '').startsWith('WAREHOUSE_OUTER_OVERRIDE_')") + expect(source).toContain('warehouseOuterOverride') + expect(source).toContain('Math.abs(Number(value) - Number(recalculatedDimensions[index])) < 0.001') expect(source).toContain('lengthCm: Number(savedDimensions[0])') }) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index c5ffa007..b8ec51a0 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -63,7 +63,6 @@ import { createFeeActionIdempotencyKey, getInboundMeasurement, saveInboundMeasurementDraft, - getLatestInboundFee, InboundFeePanel, InboundMeasurementDialog, MeasurementEvidenceThumbnail, @@ -188,6 +187,7 @@ type MeasurementCalculationMode = 'ACTUAL' | 'ADD' | 'MULTIPLY' const measurementSectionModes = reactive>({ customer: 'ACTUAL', outer: 'ACTUAL', actual: 'ACTUAL', }) +const outerModeAtDropdownOpen = ref(null) const cargoImagePreviewUrls = new Set() const measurementDialogOpen = ref(false) const openWarehouseSuggestionOnMeasurementLoad = ref(false) @@ -202,7 +202,7 @@ const feeConfirmRows = computed(() => { const actualVolume = Number(measurement?.actualVolumeCbm) const customerTotals = measurementTotals(customerMeasurementRows.value) return [ - { source: '客户下单数据', count: summary?.totalPackageCount, weight: customerTotals.gross, volume: customerTotals.volume ?? summary?.expectedVolumeCbm }, + { source: '客户下单数据', count: customerTotals.count, weight: customerTotals.gross, volume: customerTotals.volume ?? summary?.expectedVolumeCbm }, { source: '外箱规数据', count: measurementTotals(outerMeasurementRows.value).count, weight: measurementTotals(outerMeasurementRows.value).gross, volume: measurementTotals(outerMeasurementRows.value).volume }, { source: '实际测量数据', count: measurementDetail.value?.actualPackageCount, weight: actualWeight, volume: actualVolume }, ] @@ -324,8 +324,20 @@ function matchedSpecificationCoefficient(rule: CustomerChargeRule, row: Record measurementChargeRules.value - .find((rule) => rule.defaultRule && rule.status === 'ACTIVE') || null) + .find((rule) => rule.defaultRule && rule.status === 'ACTIVE') + || measurementChargeRules.value.find((rule) => rule.status === 'ACTIVE') + || null) +const boundOuterMeasurementChargeRule = computed(() => { + const boundId = outerMeasurementRows.value + .map((row) => String(row.chargeRuleId || '')) + .find((id) => id) + return boundId + ? measurementChargeRules.value.find((rule) => rule.id === boundId && rule.status === 'ACTIVE') || null + : null +}) +const activeMeasurementChargeRules = computed(() => measurementChargeRules.value.filter((rule) => rule.status === 'ACTIVE')) const multiplyModeDisabled = computed(() => !measurementChargeRulesLoading.value && !activeMeasurementChargeRule.value) function roundedUp(value: number, digits: number) { @@ -337,9 +349,10 @@ function calculatedMeasurementRow( source: Record, mode: MeasurementCalculationMode, customerSource = false, -) { + useWarehouseOuterOverrides = true, +): Record { const outerOverrides = [source.outerLengthCm, source.outerWidthCm, source.outerHeightCm] - const hasOuterOverride = !customerSource && mode !== 'ACTUAL' + const hasOuterOverride = useWarehouseOuterOverrides && !customerSource && outerOverrides.some((value) => value != null && value !== '') const rawDimensions = [source.lengthCm, source.widthCm, source.heightCm].map((value, index) => { const override = outerOverrides[index] @@ -374,7 +387,7 @@ function calculatedMeasurementRow( : null const sourceUnitWeight = customerSource ? Number(source.grossWeightKg) / packageCount - : Number.isFinite(Number(source.unitWeightKg)) ? Number(source.unitWeightKg) : fallbackMeasuredUnitWeight + : Number(source.unitWeightKg) > 0 ? Number(source.unitWeightKg) : fallbackMeasuredUnitWeight const unitWeightKg = Number.isFinite(sourceUnitWeight) ? sourceUnitWeight : null const totalWeightKg = unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null return { @@ -391,11 +404,21 @@ const customerMeasurementRows = computed(() => (detail.value?.order.cargoes || [ ))) const outerMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => { const source = row as unknown as Record + const recalculated = calculatedMeasurementRow(source, measurementSectionModes.outer) const savedDimensions = [row.calculatedOuterLengthCm, row.calculatedOuterWidthCm, row.calculatedOuterHeightCm] + const recalculatedDimensions = [recalculated.lengthCm, recalculated.widthCm, recalculated.heightCm] + const warehouseOuterOverride = String(row.calculationRuleVersion || '').startsWith('WAREHOUSE_OUTER_OVERRIDE_') if (savedDimensions.every((value) => Number.isFinite(Number(value)) && Number(value) > 0) + && (warehouseOuterOverride + || savedDimensions.every((value, index) => Math.abs(Number(value) - Number(recalculatedDimensions[index])) < 0.001)) && measurementCalculationModeForRow(source) === measurementSectionModes.outer) { const packageCount = Number(row.packageCount) - const unitWeightKg = Number.isFinite(Number(row.unitWeightKg)) ? Number(row.unitWeightKg) : null + const unitWeightKg = Number(row.unitWeightKg) > 0 + ? Number(row.unitWeightKg) + : Number.isFinite(Number(measurementDetail.value?.actualGrossWeightKg)) + && Number(measurementDetail.value?.actualPackageCount) > 0 + ? Number(measurementDetail.value?.actualGrossWeightKg) / Number(measurementDetail.value?.actualPackageCount) + : null return { ...source, lengthCm: Number(savedDimensions[0]), @@ -405,11 +428,17 @@ const outerMeasurementRows = computed(() => (measurementDetail.value?.items || [ totalWeightKg: unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null, } } - return calculatedMeasurementRow(source, measurementSectionModes.outer) + return recalculated })) const actualMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => ( - calculatedMeasurementRow(row as unknown as Record, measurementSectionModes.actual) + calculatedMeasurementRow(row as unknown as Record, measurementSectionModes.actual, false, false) ))) +const hasActualMeasurementData = computed(() => (measurementDetail.value?.items?.length ?? 0) > 0) +const feeSendEditableStatuses = new Set([ + 'WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED', + 'WAIT_PRICING', 'FEE_CONFIRMING', 'WAIT_SPLIT', +]) +const canSendFeeForCurrentStatus = computed(() => feeSendEditableStatuses.has(String(detail.value?.summary?.mainStatus || ''))) function updateMeasurementSectionMode(section: MeasurementSection, mode: MeasurementCalculationMode) { if (mode === 'MULTIPLY' && !activeMeasurementChargeRule.value) { @@ -419,7 +448,7 @@ function updateMeasurementSectionMode(section: MeasurementSection, mode: Measure measurementSectionModes[section] = mode } -async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMode) { +async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMode, ruleId?: string) { if (measurementStrategySavingId.value) return const previousMode = measurementSectionModes.outer if (mode === 'MULTIPLY' && !activeMeasurementChargeRule.value) { @@ -427,10 +456,25 @@ async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMod return } measurementSectionModes.outer = mode - const saved = await updateMeasurementCalculationMode(null, mode) + const saved = await updateMeasurementCalculationMode(null, mode, ruleId) if (!saved) measurementSectionModes.outer = previousMode } +async function handleOuterModeDropdownVisibleChange(visible: boolean) { + if (visible) { + outerModeAtDropdownOpen.value = measurementSectionModes.outer + return + } + // Element Plus may emit dropdown-close before change. Wait until the change handler has + // updated the mode/saving state, otherwise one click can submit both the old and new mode. + await nextTick() + const selectedSameMode = outerModeAtDropdownOpen.value === measurementSectionModes.outer + outerModeAtDropdownOpen.value = null + if (selectedSameMode && !measurementStrategySavingId.value) { + void selectAndSaveOuterMeasurementMode(measurementSectionModes.outer) + } +} + const statusTabs = computed(() => [ { value: '' as const, label: '全部' }, ...unifiedOrderStatusOptions, @@ -842,7 +886,7 @@ function measurementCalculationModeForRow(row: Record): Measure return measuredItem?.coefficientMode === 'MULTIPLY' ? 'MULTIPLY' : measuredItem?.coefficientMode === 'ADD' ? 'ADD' : 'ACTUAL' } -async function updateMeasurementCalculationMode(row: Record | null, mode: MeasurementCalculationMode): Promise { +async function updateMeasurementCalculationMode(row: Record | null, mode: MeasurementCalculationMode, selectedRuleId?: string): Promise { const summary = detail.value?.summary const measurement = measurementDetail.value if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null || !can('platform:firstleg:inbound:measure:edit')) return false @@ -851,6 +895,9 @@ async function updateMeasurementCalculationMode(row: Record | n const targetIds = new Set(measurement.items .filter((item) => !row || (itemId ? String(item.id) === itemId : normalizedIdentifier(item.itemNo) === itemNo)) .map((item) => String(item.id))) + const targetItemNos = new Set(measurement.items + .filter((item) => targetIds.has(String(item.id))) + .map((item) => normalizedIdentifier(item.itemNo))) if (!targetIds.size) { ElMessage.warning('当前数据尚未关联实测明细,暂不能设置计算方式') return false @@ -858,7 +905,15 @@ async function updateMeasurementCalculationMode(row: Record | n measurementStrategySavingId.value = row ? (itemId || `item-${itemNo}`) : 'all' try { await Promise.all([loadMeasurementChargeRules(), loadMeasurementDimensionConfig()]) - const activeRule = measurementChargeRules.value.find((rule) => rule.defaultRule && rule.status === 'ACTIVE') + const existingRuleId = selectedRuleId || measurement.items + .map((item) => String(item.chargeRuleId || '')) + .find((id) => id) + const activeRule = measurementChargeRules.value.find((rule) => rule.id === existingRuleId && rule.status === 'ACTIVE') + || activeMeasurementChargeRule.value + if (mode === 'MULTIPLY' && selectedRuleId && !activeRule) { + ElMessage.warning('请选择当前客户已启用的计费规则') + return false + } const requiresMultiply = measurement.items.some((item) => ( targetIds.has(String(item.id)) ? mode : item.coefficientMode ) === 'MULTIPLY') @@ -870,7 +925,7 @@ async function updateMeasurementCalculationMode(row: Record | n measurementId: String(measurement.measurementId), version: measurement.version, actualGoodsName: measurement.actualGoodsName, - items: measurement.items.map((item) => ({ + items: measurement.items.filter((item) => Number(item.packageCount) > 0).map((item) => ({ inboundDetailId: item.inboundDetailId ?? undefined, boxCode: item.boxCode, itemNo: item.itemNo, @@ -884,11 +939,13 @@ async function updateMeasurementCalculationMode(row: Record | n machineNo: item.machineNo, measuredAt: item.deviceMeasuredAt, overLimitWarning: item.overLimitWarning, + // 人工外箱值是本次策略的计算基数,切换策略时必须一并保留。 outerLengthCm: item.outerLengthCm, outerWidthCm: item.outerWidthCm, outerHeightCm: item.outerHeightCm, chargeRuleId: (targetIds.has(String(item.id)) ? mode : item.coefficientMode) === 'MULTIPLY' - ? activeRule!.id + ? (targetIds.has(String(item.id)) || !measurementChargeRules.value.some((rule) => rule.id === String(item.chargeRuleId) && rule.status === 'ACTIVE') + ? activeRule!.id : item.chargeRuleId) : null, coefficientMode: targetIds.has(String(item.id)) ? mode : item.coefficientMode || 'ACTUAL', remark: item.remark, @@ -896,7 +953,24 @@ async function updateMeasurementCalculationMode(row: Record | n }) measurementLoadedKey.value = '' await loadMeasurementDetails() - ElMessage.success(`${row ? '当前数据' : '全部测量数据'}的计算方式已更新,体积已重新计算`) + const persistedItems = measurementDetail.value?.items + .filter((item) => targetItemNos.has(normalizedIdentifier(item.itemNo))) || [] + const persisted = persistedItems.length === targetItemNos.size && persistedItems.every((item) => ( + item.coefficientMode === mode + && (mode !== 'MULTIPLY' || String(item.chargeRuleId) === String(activeRule!.id)) + && [item.calculatedOuterLengthCm, item.calculatedOuterWidthCm, item.calculatedOuterHeightCm] + .every((value, index, dimensions) => { + // 旧服务实例可能尚未返回新版计算结果字段;计算方式已回读成功时不能误判并回滚下拉。 + if (dimensions.every((dimension) => dimension == null)) return true + if (value == null) return false + const expected = calculatedMeasurementRow(item as unknown as Record, mode) + return Math.abs(Number(value) - Number([expected.lengthCm, expected.widthCm, expected.heightCm][index])) < 0.001 + }) + )) + if (!persisted) { + throw new Error('计算策略保存后回读不一致,请刷新后重试') + } + ElMessage.success(`${row ? '当前数据' : '全部测量数据'}已保存并绑定计算规则,体积已重新计算`) return true } catch (error) { measurementLoadedKey.value = '' @@ -924,10 +998,6 @@ async function saveWarehouseOuterDimensions(row: Record) { || !can('platform:firstleg:inbound:measure:edit')) return const dimensions = [row.outerLengthCm, row.outerWidthCm, row.outerHeightCm] .map((value) => value == null || value === '' ? null : Number(value)) - if (dimensions.every((value) => value == null)) { - ElMessage.warning('请至少输入一项需要修改的外箱尺寸') - return - } if (dimensions.some((value) => value != null && (!Number.isFinite(value) || value <= 0))) { ElMessage.warning('仓库修改外箱尺寸必须大于 0') return @@ -942,7 +1012,7 @@ async function saveWarehouseOuterDimensions(row: Record) { measurementId: String(measurement.measurementId), version: measurement.version, actualGoodsName: measurement.actualGoodsName, - items: measurement.items.map((item) => ({ + items: measurement.items.filter((item) => Number(item.packageCount) > 0).map((item) => ({ inboundDetailId: item.inboundDetailId ?? undefined, boxCode: item.boxCode, itemNo: item.itemNo, @@ -1010,9 +1080,9 @@ async function loadMeasurementChargeRules() { } } -async function loadMeasurementDimensionConfig() { +async function loadMeasurementDimensionConfig(force = false) { const tenantId = normalizedIdentifier(detail.value?.summary.tenantId) - if (!tenantId || measurementDimensionConfigLoadedKey.value === tenantId || measurementDimensionConfigLoading.value) return + if (!tenantId || (!force && measurementDimensionConfigLoadedKey.value === tenantId) || measurementDimensionConfigLoading.value) return measurementDimensionConfigLoading.value = true try { measurementDimensionConfig.value = await getCustomerDimensionConfig(tenantId) @@ -1089,18 +1159,23 @@ async function loadMeasurementDetails() { const [response] = await Promise.all([ getInboundMeasurement(String(summary.tenantId), String(summary.inboundId)), loadMeasurementChargeRules(), - loadMeasurementDimensionConfig(), + loadMeasurementDimensionConfig(true), ]) if (serial !== measurementRequestSerial) return measurementDetail.value = response - const savedModes = [...new Set(response.items.map((item) => item.coefficientMode) - .filter((mode): mode is MeasurementCalculationMode => mode === 'ACTUAL' || mode === 'ADD' || mode === 'MULTIPLY'))] const configuredMode = measurementDimensionConfig.value?.defaultCalculationMode - // 已保存的明细规则优先于客户默认配置,确保再次进入时回显上次选择及其计算结果。 - measurementSectionModes.outer = savedModes.length === 1 ? savedModes[0]! - : configuredMode === 'MULTIPLY' && activeMeasurementChargeRule.value ? 'MULTIPLY' - : configuredMode === 'ADD' ? 'ADD' : 'ACTUAL' - measurementSectionModes.customer = measurementSectionModes.outer + const savedModes = [...new Set(response.items.map((item) => item.coefficientMode) + .filter((mode): mode is MeasurementCalculationMode => ( + mode === 'ACTUAL' || mode === 'ADD' || mode === 'MULTIPLY' + )))] + // 订单测量明细中已落库的规则是本次计费快照,必须优先于客户当前默认规则。 + // 客户默认值只用于尚未绑定规则的新测量数据,避免保存后又被覆盖回 ACTUAL。 + measurementSectionModes.outer = savedModes.length === 1 + ? savedModes[0]! + : configuredMode === 'MULTIPLY' && activeMeasurementChargeRule.value + ? 'MULTIPLY' : configuredMode === 'ADD' ? 'ADD' : 'ACTUAL' + measurementSectionModes.customer = configuredMode === 'MULTIPLY' && activeMeasurementChargeRule.value + ? 'MULTIPLY' : configuredMode === 'ADD' ? 'ADD' : 'ACTUAL' measurementSectionModes.actual = 'ACTUAL' measurementLoadedKey.value = key } catch (error) { @@ -1331,26 +1406,19 @@ function openMeasurement(row: UnifiedOrderResource) { async function openFeeConfirmDialog() { const summary = detail.value?.summary - const reviewStatus = String(summary?.measurement?.reviewStatus ?? '').trim().toUpperCase() - if (reviewStatus !== 'APPROVED') { - ElMessage.warning('测量审核通过后才能确认并发送费用') - return - } if (!summary?.inboundId) { ElMessage.warning('当前单据缺少入仓信息,暂不能发送费用') return } - if (confirmFeeSending.value) { + if (!hasActualMeasurementData.value) { + ElMessage.warning('暂无实际测量数据,不能确认并发送费用') return } - try { - const snapshot = await getLatestInboundFee(String(summary.tenantId), String(summary.inboundId)) - if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) { - ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送') - return - } - } catch (error) { - ElMessage.error(getApiErrorDetails(error, '费用状态查询失败,请刷新后重试').message) + if (!canSendFeeForCurrentStatus.value) { + ElMessage.warning('订单已进入装柜流程,不能再修改或发送费用') + return + } + if (confirmFeeSending.value) { return } if (measurementStrategySavingId.value) { @@ -1358,12 +1426,16 @@ async function openFeeConfirmDialog() { return } const outerTotals = measurementTotals(outerMeasurementRows.value) - if (!outerMeasurementRows.value.length || !(Number(outerTotals.gross) > 0) - || !(Number(outerTotals.volume) > 0)) { + if (!outerMeasurementRows.value.length || !(Number(outerTotals.volume) > 0)) { ElMessage.warning('外箱规数据不完整,请先保存外箱规尺寸和计算规则') return } - feeConfirmWeight.value = Number(outerTotals.gross) + const actualTotals = measurementTotals(actualMeasurementRows.value) + const customerTotals = measurementTotals(customerMeasurementRows.value) + const defaultWeight = Number(outerTotals.gross) > 0 ? Number(outerTotals.gross) + : Number(actualTotals.gross) > 0 ? Number(actualTotals.gross) + : Number(customerTotals.gross) > 0 ? Number(customerTotals.gross) : null + feeConfirmWeight.value = defaultWeight feeConfirmVolume.value = Number(outerTotals.volume) feeConfirmOpen.value = true } @@ -1381,21 +1453,13 @@ async function executeConfirmAndSendFee() { try { const tenantId = String(summary.tenantId) const inboundId = String(summary.inboundId) - let snapshot = await getLatestInboundFee(tenantId, inboundId) - if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) { - ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送') - feeConfirmOpen.value = false - return - } - if (snapshot.feeStatus === 'CALCULATED') { - snapshot = await calculateInboundFee(tenantId, inboundId, { - selectedWeightKg, - selectedVolumeCbm, - weightSource: 'OUTER', - volumeSource: 'OUTER', - }, createFeeActionIdempotencyKey('calculate')) - snapshot = await confirmInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('confirm')) - } + let snapshot = await calculateInboundFee(tenantId, inboundId, { + selectedWeightKg, + selectedVolumeCbm, + weightSource: 'OUTER', + volumeSource: 'OUTER', + }, createFeeActionIdempotencyKey('calculate')) + snapshot = await confirmInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('confirm')) if (snapshot.feeStatus === 'CONFIRMED') { await sendInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('send')) ElMessage.success('费用已确认并发送给客户') @@ -1403,8 +1467,7 @@ async function executeConfirmAndSendFee() { await loadDetail(true) return } - if (snapshot.feeStatus === 'SENT') ElMessage.info('该费用已经发送给客户,无需重复发送') - else ElMessage.warning('当前没有可发送的试算费用,请先在费用信息中完成费用试算') + ElMessage.warning('当前费用状态不允许发送,请刷新后重试') } catch (error) { ElMessage.error(getApiErrorDetails(error, '确认费用发送失败').message) } finally { @@ -1746,10 +1809,8 @@ async function loadDetail(force = false) { measurementChargeRules.value = [] measurementChargeRulesLoadedKey.value = '' } - if (measurementDimensionConfigLoadedKey.value !== tenantId) { - measurementDimensionConfig.value = null - measurementDimensionConfigLoadedKey.value = '' - } + measurementDimensionConfig.value = null + measurementDimensionConfigLoadedKey.value = '' measurementPreviewOpen.value = false releaseMeasurementPreview() releaseCargoImagePreviews() @@ -2030,7 +2091,8 @@ onBeforeUnmount(() => { v-if="can('firstleg:inbound:confirm-send-fee')" type="primary" :loading="confirmFeeSending" - :title="detail.summary.measurement?.reviewStatus === 'APPROVED' ? '确认并发送费用' : '测量审核通过后才能发送费用'" + :disabled="confirmFeeSending || measurementLoading || !hasActualMeasurementData || !canSendFeeForCurrentStatus" + title="确认并发送费用" data-testid="confirm-send-fee-button" @click="openFeeConfirmDialog" >确认费用发送 @@ -2058,12 +2120,23 @@ onBeforeUnmount(() => {
箱规计算规则选择 - + - 规则参数来源:客户管理 + + + + 规则参数来源:客户管理 · 当前绑定:{{ boundOuterMeasurementChargeRule.ruleCode }} v{{ boundOuterMeasurementChargeRule.ruleVersion }}