Merge remote-tracking branch 'origin/release/test' into dev
# Conflicts: # src/modules/loading-lists/api/platformLoadingLists.test.ts
This commit is contained in:
@@ -35,9 +35,11 @@ const snapshot: InboundFeeSnapshot = {
|
||||
carrierRateId: '202', rateCode: 'RATE-LAX', carrierRateVersion: 5,
|
||||
customerId: '303', customerName: '测试客户', originWarehouseId: '404', originWarehouseName: '义乌仓',
|
||||
businessType: 'FBA_SEA', channelCode: 'SEA-TAX', carrierId: '505', carrierName: '测试船司',
|
||||
destinationCode: 'LAX9', taxMode: 'TAX_INCLUDED', chargeMode: 'KG',
|
||||
destinationCode: 'LAX9', taxMode: 'TAX_REFUND', pricingTaxMode: 'TAX_INCLUDED', chargeMode: 'KG',
|
||||
conversionFactor: 167, roundingScale: 2, roundingMode: 'HALF_UP',
|
||||
actualWeightKg: 100, actualVolumeCbm: 0.8, convertedQuantity: 133.6, chargeableQuantity: 133.6,
|
||||
actualWeightKg: 100, actualVolumeCbm: 0.8, convertedQuantity: 133.6,
|
||||
baselineChargeableQuantity: 133.6, lowerChargeableQuantity: 120.24, upperChargeableQuantity: 146.96,
|
||||
tolerancePercent: 10, chargeableQuantity: 133.6,
|
||||
chargeUnit: 'KG', unitPrice: 8.5, subtotalAmount: 1135.6, additionalAmount: 100,
|
||||
totalAmount: 1235.6, currency: 'CNY',
|
||||
rateEffectiveFrom: '2026-08-01T00:00:00', rateEffectiveTo: null,
|
||||
@@ -88,6 +90,8 @@ describe('InboundFeePanel', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(getLatestInboundFee).toHaveBeenCalledWith('10001', '88')
|
||||
expect(document.body.textContent).toContain('订单报关:退税报关')
|
||||
expect(document.body.textContent).toContain('报价税制:包税')
|
||||
expect(document.body.textContent).not.toContain('租户')
|
||||
expect(document.body.textContent).toContain('头程自动费用 · CNY')
|
||||
expect(document.body.textContent).toContain('¥1,235.60')
|
||||
@@ -237,6 +241,18 @@ describe('InboundFeePanel', () => {
|
||||
expect(wrapper.find('[data-testid="adjust-fee-button"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('allows a published fee adjustment until the customer confirms it', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
expect(wrapper.get('[data-testid="adjust-fee-button"]').text()).toContain('调整已发布费用')
|
||||
|
||||
document.body.innerHTML = ''
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, customerConfirmationStatus: 'CONFIRMED' })
|
||||
const confirmed = mountPanel()
|
||||
await flushPromises()
|
||||
expect(confirmed.find('[data-testid="adjust-fee-button"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('confirms and then sends a calculated fee through the implemented backend transitions', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED', confirmedAt: null, sentAt: null })
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
@@ -294,7 +310,7 @@ describe('InboundFeePanel', () => {
|
||||
expect(adjustPublishedInboundFee).toHaveBeenCalledWith(
|
||||
'10001',
|
||||
'88',
|
||||
{ expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费用' },
|
||||
{ expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费用', toleranceConfirmed: true },
|
||||
expect.stringMatching(/^fee-adjust-/),
|
||||
)
|
||||
expect(wrapper.emitted('changed')?.[0]?.[0]).toMatchObject({ feeVersion: 3 })
|
||||
@@ -307,6 +323,11 @@ describe('InboundFeePanel', () => {
|
||||
expect(source).toContain('grid-template-columns: repeat(3, minmax(0, 1fr))')
|
||||
expect(source).toContain('class="adjustment-unit"')
|
||||
expect(source).toContain('white-space: nowrap')
|
||||
expect(source).toContain('snapshot.value?.lowerChargeableQuantity')
|
||||
expect(source).toContain('snapshot.value?.upperChargeableQuantity')
|
||||
expect(source).toContain('toleranceConfirmed: true')
|
||||
expect(source).toContain("'再次确认计费量调整'")
|
||||
expect(source).toContain('nextQuantity < adjustmentMinimum.value || nextQuantity > adjustmentMaximum.value')
|
||||
})
|
||||
|
||||
it('refreshes the latest snapshot after a conflict while retaining the dialog', async () => {
|
||||
|
||||
@@ -86,11 +86,17 @@ const normalizedInboundId = computed(() => {
|
||||
return identifier && identifier !== 'null' && identifier !== 'undefined' ? identifier : ''
|
||||
})
|
||||
const adjustmentEligible = computed(() => (
|
||||
snapshot.value?.feeStatus === 'CALCULATED'
|
||||
['CALCULATED', 'CONFIRMED', 'SENT'].includes(snapshot.value?.feeStatus || '')
|
||||
&& snapshot.value?.nextProcessStatus === 'WAIT_SPLIT'
|
||||
&& snapshot.value?.customerConfirmationStatus !== 'CONFIRMED'
|
||||
))
|
||||
const chargeModeLabel = computed(() => snapshot.value?.chargeMode === 'KG' ? '按重量计费' : '按体积计费')
|
||||
const chargeUnitLabel = computed(() => snapshot.value?.chargeUnit === 'KG' ? 'KG' : 'CBM')
|
||||
const convertedQuantityLabel = computed(() => snapshot.value?.chargeMode === 'KG' ? '体积重量' : '重量折算体积')
|
||||
const adjustmentBaseline = computed(() => Number(snapshot.value?.baselineChargeableQuantity || 0))
|
||||
const adjustmentMinimum = computed(() => Number(snapshot.value?.lowerChargeableQuantity || 0))
|
||||
const adjustmentMaximum = computed(() => Number(snapshot.value?.upperChargeableQuantity || 0))
|
||||
const adjustmentQuantityChanged = computed(() => Math.abs(Number(adjustmentForm.chargeableQuantity) - adjustmentBaseline.value) > 1e-8)
|
||||
const statusInfo = computed(() => feeStatusInfo(snapshot.value?.feeStatus))
|
||||
const sourceOptions = computed(() => {
|
||||
const value = measurement.value
|
||||
@@ -145,6 +151,12 @@ function taxModeLabel(value: string) {
|
||||
return firstLegTaxModeLabel(value)
|
||||
}
|
||||
|
||||
function pricingTaxModeLabel(value: string | null | undefined) {
|
||||
if (value === 'TAX_INCLUDED') return '包税'
|
||||
if (value === 'TAX_EXCLUDED') return '不包税'
|
||||
return '—'
|
||||
}
|
||||
|
||||
function roundingModeLabel(value: string) {
|
||||
const labels: Record<string, string> = {
|
||||
UP: '远离零方向', DOWN: '向零方向', CEILING: '向上', FLOOR: '向下',
|
||||
@@ -275,6 +287,7 @@ function adjustmentPayload(): AdjustPublishedInboundFeeRequest {
|
||||
unitPrice: Number(adjustmentForm.unitPrice),
|
||||
additionalAmount: Number(adjustmentForm.additionalAmount),
|
||||
reason: adjustmentForm.reason.trim(),
|
||||
toleranceConfirmed: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +304,22 @@ async function submitAdjustment() {
|
||||
if (!snapshot.value) return
|
||||
const valid = await adjustFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
const nextQuantity = Number(adjustmentForm.chargeableQuantity)
|
||||
if (nextQuantity < adjustmentMinimum.value || nextQuantity > adjustmentMaximum.value) {
|
||||
ElMessage.warning(`调整后计费量须在基准值的 ±10% 范围内(${formatNumber(adjustmentMinimum.value, 4)}–${formatNumber(adjustmentMaximum.value, 4)} ${chargeUnitLabel.value})`)
|
||||
return
|
||||
}
|
||||
if (adjustmentQuantityChanged.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`计费量将由 ${formatNumber(adjustmentBaseline.value, 4)} 调整为 ${formatNumber(nextQuantity, 4)} ${chargeUnitLabel.value},并生成新费用版本。是否继续?`,
|
||||
'再次确认计费量调整',
|
||||
{ type: 'warning', confirmButtonText: '确认调整', cancelButtonText: '返回检查' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
const payload = adjustmentPayload()
|
||||
adjustmentError.value = null
|
||||
conflictRefreshed.value = false
|
||||
@@ -364,7 +393,7 @@ defineExpose({ reload: loadFee })
|
||||
data-testid="adjust-fee-button"
|
||||
type="primary"
|
||||
@click="openAdjustment"
|
||||
><PencilLine :size="16" />调整初算费用</el-button>
|
||||
><PencilLine :size="16" />{{ snapshot?.feeStatus === 'CALCULATED' ? '调整初算费用' : '调整已发布费用' }}</el-button>
|
||||
<el-button
|
||||
v-if="!displayOnly && canConfirm && snapshot?.feeStatus === 'CALCULATED'"
|
||||
data-testid="confirm-fee-button"
|
||||
@@ -452,12 +481,15 @@ defineExpose({ reload: loadFee })
|
||||
<section class="calculation-section" aria-labelledby="calculation-title">
|
||||
<div class="section-title">
|
||||
<div><h4 id="calculation-title">计费依据</h4><p v-if="!compact">实际重量与体积重量取大后计费,金额以服务端快照为准。</p></div>
|
||||
<el-tag effect="plain">{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
|
||||
<div class="calculation-tags">
|
||||
<el-tag effect="plain">订单报关:{{ taxModeLabel(snapshot.taxMode) }}</el-tag>
|
||||
<el-tag effect="plain">报价税制:{{ pricingTaxModeLabel(snapshot.pricingTaxMode) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calculation-flow">
|
||||
<div><span>实际重量</span><strong>{{ formatNumber(snapshot.actualWeightKg, 3) }} KG</strong></div>
|
||||
<div><span>实际体积</span><strong>{{ formatNumber(snapshot.actualVolumeCbm, 4) }} CBM</strong></div>
|
||||
<div><span>体积重量</span><strong>{{ formatNumber(snapshot.convertedQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong><small>系数 {{ formatNumber(snapshot.conversionFactor, 4) }}</small></div>
|
||||
<div><span>{{ convertedQuantityLabel }}</span><strong>{{ formatNumber(snapshot.convertedQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong><small>系数 {{ formatNumber(snapshot.conversionFactor, 4) }}</small></div>
|
||||
<div class="is-chargeable"><span>取大后的计费量</span><strong>{{ formatNumber(snapshot.chargeableQuantity, snapshot.roundingScale) }} {{ chargeUnitLabel }}</strong></div>
|
||||
</div>
|
||||
<div class="amount-equation" aria-label="费用计算公式">
|
||||
@@ -517,6 +549,10 @@ defineExpose({ reload: loadFee })
|
||||
@confirm="submitAdjustment"
|
||||
>
|
||||
<el-alert type="warning" :closable="false" show-icon title="调整会生成新的费用版本;小计和总额由服务端按计费量、单价和附加费重算。" />
|
||||
<el-alert class="adjustment-alert" type="info" :closable="false" show-icon>
|
||||
<template #title>计费量允许在系统原始基准值 ±{{ formatNumber(snapshot?.tolerancePercent, 2) }}% 内调整</template>
|
||||
<p>系统基准 {{ formatNumber(adjustmentBaseline, 4) }} {{ chargeUnitLabel }},服务端允许范围 {{ formatNumber(adjustmentMinimum, 4) }}–{{ formatNumber(adjustmentMaximum, 4) }} {{ chargeUnitLabel }}(包含边界);超出范围将无法提交。</p>
|
||||
</el-alert>
|
||||
<el-alert v-if="conflictRefreshed" class="adjustment-alert" type="warning" :closable="false" show-icon>
|
||||
<template #title>费用已被其他操作员更新,系统已刷新最新快照</template>
|
||||
<p>当前输入仍保留,请先核对最新版本后再提交。</p>
|
||||
@@ -524,7 +560,7 @@ defineExpose({ reload: loadFee })
|
||||
<el-form ref="adjustFormRef" :model="adjustmentForm" :rules="adjustmentRules" label-position="top" class="adjustment-form" :disabled="adjusting">
|
||||
<div class="adjustment-amounts">
|
||||
<el-form-item label="调整后计费量" prop="chargeableQuantity" required>
|
||||
<el-input-number v-model="adjustmentForm.chargeableQuantity" aria-label="调整后计费量" :min="0.0001" :max="999999999999.9999" :precision="4" :step="1" controls-position="right" placeholder="请输入调整后计费量" />
|
||||
<el-input-number v-model="adjustmentForm.chargeableQuantity" aria-label="调整后计费量" :min="adjustmentMinimum" :max="adjustmentMaximum" :precision="4" :step="1" controls-position="right" placeholder="请输入调整后计费量" />
|
||||
<span class="adjustment-unit">{{ chargeUnitLabel }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整后单价" prop="unitPrice" required>
|
||||
@@ -571,6 +607,7 @@ defineExpose({ reload: loadFee })
|
||||
.version-status { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.calculation-section, .fee-timeline { padding: 10px 0; border-bottom: 1px solid var(--cc-border); }
|
||||
.section-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||
.calculation-tags { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
|
||||
.section-title h4 { font-size: 14px; }
|
||||
.calculation-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 7px; border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.calculation-flow > div { display: flex; min-width: 0; min-height: 46px; flex-direction: row; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-right: 1px solid var(--cc-border); }
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('inbound fee API', () => {
|
||||
|
||||
it('adjusts a published snapshot with the required idempotency key', async () => {
|
||||
vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: { ...snapshot, feeVersion: 3 } })
|
||||
const payload = { expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费' }
|
||||
const payload = { expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费', toleranceConfirmed: true as const }
|
||||
|
||||
await expect(adjustPublishedInboundFee('10001', '88', payload, 'fee-adjust-1'))
|
||||
.resolves.toMatchObject({ feeVersion: 3 })
|
||||
@@ -78,7 +78,7 @@ describe('inbound fee API', () => {
|
||||
const error = await adjustPublishedInboundFee(
|
||||
'10001',
|
||||
'88',
|
||||
{ expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '价格修正' },
|
||||
{ expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '价格修正', toleranceConfirmed: true },
|
||||
'fee-adjust-2',
|
||||
).catch((reason: unknown) => reason)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface InboundFeeSnapshot {
|
||||
carrierName: string
|
||||
destinationCode: string
|
||||
taxMode: string
|
||||
pricingTaxMode: string | null
|
||||
chargeMode: InboundChargeMode
|
||||
conversionFactor: number
|
||||
roundingScale: number
|
||||
@@ -33,6 +34,10 @@ export interface InboundFeeSnapshot {
|
||||
actualWeightKg: number
|
||||
actualVolumeCbm: number
|
||||
convertedQuantity: number
|
||||
baselineChargeableQuantity: number
|
||||
lowerChargeableQuantity: number
|
||||
upperChargeableQuantity: number
|
||||
tolerancePercent: number
|
||||
chargeableQuantity: number
|
||||
chargeUnit: string
|
||||
unitPrice: number | null
|
||||
@@ -58,4 +63,5 @@ export interface AdjustPublishedInboundFeeRequest {
|
||||
unitPrice: number
|
||||
additionalAmount: number
|
||||
reason: string
|
||||
toleranceConfirmed: true
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ function measurement(overrides: Partial<InboundMeasurementDetail> = {}): Inbound
|
||||
items: [{
|
||||
id: '9101',
|
||||
inboundDetailId: '8101',
|
||||
inboundCode: 'RK202608040001',
|
||||
fbaShipmentNo: 'FBA18ABC1234',
|
||||
scannedShipmentId: 'FBA18SCAN5678',
|
||||
boxCode: 'RK202608040001#10-1',
|
||||
itemNo: '001',
|
||||
boxSpecification: '50×40×30cm',
|
||||
@@ -168,6 +171,8 @@ describe('InboundMeasurementPanel', () => {
|
||||
|
||||
expect(getInboundMeasurement).toHaveBeenCalledWith('10001', '7001')
|
||||
expect(wrapper.text()).toContain('RK202608040001')
|
||||
expect(wrapper.find('.measurement-table').text()).toContain('FBA18ABC1234')
|
||||
expect(wrapper.find('.measurement-table').text()).toContain('FBA18SCAN5678')
|
||||
expect(wrapper.find('.measurement-table').text()).toContain('0.6')
|
||||
expect(wrapper.find('.measurement-table').text()).not.toContain('周长')
|
||||
await wrapper.find('.el-table__expand-icon').trigger('click')
|
||||
|
||||
@@ -32,6 +32,9 @@ const emit = defineEmits<{
|
||||
type EditableMeasurementItem = Omit<SaveInboundMeasurementItemRequest, 'coefficientMode'> & {
|
||||
itemId: string
|
||||
clientKey: string
|
||||
inboundCode: string
|
||||
fbaShipmentNo: string | null
|
||||
scannedShipmentId: string | null
|
||||
serverValues: Pick<InboundMeasurementItem, 'girthCm' | 'unitVolumeCbm' | 'totalWeightKg' | 'totalVolumeCbm' | 'volumeWeightKg'>
|
||||
chargeRuleId: string | null
|
||||
coefficientMode: 'ACTUAL' | 'ADD' | 'MULTIPLY'
|
||||
@@ -266,6 +269,9 @@ function editableItem(item: InboundMeasurementItem): EditableMeasurementItem {
|
||||
itemId: String(item.id),
|
||||
inboundDetailId: String(item.inboundDetailId || item.id),
|
||||
clientKey: nextItemKey(),
|
||||
inboundCode: item.inboundCode || '—',
|
||||
fbaShipmentNo: item.fbaShipmentNo || null,
|
||||
scannedShipmentId: item.scannedShipmentId || null,
|
||||
boxCode: item.boxCode,
|
||||
itemNo: item.itemNo,
|
||||
boxSpecification: item.boxSpecification,
|
||||
@@ -701,6 +707,15 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemNo" label="#" width="70" align="center" />
|
||||
<el-table-column label="入仓码" min-width="154" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.inboundCode || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="FBA 货件号" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.fbaShipmentNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备实扫货件号" min-width="164" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.scannedShipmentId || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="箱号" min-width="156" show-overflow-tooltip>
|
||||
<template #default="{ row }"><span class="measurement-box-code">{{ row.boxCode || '—' }}</span></template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -13,7 +13,10 @@ export type InboundMeasurementProcessStatus =
|
||||
|
||||
export interface InboundMeasurementItem {
|
||||
id: MeasurementIdentifier
|
||||
inboundCode?: string | null
|
||||
inboundDetailId?: MeasurementIdentifier | null
|
||||
fbaShipmentNo?: string | null
|
||||
scannedShipmentId?: string | null
|
||||
boxCode: string | null
|
||||
itemNo: string
|
||||
boxSpecification: string
|
||||
|
||||
Reference in New Issue
Block a user