diff --git a/src/modules/first-leg/utils/priceEstimate.test.mjs b/src/modules/first-leg/utils/priceEstimate.test.mjs new file mode 100644 index 0000000..063756c --- /dev/null +++ b/src/modules/first-leg/utils/priceEstimate.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { createPriceEstimateBreakdown } from './priceEstimate.ts' + +test('explains KG estimates and selects volume weight when it is larger', () => { + assert.deepEqual(createPriceEstimateBreakdown(16, 0.6, 'KG'), { + actualLabel: '实际毛重', + actualQuantity: 16, + convertedLabel: '体积重量', + convertedQuantity: 100.2, + convertedFormula: '0.6 m³ × 167', + chargeableLabel: '计费重量', + basisLabel: '采用体积重量', + }) +}) + +test('explains CBM estimates and selects the larger converted quantity', () => { + const result = createPriceEstimateBreakdown(400, 1, 'CBM') + + assert.equal(result?.actualLabel, '实际体积') + assert.equal(result?.convertedLabel, '重量折算体积') + assert.equal(result?.convertedQuantity, 400 / 363) + assert.equal(result?.convertedFormula, '400 KG ÷ 363') + assert.equal(result?.chargeableLabel, '计费体积') + assert.equal(result?.basisLabel, '采用重量折算体积') +}) + +test('does not invent an explanation for an unsupported charge unit', () => { + assert.equal(createPriceEstimateBreakdown(16, 0.6, 'PALLET'), null) +}) + +test('keeps floating point artifacts out of the displayed formula', () => { + assert.equal(createPriceEstimateBreakdown(16, 0.1 + 0.2, 'KG')?.convertedFormula, '0.3 m³ × 167') +}) diff --git a/src/modules/first-leg/utils/priceEstimate.ts b/src/modules/first-leg/utils/priceEstimate.ts new file mode 100644 index 0000000..a39f5ee --- /dev/null +++ b/src/modules/first-leg/utils/priceEstimate.ts @@ -0,0 +1,54 @@ +const KG_PER_CBM = 167 +const KG_PER_BILLABLE_CBM = 363 + +function formatFormulaNumber(value: number) { + return value.toLocaleString('zh-CN', { maximumFractionDigits: 6, useGrouping: false }) +} + +export interface PriceEstimateBreakdown { + actualLabel: string + actualQuantity: number + convertedLabel: string + convertedQuantity: number + convertedFormula: string + chargeableLabel: string + basisLabel: string +} + +/** + * Explains the server's current first-leg chargeable-quantity rules. The server + * response remains authoritative for the final chargeable quantity and amount. + */ +export function createPriceEstimateBreakdown( + actualWeightKg: number, + actualVolumeCbm: number, + chargeUnit: string, +): PriceEstimateBreakdown | null { + if (chargeUnit === 'KG') { + const volumeWeightKg = actualVolumeCbm * KG_PER_CBM + return { + actualLabel: '实际毛重', + actualQuantity: actualWeightKg, + convertedLabel: '体积重量', + convertedQuantity: volumeWeightKg, + convertedFormula: `${formatFormulaNumber(actualVolumeCbm)} m³ × ${KG_PER_CBM}`, + chargeableLabel: '计费重量', + basisLabel: actualWeightKg >= volumeWeightKg ? '采用实际毛重' : '采用体积重量', + } + } + + if (chargeUnit === 'CBM') { + const convertedVolumeCbm = actualWeightKg / KG_PER_BILLABLE_CBM + return { + actualLabel: '实际体积', + actualQuantity: actualVolumeCbm, + convertedLabel: '重量折算体积', + convertedQuantity: convertedVolumeCbm, + convertedFormula: `${formatFormulaNumber(actualWeightKg)} KG ÷ ${KG_PER_BILLABLE_CBM}`, + chargeableLabel: '计费体积', + basisLabel: actualVolumeCbm >= convertedVolumeCbm ? '采用实际体积' : '采用重量折算体积', + } + } + + return null +} diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 5b43b6c..0070544 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -362,6 +362,10 @@ test('estimates by warehouse, destination, tax mode, weight and volume without b assert.match(source, /v-for="mode in availablePricingTaxModes"/) assert.match(source, /pricingTaxModeLabel\(mode\)/) assert.match(source, /priceEstimate\.chargeableQuantity/) + assert.match(source, /priceEstimateBreakdown\.actualLabel/) + assert.match(source, /priceEstimateBreakdown\.convertedLabel/) + assert.match(source, /priceEstimateBreakdown\.basisLabel/) + assert.match(source, /两者取大/) assert.match(source, /priceEstimate\.unitPrice/) assert.match(source, /priceEstimate\.estimatedAmount/) assert.match(source, /priceEstimateDisclaimer/) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index 20d822a..e2d622b 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -41,6 +41,7 @@ import { validateFirstLegOrderDraft, } from '@/modules/first-leg/utils/firstLegOrder' import { validateProductImageFile } from '@/modules/first-leg/utils/productImage' +import { createPriceEstimateBreakdown } from '@/modules/first-leg/utils/priceEstimate' const store = useFirstLegOrdersStore() const tenantStore = useTenantAdministrationStore() @@ -754,6 +755,11 @@ function formatEstimateQuantity(value: number, unit: string) { } const priceEstimateDisclaimer = computed(() => priceEstimate.value?.disclaimer || '仅供参考,最终以实际结算为准') +const priceEstimateBreakdown = computed(() => { + if (!priceEstimate.value) return null + const summary = summarizeFirstLegCargo(form.items) + return createPriceEstimateBreakdown(summary.grossWeightKg, summary.volumeCbm, priceEstimate.value.chargeUnit) +}) function resetPriceEstimate() { estimateRequestSequence += 1 @@ -1774,7 +1780,17 @@ async function searchOrders(model: QueryModel) { {{ selectedPricingChannel?.channelName || priceEstimate.channelCode }} {{ pricingTaxModeLabel(priceEstimate.pricingTaxMode) }} · 参考时效 {{ selectedPricingChannel.referenceTransit }} -