优化
This commit is contained in:
@@ -31,6 +31,19 @@ describe('container fee API', () => {
|
||||
expect(listSuppliers).toHaveBeenCalledWith({ status: 'ACTIVE', category: 'TRUCKING', page: 1, size: 200 })
|
||||
})
|
||||
|
||||
it('loads every ACTIVE supplier without a category filter when the fee item category is empty', async () => {
|
||||
vi.mocked(listSuppliers).mockResolvedValue({ records: [
|
||||
{ id: '41', name: '承运商A', status: 'ACTIVE', category: 'TRUCKING' },
|
||||
{ id: '42', name: '停用承运商', status: 'INACTIVE', category: 'TRUCKING' },
|
||||
{ id: '43', name: '报关行', status: 'ACTIVE', category: 'CUSTOMS' },
|
||||
], page: 1, size: 200, total: 3, pages: 1 } as never)
|
||||
await expect(listContainerFeeSupplierOptions(null)).resolves.toEqual([
|
||||
{ id: '41', supplierName: '承运商A', category: 'TRUCKING' },
|
||||
{ id: '43', supplierName: '报关行', category: 'CUSTOMS' },
|
||||
])
|
||||
expect(listSuppliers).toHaveBeenCalledWith({ status: 'ACTIVE', category: undefined, page: 1, size: 200 })
|
||||
})
|
||||
|
||||
it('reads allocation rows from Finance and never calculates them', async () => {
|
||||
const get = vi.spyOn(http, 'get')
|
||||
.mockResolvedValueOnce({ code: '200', data: [{ id: 81, customerId: 11, containerId: 99 }, { id: 82, customerId: 11, containerId: 100 }] })
|
||||
|
||||
@@ -83,7 +83,11 @@ export interface ContainerCostDetail {
|
||||
const id = (value: unknown) => value == null ? '' : String(value)
|
||||
const numberValue = (value: unknown) => Number.isFinite(Number(value)) ? Number(value) : 0
|
||||
const supplierCategories: SupplierCategory[] = ['BOOKING', 'CUSTOMS', 'TRUCKING', 'CERTIFICATE']
|
||||
const supplierCategory = (value: unknown): SupplierCategory | null => supplierCategories.includes(value as SupplierCategory) ? value as SupplierCategory : null
|
||||
function supplierCategory(value: unknown): SupplierCategory | null {
|
||||
if (value == null || value === '') return null
|
||||
if (supplierCategories.includes(value as SupplierCategory)) return value as SupplierCategory
|
||||
throw new TypeError(`Unsupported supplierCategory: ${String(value)}`)
|
||||
}
|
||||
const base = (planId: string) => `/platform/v1/loading-plans/${encodeURIComponent(planId)}/fee-details`
|
||||
const params = (accountId: string) => ({ accountId })
|
||||
|
||||
@@ -134,9 +138,9 @@ export async function listApplicableFeeItems() {
|
||||
return (unwrapApiResponse(response).records || []).map((raw) => ({ id: id(raw.id), name: String(raw.feeTypeName || ''), feeCode: String(raw.feeCode || ''), supplierCategory: supplierCategory(raw.supplierCategory), warehouseReceivable: Boolean(raw.warehouseReceivable), warehousePayable: Boolean(raw.warehousePayable), pickupReceivable: Boolean(raw.pickupReceivable), pickupPayable: Boolean(raw.pickupPayable) }))
|
||||
}
|
||||
|
||||
export async function listContainerFeeSupplierOptions(category: SupplierCategory): Promise<ContainerFeeSupplierOption[]> {
|
||||
const page = await listSuppliers({ status: 'ACTIVE', category, page: 1, size: 200 })
|
||||
return page.records.filter((supplier) => supplier.status === 'ACTIVE' && supplier.category === category).map((supplier) => ({ id: supplier.id, supplierName: supplier.name, category: supplier.category }))
|
||||
export async function listContainerFeeSupplierOptions(category: SupplierCategory | null): Promise<ContainerFeeSupplierOption[]> {
|
||||
const page = await listSuppliers({ status: 'ACTIVE', category: category ?? undefined, page: 1, size: 200 })
|
||||
return page.records.filter((supplier) => supplier.status === 'ACTIVE' && (!category || supplier.category === category)).map((supplier) => ({ id: supplier.id, supplierName: supplier.name, category: supplier.category }))
|
||||
}
|
||||
|
||||
export async function listContainerCostDetails(customerId: string, containerId: string): Promise<ContainerCostDetail[]> {
|
||||
|
||||
@@ -175,15 +175,16 @@ describe('ContainerFeesPanel contract', () => {
|
||||
expect(createContainerFee).not.toHaveBeenCalledWith(expect.anything(), expect.anything(), expect.objectContaining({ partnerId: expect.anything() }))
|
||||
})
|
||||
|
||||
it('fails closed when the payable fee item has no supplier category', async () => {
|
||||
it('allows any loaded ACTIVE supplier when the payable fee item has no supplier category', async () => {
|
||||
vi.mocked(listApplicableFeeItems).mockResolvedValue([{ id: '7', name: '未映射费用', feeCode: 'UNMAPPED', supplierCategory: null, warehouseReceivable: false, warehousePayable: true, pickupReceivable: false, pickupPayable: false }])
|
||||
vi.mocked(listContainerFeeSupplierOptions).mockResolvedValue([{ id: '41', supplierName: '报关行', category: 'CUSTOMS' }])
|
||||
const wrapper = await mountPanel(['firstleg:inbound:fee-detail:view', 'firstleg:inbound:fee-detail:edit', 'firstleg:fee-item:view', 'platform:supplier:list'])
|
||||
const state = (wrapper.vm.$ as unknown as { setupState: { openCreate(): void; save(): Promise<void>; form: { direction: string; feeItemId: string; supplierId: string | null; amount: number } } }).setupState
|
||||
state.openCreate(); await flushPromises()
|
||||
state.form.direction = 'PAYABLE'; state.form.feeItemId = '7'; state.form.supplierId = '41'; state.form.amount = 100; await flushPromises()
|
||||
state.form.direction = 'PAYABLE'; state.form.feeItemId = '7'; await flushPromises(); state.form.supplierId = '41'; state.form.amount = 100
|
||||
await state.save(); await flushPromises()
|
||||
expect(createContainerFee).not.toHaveBeenCalled()
|
||||
expect(document.body.textContent).toContain('该应付费用项目未配置供应商类别,已阻断保存')
|
||||
expect(listContainerFeeSupplierOptions).toHaveBeenCalledWith(null)
|
||||
expect(createContainerFee).toHaveBeenCalledWith('9', '11', expect.objectContaining({ direction: 'PAYABLE', supplierId: '41' }))
|
||||
})
|
||||
|
||||
it('fails closed when a supplier candidate does not match the fee-item category', async () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ const feeItemsError = ref<ApiErrorDetails | null>(null)
|
||||
const suppliersError = ref<ApiErrorDetails | null>(null)
|
||||
const feeItemsLoading = ref(false)
|
||||
const suppliersLoading = ref(false)
|
||||
const loadedSupplierCategory = ref<string | null>(null)
|
||||
const loadedSupplierCategory = ref<string | null | undefined>(undefined)
|
||||
const editorOpen = ref(false)
|
||||
const editorError = ref<ApiErrorDetails | null>(null)
|
||||
const rejectOpen = ref(false)
|
||||
@@ -87,9 +87,10 @@ async function loadFeeItems() {
|
||||
|
||||
async function loadSuppliers() {
|
||||
const sequence = ++supplierLoadSequence
|
||||
suppliers.value = []; loadedSupplierCategory.value = null; suppliersError.value = null
|
||||
const category = selectedFeeItem.value?.supplierCategory
|
||||
if (!canViewSuppliers.value || form.direction !== 'PAYABLE' || !category) return
|
||||
suppliers.value = []; loadedSupplierCategory.value = undefined; suppliersError.value = null
|
||||
const item = selectedFeeItem.value
|
||||
if (!canViewSuppliers.value || form.direction !== 'PAYABLE' || !item) return
|
||||
const category = item.supplierCategory
|
||||
suppliersLoading.value = true
|
||||
try {
|
||||
const result = await listContainerFeeSupplierOptions(category)
|
||||
@@ -148,12 +149,11 @@ async function save() {
|
||||
} finally { saving.value = false }
|
||||
return
|
||||
}
|
||||
if (form.direction === 'PAYABLE' && !selectedFeeItem.value?.supplierCategory) { ElMessage.warning('该应付费用项目未配置供应商类别,已阻断保存'); return }
|
||||
if (form.direction === 'PAYABLE' && suppliersError.value) { ElMessage.warning('供应商列表加载失败,无法校验应付费用供应商'); return }
|
||||
if (form.direction === 'PAYABLE' && loadedSupplierCategory.value !== selectedFeeItem.value?.supplierCategory) { ElMessage.warning('供应商候选尚未按费用项目类别加载完成,无法保存'); return }
|
||||
if (form.direction === 'PAYABLE' && !form.supplierId) { ElMessage.warning('应付费用必须选择供应商'); return }
|
||||
const supplier = suppliers.value.find((item) => item.id === String(form.supplierId))
|
||||
if (form.direction === 'PAYABLE' && (!supplier || supplier.category !== selectedFeeItem.value?.supplierCategory)) { ElMessage.warning('所选供应商与费用项目要求的供应商类别不匹配'); return }
|
||||
if (form.direction === 'PAYABLE' && (!supplier || (selectedFeeItem.value?.supplierCategory && supplier.category !== selectedFeeItem.value.supplierCategory))) { ElMessage.warning('所选供应商与费用项目要求的供应商类别不匹配'); return }
|
||||
const payload: ContainerFeeSaveRequest = { ...form, supplierId: form.direction === 'PAYABLE' ? form.supplierId : null }
|
||||
saving.value = true
|
||||
try {
|
||||
@@ -181,7 +181,7 @@ function initializeCustomer() { accountId.value = customerOptions.value.length =
|
||||
function clearCustomerContext() {
|
||||
loadSequence += 1
|
||||
supplierLoadSequence += 1
|
||||
fees.value = []; costs.value = []; feeItems.value = []; suppliers.value = []; error.value = null; feeItemsError.value = null; suppliersError.value = null; loading.value = false; feeItemsLoading.value = false; suppliersLoading.value = false; loadedSupplierCategory.value = null
|
||||
fees.value = []; costs.value = []; feeItems.value = []; suppliers.value = []; error.value = null; feeItemsError.value = null; suppliersError.value = null; loading.value = false; feeItemsLoading.value = false; suppliersLoading.value = false; loadedSupplierCategory.value = undefined
|
||||
editorOpen.value = false; rejectOpen.value = false; target.value = null; rejection.value = ''
|
||||
resetForm()
|
||||
}
|
||||
@@ -189,10 +189,10 @@ watch(customerOptions, initializeCustomer, { immediate: true })
|
||||
watch(accountId, () => { clearCustomerContext(); void load() })
|
||||
watch(() => form.direction, (direction) => {
|
||||
if (!applicableItems.value.some((item) => item.id === form.feeItemId)) form.feeItemId = ''
|
||||
if (direction === 'RECEIVABLE') { supplierLoadSequence += 1; form.supplierId = null; suppliers.value = []; loadedSupplierCategory.value = null; suppliersError.value = null; suppliersLoading.value = false }
|
||||
if (direction === 'RECEIVABLE') { supplierLoadSequence += 1; form.supplierId = null; suppliers.value = []; loadedSupplierCategory.value = undefined; suppliersError.value = null; suppliersLoading.value = false }
|
||||
else void loadSuppliers()
|
||||
})
|
||||
watch(() => form.feeItemId, () => { form.supplierId = null; suppliers.value = []; loadedSupplierCategory.value = null; suppliersError.value = null; if (form.direction === 'PAYABLE') void loadSuppliers() })
|
||||
watch(() => form.feeItemId, () => { form.supplierId = null; suppliers.value = []; loadedSupplierCategory.value = undefined; suppliersError.value = null; if (form.direction === 'PAYABLE') void loadSuppliers() })
|
||||
onMounted(() => void load())
|
||||
</script>
|
||||
|
||||
@@ -242,7 +242,7 @@ onMounted(() => void load())
|
||||
<template v-if="!target">
|
||||
<el-form-item label="方向" prop="direction"><el-segmented v-model="form.direction" :options="[{ label: '应收', value: 'RECEIVABLE' }, { label: '应付', value: 'PAYABLE', disabled: !canViewSuppliers }]" /></el-form-item>
|
||||
<el-form-item label="费用项目" prop="feeItemId"><el-select v-model="form.feeItemId" filterable :loading="feeItemsLoading" :disabled="!canViewFeeItems" placeholder="请选择启用且适用的费用项目"><el-option v-for="item in applicableItems" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
|
||||
<el-form-item v-if="form.direction === 'PAYABLE'" label="供应商" required><el-select v-model="form.supplierId" filterable :loading="suppliersLoading" :disabled="!canViewSuppliers || !selectedFeeItem?.supplierCategory || !!suppliersError" placeholder="请先选择已配置供应商类别的费用项目"><el-option v-for="supplier in suppliers" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" /></el-select></el-form-item>
|
||||
<el-form-item v-if="form.direction === 'PAYABLE'" label="供应商" required><el-select v-model="form.supplierId" filterable :loading="suppliersLoading" :disabled="!canViewSuppliers || !selectedFeeItem || !!suppliersError" placeholder="请先选择费用项目"><el-option v-for="supplier in suppliers" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" /></el-select></el-form-item>
|
||||
</template>
|
||||
<el-form-item label="业务日期" prop="businessDate"><el-date-picker v-model="form.businessDate" type="date" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="金额(CNY)" prop="amount"><el-input-number v-model="form.amount" :min="0" :precision="2" controls-position="right" /></el-form-item>
|
||||
|
||||
Reference in New Issue
Block a user