优化
This commit is contained in:
@@ -1,6 +1,19 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { PlatformTenantResource } from '@/modules/iam'
|
||||
import { loadAllActiveCustomers } from '../task-list'
|
||||
|
||||
function activeCustomer(id: string, tenantName: string, legalName: string | null, tenantCode: string): PlatformTenantResource {
|
||||
return {
|
||||
id, tenantName, legalName, tenantCode, status: 'ACTIVE', legalRepresentative: null,
|
||||
unifiedSocialCreditCode: null, contactName: null, contactPhone: null, contactEmail: null,
|
||||
servicePackage: null, contactWechat: null, salesOwnerName: null, paymentTermDays: null,
|
||||
enterpriseType: null, establishedDate: null, registeredCapital: null,
|
||||
registeredCapitalCurrency: null, registeredAddress: null, internalRemark: null,
|
||||
createdAt: null, approvedAt: null,
|
||||
}
|
||||
}
|
||||
|
||||
describe('InboundTaskList measurement scope', () => {
|
||||
it('uses the inbound tenant isolation key for measurement and review requests', () => {
|
||||
@@ -10,6 +23,42 @@ describe('InboundTaskList measurement scope', () => {
|
||||
expect(source).not.toContain('selectedInbound.customerId || selectedInbound.tenantId')
|
||||
})
|
||||
|
||||
it('loads only active customers and exposes a searchable name-only select', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
const queryFieldsSource = source.slice(source.indexOf('const queryFields'), source.indexOf('const columns'))
|
||||
|
||||
expect(source).toContain('loadAllActiveCustomers')
|
||||
expect(queryFieldsSource).toContain("type: 'select'")
|
||||
expect(queryFieldsSource).toContain('filterable: true')
|
||||
expect(queryFieldsSource).toContain('label: customer.legalName?.trim() || customer.tenantName')
|
||||
expect(queryFieldsSource).not.toContain('tenantCode')
|
||||
expect(queryFieldsSource).not.toContain('客户编码')
|
||||
})
|
||||
|
||||
it('requests every active-customer page and merges name-only options', async () => {
|
||||
const calls: Array<{ page: number; size: number; status: 'ACTIVE' }> = []
|
||||
const records = await loadAllActiveCustomers(async (query) => {
|
||||
calls.push(query)
|
||||
if (query.page === 1) {
|
||||
return {
|
||||
pages: 2,
|
||||
records: [activeCustomer('10001', '客户一', '客户一有限公司', 'CUS-001')],
|
||||
}
|
||||
}
|
||||
return {
|
||||
pages: 2,
|
||||
records: [activeCustomer('10002', '客户二', null, 'CUS-002')],
|
||||
}
|
||||
})
|
||||
|
||||
expect(calls).toEqual([
|
||||
{ page: 1, size: 200, status: 'ACTIVE' },
|
||||
{ page: 2, size: 200, status: 'ACTIVE' },
|
||||
])
|
||||
expect(records.map((customer) => customer.legalName?.trim() || customer.tenantName)).toEqual(['客户一有限公司', '客户二'])
|
||||
expect(records.map((customer) => customer.legalName?.trim() || customer.tenantName).join(' ')).not.toContain('CUS-')
|
||||
})
|
||||
|
||||
it('opens the shared unified order detail from the view action', () => {
|
||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { InboundOrderSummary } from '../core/types'
|
||||
import InboundMeasurementPanel from '../measurement/InboundMeasurementPanel.vue'
|
||||
import MeasurementReviewPanel from '../review/MeasurementReviewPanel.vue'
|
||||
import { inboundProcessStatusLabels, inboundReviewStatusLabels, inboundStatusLabel } from '../status'
|
||||
import { buildInboundTaskQuery, type InboundTaskMode } from '../task-list'
|
||||
import { buildInboundTaskQuery, loadAllActiveCustomers, type InboundTaskMode } from '../task-list'
|
||||
|
||||
type FilterModel = QueryModel & {
|
||||
customerId: string
|
||||
@@ -43,6 +43,7 @@ const warehouseOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
const selectedInbound = ref<InboundOrderSummary | null>(null)
|
||||
const taskDialogOpen = ref(false)
|
||||
let requestSerial = 0
|
||||
let optionsRequestSerial = 0
|
||||
let disposed = false
|
||||
let activatedOnce = false
|
||||
|
||||
@@ -109,14 +110,15 @@ function dateParam(value: string | undefined, end = false) {
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
const serial = ++optionsRequestSerial
|
||||
customerLoadError.value = null
|
||||
try {
|
||||
const [customerPage, warehousePage] = await Promise.all([
|
||||
listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }),
|
||||
const [customerRecords, warehousePage] = await Promise.all([
|
||||
loadAllActiveCustomers((query) => listPlatformTenants(query)),
|
||||
listPlatformWarehouses(1, 200),
|
||||
])
|
||||
if (disposed) return
|
||||
customers.value = customerPage.records
|
||||
if (disposed || serial !== optionsRequestSerial) return
|
||||
customers.value = customerRecords
|
||||
warehouseOptions.value = warehousePage.records.map((warehouse) => ({
|
||||
label: `${warehouse.warehouseName} · ${warehouse.warehouseCode}`,
|
||||
value: String(warehouse.id),
|
||||
@@ -190,7 +192,7 @@ onActivated(() => {
|
||||
}
|
||||
void loadTasks()
|
||||
})
|
||||
onBeforeUnmount(() => { disposed = true; requestSerial += 1 })
|
||||
onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSerial += 1 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -241,7 +241,7 @@ onBeforeUnmount(() => {
|
||||
<div class="inbound-operation-dialog__footer">
|
||||
<el-button :disabled="Boolean(reviewPanelRef?.actionLoading)" @click="reviewDialogOpen = false">取消</el-button>
|
||||
<el-button v-if="reviewPanelRef?.isPending && reviewPanelRef?.canReview" type="danger" plain :loading="reviewPanelRef?.actionLoading === 'reject'" @click="reviewPanelRef?.openReasonDialog('reject')">驳回</el-button>
|
||||
<el-button v-if="reviewPanelRef?.isPending && reviewPanelRef?.canReview" type="primary" :loading="reviewPanelRef?.actionLoading === 'approve'" @click="reviewPanelRef?.confirmApprove()">通过并完成入库</el-button>
|
||||
<el-button v-if="reviewPanelRef?.isPending && reviewPanelRef?.canReview" type="primary" :loading="reviewPanelRef?.actionLoading === 'approve'" @click="reviewPanelRef?.confirmApprove()">通过并进入待分柜</el-button>
|
||||
<el-button v-if="reviewPanelRef?.isRejected && reviewPanelRef?.canRelease" type="warning" plain :loading="reviewPanelRef?.actionLoading === 'release'" @click="reviewPanelRef?.openReasonDialog('release')">异常放行</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -177,6 +177,14 @@ describe('InboundFeePanel', () => {
|
||||
expect(document.querySelector('[data-testid="adjust-fee-button"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('allows adjustment for a calculated fee waiting for split', async () => {
|
||||
vi.mocked(getLatestInboundFee).mockResolvedValue({ ...snapshot, feeStatus: 'CALCULATED' })
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('[data-testid="adjust-fee-button"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('submits a reasoned adjustment and emits the refreshed snapshot', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await flushPromises()
|
||||
@@ -184,12 +192,15 @@ describe('InboundFeePanel', () => {
|
||||
await flushPromises()
|
||||
|
||||
const numbers = [...document.querySelectorAll<HTMLInputElement>('.adjustment-form input')]
|
||||
numbers[0]!.value = '1200'
|
||||
numbers[0]!.value = '140'
|
||||
numbers[0]!.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
numbers[0]!.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
numbers[1]!.value = '80'
|
||||
numbers[1]!.value = '8.2'
|
||||
numbers[1]!.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
numbers[1]!.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
numbers[2]!.value = '80'
|
||||
numbers[2]!.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
numbers[2]!.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
const reason = document.querySelector<HTMLTextAreaElement>('.adjustment-form textarea')!
|
||||
reason.value = '补充已确认的附加费用'
|
||||
reason.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
@@ -203,7 +214,7 @@ describe('InboundFeePanel', () => {
|
||||
expect(adjustPublishedInboundFee).toHaveBeenCalledWith(
|
||||
'10001',
|
||||
'88',
|
||||
{ subtotalAmount: 1200, additionalAmount: 80, reason: '补充已确认的附加费用' },
|
||||
{ expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费用' },
|
||||
expect.stringMatching(/^fee-adjust-/),
|
||||
)
|
||||
expect(wrapper.emitted('changed')?.[0]?.[0]).toMatchObject({ feeVersion: 3 })
|
||||
@@ -219,10 +230,12 @@ describe('InboundFeePanel', () => {
|
||||
await flushPromises()
|
||||
|
||||
const numbers = [...document.querySelectorAll<HTMLInputElement>('.adjustment-form input')]
|
||||
numbers[0]!.value = '1200'
|
||||
numbers[0]!.value = '140'
|
||||
numbers[0]!.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
numbers[1]!.value = '80'
|
||||
numbers[1]!.value = '8.2'
|
||||
numbers[1]!.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
numbers[2]!.value = '80'
|
||||
numbers[2]!.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
const reason = document.querySelector<HTMLTextAreaElement>('.adjustment-form textarea')!
|
||||
reason.value = '并发场景价格修正'
|
||||
reason.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
|
||||
@@ -43,13 +43,16 @@ let adjustmentFingerprint = ''
|
||||
let adjustmentIdempotencyKey = ''
|
||||
|
||||
const adjustmentForm = reactive({
|
||||
subtotalAmount: 0,
|
||||
expectedFeeVersion: 0,
|
||||
chargeableQuantity: 0,
|
||||
unitPrice: 0,
|
||||
additionalAmount: 0,
|
||||
reason: '',
|
||||
})
|
||||
|
||||
const adjustmentRules: FormRules = {
|
||||
subtotalAmount: [{ required: true, message: '请输入调整后的基础运费', trigger: 'change' }],
|
||||
chargeableQuantity: [{ required: true, message: '请输入调整后的计费量', trigger: 'change' }],
|
||||
unitPrice: [{ required: true, message: '请输入调整后的单价', trigger: 'change' }],
|
||||
additionalAmount: [{ required: true, message: '请输入调整后的附加费', trigger: 'change' }],
|
||||
reason: [
|
||||
{ required: true, whitespace: true, message: '请填写费用调整原因', trigger: 'blur' },
|
||||
@@ -61,8 +64,8 @@ const canView = computed(() => access.can('firstleg:fba-pricing:view'))
|
||||
const canAdjust = computed(() => access.can('firstleg:fba-pricing:adjust'))
|
||||
const customerContextId = computed(() => props.customerId || props.tenantId || '')
|
||||
const adjustmentEligible = computed(() => (
|
||||
snapshot.value?.feeStatus === 'SENT'
|
||||
&& snapshot.value.nextProcessStatus === 'WAIT_SPLIT'
|
||||
['CALCULATED', 'CONFIRMED', 'SENT'].includes(snapshot.value?.feeStatus || '')
|
||||
&& snapshot.value?.nextProcessStatus === 'WAIT_SPLIT'
|
||||
))
|
||||
const chargeModeLabel = computed(() => snapshot.value?.chargeMode === 'KG' ? '按重量计费' : '按体积计费')
|
||||
const chargeUnitLabel = computed(() => snapshot.value?.chargeUnit === 'KG' ? 'KG' : 'CBM')
|
||||
@@ -142,7 +145,9 @@ async function loadFee(options: { preserveAdjustment?: boolean } = {}) {
|
||||
}
|
||||
|
||||
function fillAdjustmentForm(value: InboundFeeSnapshot) {
|
||||
adjustmentForm.subtotalAmount = Number(value.subtotalAmount)
|
||||
adjustmentForm.expectedFeeVersion = Number(value.feeVersion)
|
||||
adjustmentForm.chargeableQuantity = Number(value.chargeableQuantity)
|
||||
adjustmentForm.unitPrice = Number(value.unitPrice)
|
||||
adjustmentForm.additionalAmount = Number(value.additionalAmount)
|
||||
adjustmentForm.reason = ''
|
||||
adjustmentError.value = null
|
||||
@@ -159,7 +164,9 @@ function openAdjustment() {
|
||||
|
||||
function adjustmentPayload(): AdjustPublishedInboundFeeRequest {
|
||||
return {
|
||||
subtotalAmount: Number(adjustmentForm.subtotalAmount),
|
||||
expectedFeeVersion: adjustmentForm.expectedFeeVersion,
|
||||
chargeableQuantity: Number(adjustmentForm.chargeableQuantity),
|
||||
unitPrice: Number(adjustmentForm.unitPrice),
|
||||
additionalAmount: Number(adjustmentForm.additionalAmount),
|
||||
reason: adjustmentForm.reason.trim(),
|
||||
}
|
||||
@@ -244,7 +251,7 @@ defineExpose({ reload: loadFee })
|
||||
data-testid="adjust-fee-button"
|
||||
type="primary"
|
||||
@click="openAdjustment"
|
||||
><PencilLine :size="16" />调整已发布费用</el-button>
|
||||
><PencilLine :size="16" />调整初算费用</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -343,14 +350,14 @@ defineExpose({ reload: loadFee })
|
||||
|
||||
<AppDialog
|
||||
v-model="adjustOpen"
|
||||
title="调整已发布费用"
|
||||
title="调整入库费用"
|
||||
width="min(560px, calc(100vw - 28px))"
|
||||
confirm-text="确认调整并生成新版本"
|
||||
confirm-type="warning"
|
||||
:confirm-loading="adjusting"
|
||||
@confirm="submitAdjustment"
|
||||
>
|
||||
<el-alert type="warning" :closable="false" show-icon title="调整会生成新的已发布版本,不会回退实物入库状态。" />
|
||||
<el-alert type="warning" :closable="false" show-icon title="调整会生成新的费用版本;小计和总额由服务端按计费量、单价和附加费重算。" />
|
||||
<el-alert v-if="conflictRefreshed" class="adjustment-alert" type="warning" :closable="false" show-icon>
|
||||
<template #title>费用已被其他操作员更新,系统已刷新最新快照</template>
|
||||
<p>当前输入仍保留,请先核对最新版本后再提交。</p>
|
||||
@@ -362,16 +369,20 @@ defineExpose({ reload: loadFee })
|
||||
</el-alert>
|
||||
<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="subtotalAmount" required>
|
||||
<el-input-number v-model="adjustmentForm.subtotalAmount" aria-label="调整后基础运费" :min="0" :max="999999999999.99" :precision="2" :step="100" controls-position="right" />
|
||||
<span>{{ snapshot?.currency }}</span>
|
||||
<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" />
|
||||
<span>{{ chargeUnitLabel }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整后单价" prop="unitPrice" required>
|
||||
<el-input-number v-model="adjustmentForm.unitPrice" aria-label="调整后单价" :min="0" :max="999999999999.9999" :precision="4" :step="0.1" controls-position="right" />
|
||||
<span>{{ snapshot?.currency }} / {{ chargeUnitLabel }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整后附加费" prop="additionalAmount" required>
|
||||
<el-input-number v-model="adjustmentForm.additionalAmount" aria-label="调整后附加费" :min="0" :max="999999999999.99" :precision="2" :step="10" controls-position="right" />
|
||||
<span>{{ snapshot?.currency }}</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="adjustment-total"><span>调整后总额</span><strong>{{ formatMoney(adjustmentForm.subtotalAmount + adjustmentForm.additionalAmount, snapshot?.currency) }}</strong></div>
|
||||
<div class="adjustment-total"><span>当前服务端计算金额</span><strong>{{ formatMoney(snapshot?.totalAmount, snapshot?.currency) }}</strong><small>提交后以服务端返回的新版本金额为准</small></div>
|
||||
<el-form-item label="调整原因" prop="reason" required>
|
||||
<el-input v-model="adjustmentForm.reason" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="说明价格变更、补充费用或修正原因,记录将进入审计历史" />
|
||||
</el-form-item>
|
||||
|
||||
@@ -34,7 +34,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 = { subtotalAmount: 1200, additionalAmount: 80, reason: '补充已确认的附加费' }
|
||||
const payload = { expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '补充已确认的附加费' }
|
||||
|
||||
await expect(adjustPublishedInboundFee('10001', '88', payload, 'fee-adjust-1'))
|
||||
.resolves.toMatchObject({ feeVersion: 3 })
|
||||
@@ -54,7 +54,7 @@ describe('inbound fee API', () => {
|
||||
const error = await adjustPublishedInboundFee(
|
||||
'10001',
|
||||
'88',
|
||||
{ subtotalAmount: 1200, additionalAmount: 80, reason: '价格修正' },
|
||||
{ expectedFeeVersion: 2, chargeableQuantity: 140, unitPrice: 8.2, additionalAmount: 80, reason: '价格修正' },
|
||||
'fee-adjust-2',
|
||||
).catch((reason: unknown) => reason)
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@ export interface InboundFeeSnapshot {
|
||||
}
|
||||
|
||||
export interface AdjustPublishedInboundFeeRequest {
|
||||
subtotalAmount: number
|
||||
expectedFeeVersion: number
|
||||
chargeableQuantity: number
|
||||
unitPrice: number
|
||||
additionalAmount: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
@@ -110,6 +110,9 @@ describe('InboundOrdersView', () => {
|
||||
expect(vi.mocked(listPlatformInbounds).mock.calls).toHaveLength(requestsBeforeCustomerChange + 1)
|
||||
expect(listPlatformInbounds).toHaveBeenLastCalledWith('10001', expect.objectContaining({ page: 1, size: 20 }))
|
||||
expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' })
|
||||
const customerQueryField = (toolbar.props('fields') as Array<{ key: string; type?: string; props?: Record<string, unknown> }>).find((field) => field.key === 'customerId')
|
||||
expect(customerQueryField?.type).toBe('select')
|
||||
expect(customerQueryField?.props?.filterable).toBe(true)
|
||||
expect(document.body.textContent).toContain('RK-001')
|
||||
expect(document.body.textContent).toContain('查看')
|
||||
expect(document.body.textContent).toContain('示例企业有限公司')
|
||||
@@ -117,6 +120,18 @@ describe('InboundOrdersView', () => {
|
||||
expect(wrapper.find('.inbound-tabs .is-active').text()).toContain('全部')
|
||||
})
|
||||
|
||||
it('keeps the customer filter name-only and does not add a customer-code input', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/pages/InboundOrdersView.vue'), 'utf8')
|
||||
const queryFieldsSource = source.slice(source.indexOf('const allQueryFields'), source.indexOf('const columns'))
|
||||
|
||||
expect(queryFieldsSource).toContain("key: 'customerId'")
|
||||
expect(queryFieldsSource).toContain("type: 'select'")
|
||||
expect(queryFieldsSource).toContain('filterable: true')
|
||||
expect(queryFieldsSource).toContain('legalName?.trim() || customer.tenantName')
|
||||
expect(queryFieldsSource).not.toContain('tenantCode')
|
||||
expect(queryFieldsSource).not.toContain('客户编码')
|
||||
})
|
||||
|
||||
it('uses backend-supported warehouse IDs from tenant-scoped options without coercing Long IDs', async () => {
|
||||
const wrapper = mountPage()
|
||||
const toolbar = await selectCustomer(wrapper)
|
||||
|
||||
@@ -65,7 +65,7 @@ const tabs: Array<{ value: InboundListTab | ''; label: string; description: stri
|
||||
{ value: 'WAIT_MEASURE', label: '待测量', description: '等待测量或补正' },
|
||||
{ value: 'WAIT_REVIEW', label: '待审核', description: '等待测量审核' },
|
||||
{ value: 'WAIT_FEE_SEND', label: '待费用/入库', description: '等待费用编排完成' },
|
||||
{ value: 'WAIT_SPLIT', label: '待分柜', description: '费用已发布并完成入库,等待分柜' },
|
||||
{ value: 'WAIT_SPLIT', label: '待分柜', description: '测量审核完成,等待分柜;费用可继续维护' },
|
||||
]
|
||||
|
||||
const advancedFilterKeys = new Set(['orderStatus', 'feeReviewStatus', 'customerName', 'destinationCode', 'holdFlag'])
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('MeasurementReviewPanel', () => {
|
||||
expect(wrapper.text()).toContain('调整原因:去除托盘重量后采用净值')
|
||||
expect(wrapper.findComponent(DataTable).exists()).toBe(true)
|
||||
expect(wrapper.text()).not.toContain('租户')
|
||||
expect(wrapper.text()).toContain('通过并完成入库')
|
||||
expect(wrapper.text()).toContain('通过并进入待分柜')
|
||||
expect(wrapper.text()).not.toContain('确认费用')
|
||||
expect(getMeasurementReviewDetail).toHaveBeenCalledWith('10001', '90001')
|
||||
const source = await import('./MeasurementReviewPanel.vue?raw').then((module) => module.default)
|
||||
@@ -77,25 +77,37 @@ describe('MeasurementReviewPanel', () => {
|
||||
})
|
||||
|
||||
it('emits changed after a successful review write', async () => {
|
||||
vi.mocked(approveMeasurement).mockResolvedValue({ feeId: '1', feeNo: 'FEE-1', feeVersion: 1, feeStatus: 'SENT', inboundId: '90001', totalAmount: 100, currency: 'CNY', nextProcessStatus: 'WAIT_SPLIT' })
|
||||
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const buttons = wrapper.findAll('button')
|
||||
const approve = buttons.find((button) => button.text().includes('通过并完成入库'))
|
||||
const approve = buttons.find((button) => button.text().includes('通过并进入待分柜'))
|
||||
expect(approve).toBeDefined()
|
||||
await approve?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(ElMessageBox.confirm).toHaveBeenCalledWith(
|
||||
expect.stringContaining('自动试算并发布费用'),
|
||||
expect.stringContaining('初算失败不影响审核通过'),
|
||||
'确认测量审核通过',
|
||||
expect.objectContaining({ confirmButtonText: '通过并完成入库' }),
|
||||
expect.objectContaining({ confirmButtonText: '通过并进入待分柜' }),
|
||||
)
|
||||
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 7 })
|
||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('keeps approval successful when initial pricing fails', async () => {
|
||||
vi.mocked(approveMeasurement).mockResolvedValue({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'UNPRICED', initialPricingSucceeded: false, initialPricingMessage: '缺少有效价格', initialFee: null })
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('初算失败不阻断审核')
|
||||
})
|
||||
|
||||
it('rejects with the inbound optimistic-lock version instead of the measurement version', async () => {
|
||||
vi.mocked(rejectMeasurement).mockResolvedValue(undefined)
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
|
||||
@@ -145,20 +145,24 @@ async function confirmApprove() {
|
||||
if (!detail.value || !isPending.value || !canReview.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'审核通过后,系统会自动试算并发布费用,同时完成托盘库位占用和入库。客户无需再次确认费用。是否继续?',
|
||||
'审核通过后,入库单将进入待分柜。系统会尝试生成初算费用,但初算失败不影响审核通过,后续可由平台人员补充调整。是否继续?',
|
||||
'确认测量审核通过',
|
||||
{ confirmButtonText: '通过并完成入库', cancelButtonText: '返回', type: 'warning' },
|
||||
{ confirmButtonText: '通过并进入待分柜', cancelButtonText: '返回', type: 'warning' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
actionLoading.value = 'approve'
|
||||
try {
|
||||
await approveMeasurement(customerId.value, props.inboundId, {
|
||||
const result = await approveMeasurement(customerId.value, props.inboundId, {
|
||||
measurementId: detail.value.measurementId,
|
||||
expectedVersion: detail.value.inboundVersion,
|
||||
})
|
||||
ElMessage.success('审核已通过,费用发布和入库正在完成')
|
||||
if (result.initialPricingSucceeded) {
|
||||
ElMessage.success('审核已通过,入库单已进入待分柜,费用已完成初算')
|
||||
} else {
|
||||
ElMessage.warning(`审核已通过,入库单已进入待分柜;费用初算未完成${result.initialPricingMessage ? `:${result.initialPricingMessage}` : ''}`)
|
||||
}
|
||||
await reload()
|
||||
emit('changed')
|
||||
} catch (error) {
|
||||
@@ -295,10 +299,10 @@ defineExpose({ confirmApprove, openReasonDialog, isPending, isRejected, canRevie
|
||||
</div>
|
||||
|
||||
<div class="review-actions" :class="{ 'review-actions--disabled': !isPending && !isRejected }">
|
||||
<div><strong>审核结论</strong><span v-if="isPending">通过后将自动完成计费、费用发布和入库。</span><span v-else-if="isRejected">当前测量已驳回,可修正后重新提交,或按权限申请异常放行。</span><span v-else>当前记录不在可操作的测量审核状态。</span></div>
|
||||
<div><strong>审核结论</strong><span v-if="isPending">通过后进入待分柜;费用仅做初算,初算失败不阻断审核。</span><span v-else-if="isRejected">当前测量已驳回,可修正后重新提交,或按权限申请异常放行。</span><span v-else>当前记录不在可操作的测量审核状态。</span></div>
|
||||
<div class="action-buttons">
|
||||
<el-button v-if="isPending && canReview" type="danger" plain :loading="actionLoading === 'reject'" @click="openReasonDialog('reject')"><X :size="16" />驳回</el-button>
|
||||
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve'" @click="confirmApprove"><Check :size="16" />通过并完成入库</el-button>
|
||||
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve'" @click="confirmApprove"><Check :size="16" />通过并进入待分柜</el-button>
|
||||
<el-button v-if="isRejected && canRelease" type="warning" plain :loading="actionLoading === 'release'" @click="openReasonDialog('release')"><ShieldAlert :size="16" />异常放行</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type {
|
||||
ExceptionReleaseRequest,
|
||||
FeeSnapshotAfterApprove,
|
||||
MeasurementApprovalResponse,
|
||||
MeasurementReviewDetail,
|
||||
MeasurementReviewHistoryPage,
|
||||
MeasurementReviewHistoryPayload,
|
||||
@@ -63,7 +63,7 @@ export function approveMeasurement(
|
||||
request: MeasurementReviewRequest,
|
||||
key?: string,
|
||||
) {
|
||||
return postReview<FeeSnapshotAfterApprove>(
|
||||
return postReview<MeasurementApprovalResponse>(
|
||||
`/platform/v1/inbounds/${inboundId}/measurements/approve`,
|
||||
tenantId,
|
||||
request,
|
||||
|
||||
@@ -116,6 +116,16 @@ export interface ExceptionReleaseRequest {
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface MeasurementApprovalResponse {
|
||||
inboundId: string | null
|
||||
measurementId: string | null
|
||||
processStatus: 'WAIT_SPLIT' | string
|
||||
feeStatus: 'CALCULATED' | 'UNPRICED' | string
|
||||
initialPricingSucceeded: boolean
|
||||
initialPricingMessage: string | null
|
||||
initialFee: FeeSnapshotAfterApprove | null
|
||||
}
|
||||
|
||||
export interface FeeSnapshotAfterApprove {
|
||||
feeId: string | null
|
||||
feeNo: string | null
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
import type { PlatformTenantResource } from '@/modules/iam'
|
||||
import type { InboundListQuery } from './core/types'
|
||||
|
||||
export type InboundTaskMode = 'measurement' | 'review'
|
||||
|
||||
export interface ActiveCustomerPage {
|
||||
records: PlatformTenantResource[]
|
||||
pages: number
|
||||
}
|
||||
|
||||
export interface ActiveCustomerQuery {
|
||||
page: number
|
||||
size: number
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
|
||||
export async function loadAllActiveCustomers(
|
||||
fetchPage: (query: ActiveCustomerQuery) => Promise<ActiveCustomerPage>,
|
||||
) {
|
||||
const records: PlatformTenantResource[] = []
|
||||
let page = 1
|
||||
let pages = 1
|
||||
do {
|
||||
const response = await fetchPage({ page, size: 200, status: 'ACTIVE' })
|
||||
records.push(...response.records)
|
||||
pages = Math.max(1, response.pages)
|
||||
page += 1
|
||||
} while (page <= pages)
|
||||
return records
|
||||
}
|
||||
|
||||
export function buildInboundTaskQuery(
|
||||
mode: InboundTaskMode,
|
||||
query: Omit<InboundListQuery, 'tab' | 'processStatus' | 'measureReviewStatus'>,
|
||||
|
||||
Reference in New Issue
Block a user