This commit is contained in:
wujinliang
2026-08-27 21:48:15 +08:00
parent 550d8add97
commit b47323edf3
53 changed files with 624 additions and 82 deletions
+22
View File
@@ -56,6 +56,28 @@ describe('finance api', () => {
])
})
it('includes a supplier filter only for payable fee-pool requests', async () => {
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [] })
await listFinanceFeePool('10001', 'PAYABLE', '301')
expect(get).toHaveBeenCalledWith('/platform/finance/v1/fee-pool', {
params: { customerId: '10001', direction: 'PAYABLE', supplierId: '301' },
})
})
it('preserves settlement mode from fee-pool candidates for client-side eligibility checks', async () => {
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: [{
poolItemId: 'PAYABLE:42', customerId: 10001, direction: 'PAYABLE', sourceType: 'INBOUND_FEE', sourceId: 42,
sourceVersion: 1, feeItemName: '进仓费', amount: 33, currency: 'CNY', settlementMode: 'DIRECT_PAID',
status: 'AVAILABLE', createdAt: '2026-08-26T00:00:00',
}] })
const [candidate] = await listFinanceFeePool('10001', 'PAYABLE', '301')
expect(candidate.settlementMode).toBe('DIRECT_PAID')
})
it('uses the formal G1 account list path and fixed page DTO', async () => {
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', requestId: 'req-g1', traceId: 'trace-g1', data: [{ customerId: 10001, customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00' }], page: { page: 2, pageSize: 20, total: 21 } })
+5 -3
View File
@@ -111,12 +111,14 @@ export async function getFinanceCustomerAccountSummary(customerId: string): Prom
}
}
export async function listFinanceFeePool(customerId?: string, direction?: 'RECEIVABLE' | 'PAYABLE'): Promise<FinanceFeePoolItem[]> {
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/fee-pool`, { params: { customerId: customerId?.trim() || undefined, direction } })
export async function listFinanceFeePool(customerId?: string, direction?: 'RECEIVABLE' | 'PAYABLE', supplierId?: string): Promise<FinanceFeePoolItem[]> {
const params: Record<string, unknown> = { customerId: customerId?.trim() || undefined, direction }
if (supplierId?.trim()) params.supplierId = supplierId.trim()
const response = await http.get<ApiEnvelope<RawResource[]>, ApiEnvelope<RawResource[]>>(`${basePath}/fee-pool`, { params })
return unwrapApiResponse(response).map((item) => {
const poolItemId = stringValue(item.poolItemId)
const feeSourceId = stringValue(item.sourceType) === 'MANUAL_COST' ? null : (poolItemId.match(/^[A-Z_]+:(\d+)$/)?.[1] ?? null)
return { poolItemId, feeSourceId, customerId: stringValue(item.customerId), customerName: stringValue(item.customerName), direction: stringValue(item.direction) as FinanceFeePoolItem['direction'], sourceType: stringValue(item.sourceType), sourceId: stringValue(item.sourceId), sourceVersion: numberValue(item.sourceVersion), orderId: item.orderId == null ? null : stringValue(item.orderId), inboundId: item.inboundId == null ? null : stringValue(item.inboundId), containerId: item.containerId == null ? null : stringValue(item.containerId), feeItemName: stringValue(item.feeItemName), amount: numberValue(item.amount), currency: 'CNY', status: stringValue(item.status), createdAt: stringValue(item.createdAt) }
return { poolItemId, feeSourceId, customerId: stringValue(item.customerId), customerName: stringValue(item.customerName), direction: stringValue(item.direction) as FinanceFeePoolItem['direction'], sourceType: stringValue(item.sourceType), sourceId: stringValue(item.sourceId), sourceVersion: numberValue(item.sourceVersion), orderId: item.orderId == null ? null : stringValue(item.orderId), inboundId: item.inboundId == null ? null : stringValue(item.inboundId), containerId: item.containerId == null ? null : stringValue(item.containerId), feeItemName: stringValue(item.feeItemName), amount: numberValue(item.amount), currency: 'CNY', settlementMode: item.settlementMode == null ? null : stringValue(item.settlementMode), status: stringValue(item.status), createdAt: stringValue(item.createdAt) }
})
}
@@ -129,6 +129,13 @@ describe('FinanceCustomerArapView G2 operations', () => {
expect(adjustFinanceFeeSource).not.toHaveBeenCalled()
})
it('keeps the customer dropdown open while its option list is scrolled', async () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerArapView.vue'), 'utf8')
expect(source).not.toContain("document.addEventListener('scroll', closeCustomerSelectOnScroll, true)")
expect(source).not.toContain('input?.blur()')
expect(source).toContain("class: 'finance-customer-select'")
})
it('uses sourceSystem to suppress Firstleg editing when the source type is not in the legacy fallback list', async () => {
const firstlegOtherType = { ...feeSummary, sourceType: 'ORDER_FEE', reviewStatus: 'DRAFT' as const }
vi.mocked(listFinanceFeeSources).mockResolvedValue({ records: [firstlegOtherType], total: 1, pages: 1 })
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft, Ban, Download, Eye, FileCheck2, FilePlus2, Pencil, Send } from '@lucide/vue'
import { ElMessage } from 'element-plus'
@@ -54,7 +54,6 @@ const router = useRouter()
const customers = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const customersError = ref<ApiErrorDetails | null>(null)
const customerSelectOpen = ref(false)
const filters = ref<QueryModel>({ customerId: '', direction: '', status: '', reviewStatus: '', statementGenerated: '', businessRange: [], keyword: '' })
const allRows = ref<ArapRow[]>([])
const loading = ref(false)
@@ -94,15 +93,6 @@ function isFirstlegSource(source: { sourceSystem?: unknown; sourceType?: unknown
}
const stateError = computed(() => customersError.value || loadError.value)
const activeCustomers = computed(() => customers.value.filter((item) => item.status === 'ACTIVE'))
function onCustomerSelectVisible(visible: boolean) {
customerSelectOpen.value = visible
}
function closeCustomerSelectOnScroll() {
if (!customerSelectOpen.value || typeof document === 'undefined') return
const input = document.querySelector('.finance-customer-select input') as HTMLInputElement | null
input?.blur()
customerSelectOpen.value = false
}
const queryFields = computed<QueryField[]>(() => [
{
key: 'customerId',
@@ -115,7 +105,6 @@ const queryFields = computed<QueryField[]>(() => [
class: 'finance-customer-select',
popperClass: 'finance-customer-arap-popper',
teleported: false,
onVisibleChange: onCustomerSelectVisible,
filterable: true,
clearable: true,
loading: customersLoading.value,
@@ -343,15 +332,11 @@ async function restoreFeeSourceDetail(feeSourceId: string) {
}
onMounted(async () => {
document.addEventListener('scroll', closeCustomerSelectOnScroll, true)
const feeSourceId = String(route.params.feeSourceId || '')
if (route.name === 'finance-customer-arap-detail' && feeSourceId) { if (canListCustomers.value) await loadCustomers(); await restoreFeeSourceDetail(feeSourceId); return }
if (canListCustomers.value) await loadCustomers()
await load()
})
onBeforeUnmount(() => {
document.removeEventListener('scroll', closeCustomerSelectOnScroll, true)
})
watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSourceId], previous) => {
if (!previous) return
if (name === 'finance-customer-arap-detail' && feeSourceId) void restoreFeeSourceDetail(String(feeSourceId))
@@ -222,9 +222,38 @@ describe('FinanceCustomerStatementsView', () => {
expect(vm.filteredRows).toHaveLength(1)
vm.filters.containerNo = 'CONT-99'
await flushPromises()
expect(vm.filteredRows).toHaveLength(1)
await vm.load({
status: 'ISSUED', billNo: '501', customerId: '', containerNo: 'CONT-99',
periodStartRange: ['2026-08-01', '2026-08-02'], periodEndRange: ['2026-08-30', '2026-09-01'],
settled: 'false', lastPaymentRange: ['2026-08-20', '2026-08-20'],
})
await flushPromises()
expect(vm.filteredRows).toHaveLength(0)
})
it('keeps statement status filter options aligned with the list status column', async () => {
const { wrapper } = await mountPage()
const vm = wrapper.vm as unknown as {
fields: Array<{ key: string; label: string; placeholder?: string; options?: Array<{ label: string; value: string }> }>
columns: Array<{ key: string; label: string }>
}
const statusField = vm.fields.find((field) => field.key === 'status')
const statusColumn = vm.columns.find((column) => column.key === 'status')
expect(statusField).toMatchObject({ label: '对账单状态', placeholder: '请选择对账单状态' })
expect(statusField?.options).toEqual([
{ label: '草稿', value: 'DRAFT' },
{ label: '待审核', value: 'PENDING_REVIEW' },
{ label: '已审核', value: 'APPROVED' },
{ label: '已驳回', value: 'REJECTED' },
{ label: '已出账', value: 'ISSUED' },
{ label: '部分结清', value: 'PARTIALLY_PAID' },
{ label: '已结清', value: 'PAID' },
{ label: '已作废', value: 'VOIDED' },
])
expect(statusColumn).toEqual({ key: 'status', label: '对账单状态', width: 120, slotName: 'status' })
})
it('creates a formal statement export task with only non-empty filters', async () => {
const { wrapper } = await mountPage()
const vm = wrapper.vm as unknown as { filters: Record<string, unknown>; exportStatements(): Promise<void> }
@@ -18,7 +18,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { actionReconciliationStatement, createReconciliationStatement, getReconciliationStatement, listFinanceFeeSources, listFinanceStatements } from '../api/finance'
import { financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
import { financeBillStatusOptions, financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
import type { FinanceFeeSourceSummary, FinanceReconciliationDetail, FinanceStatementReadModel, ReconciliationActionPayload } from '../types'
interface AdjustmentRow {
@@ -47,6 +47,7 @@ const filters = ref<QueryModel>({
status: '', billNo: '', customerId: '', containerNo: '', periodStartRange: [], periodEndRange: [],
settled: '', lastPaymentRange: [], createdRange: [],
})
const appliedFilters = ref<QueryModel>({ ...filters.value })
const rows = ref<StatementRow[]>([])
const page = ref(1)
const pageSize = ref(20)
@@ -99,10 +100,7 @@ const selectedCreateAmount = computed(() => selectedCreateSources.value.reduce((
const detailRouteId = computed(() => route.name === 'finance-customer-statement-detail' ? String(route.params.statementId || '') : '')
const isDetailRoute = computed(() => Boolean(detailRouteId.value))
const fields = computed<QueryField[]>(() => [
{ key: 'status', type: 'select', label: '审核状态', width: 180, placeholder: '请选择审核状态', options: [
{ label: '草稿', value: 'DRAFT' }, { label: '待审核', value: 'PENDING_REVIEW' }, { label: '审核通过', value: 'APPROVED' },
{ label: '审核拒绝', value: 'REJECTED' }, { label: '已出账', value: 'ISSUED' }, { label: '已作废', value: 'VOIDED' },
] },
{ key: 'status', type: 'select', label: '对账单状态', width: 180, placeholder: '请选择对账单状态', options: financeBillStatusOptions },
{ key: 'billNo', type: 'input', label: '对账单号', width: 210, placeholder: '请输入对账单号' },
{ key: 'customerId', type: 'select', label: '客户名称', width: 260, placeholder: '请选择客户', props: { filterable: true, clearable: true }, options: customers.value.filter((item) => item.status === 'ACTIVE').map((item) => ({ label: item.tenantName, value: String(item.id) })) },
{ key: 'containerNo', type: 'input', label: '柜次号', width: 190, placeholder: canViewFeeSource.value ? '请输入柜次号' : '缺少费用来源查看权限', props: { disabled: !canViewFeeSource.value } },
@@ -114,7 +112,7 @@ const fields = computed<QueryField[]>(() => [
])
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 280, fixed: 'left', slotName: 'actions' },
{ key: 'status', label: '审核状态', width: 120, slotName: 'status' },
{ key: 'status', label: '对账单状态', width: 120, slotName: 'status' },
{ key: 'customerConfirmationStatus', label: '客户确认', width: 130, slotName: 'customerConfirmationStatus' },
{ key: 'billNo', label: '对账单号', minWidth: 180 },
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' },
@@ -208,9 +206,9 @@ function detailToRow(row: FinanceStatementReadModel, current?: FinanceReconcilia
}
}
function stringFilter(key: string) { return String(filters.value[key] || '').trim().toLowerCase() }
function stringFilter(key: string) { return String(appliedFilters.value[key] || '').trim().toLowerCase() }
function inDateRange(value: string, key: string) {
const range = filters.value[key]
const range = appliedFilters.value[key]
if (!Array.isArray(range) || range.length < 2 || !value) return !Array.isArray(range) || range.length < 2
const date = value.slice(0, 10)
return date >= String(range[0]) && date <= String(range[1])
@@ -251,6 +249,7 @@ async function load(model?: QueryModel) {
const hydrateDetails = model !== undefined
if (model) {
filters.value = { ...model }
appliedFilters.value = { ...model }
page.value = 1
}
rows.value = []
@@ -260,7 +259,7 @@ async function load(model?: QueryModel) {
error.value = null
loading.value = true
try {
const statements = await listFinanceStatements(String(filters.value.customerId || '').trim() || undefined)
const statements = await listFinanceStatements(String(appliedFilters.value.customerId || '').trim() || undefined)
const detailErrors: ApiErrorDetails[] = []
const hydrated = await Promise.all(statements.map(async (row) => {
if (!hydrateDetails) return detailToRow(row)
@@ -510,7 +509,7 @@ async function performAction(row: FinanceStatementReadModel, actionName: Reconci
try {
await actionReconciliationStatement(row.id, { action: actionName, expectedVersion: row.version, ...(reason ? { reason } : {}) })
ElMessage.success('对账单状态已更新')
await load(filters.value)
await load(appliedFilters.value)
} catch (cause) { error.value = getApiErrorDetails(cause, '对账单操作失败') }
finally { actionBusy.value = false }
}
@@ -540,7 +539,7 @@ async function submitAdjustment() {
})
adjustOpen.value = false
ElMessage.success('对账单调整已保存')
await load(filters.value)
await load(appliedFilters.value)
await openDetail(statement)
} catch (cause) { error.value = getApiErrorDetails(cause, '对账单调整失败') }
finally { adjustBusy.value = false }
@@ -648,7 +647,7 @@ watch(detailRouteId, (statementId) => {
</el-tabs>
</DetailPageLayout>
<ListPageLayout v-else aria-label="客户对账单">
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户对账单筛选" :fields="fields" preference-key="finance-statements-query" @search="load" /></template>
<template #toolbar><QueryToolbar v-model="filters" class="surface-panel" ariaLabel="客户对账单筛选" :fields="fields" preference-key="finance-statements-query" :search-on-clear="false" @search="load" /></template>
<template v-if="customerError || error" #state><LoadErrorState title="客户对账单加载失败" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState></template>
<template v-if="!customerError && !error">
<section class="surface-panel table-panel">
@@ -61,4 +61,8 @@ describe('formal settlement field contract', () => {
expect(pageSource).not.toContain('prop="sourceVersion" label="来源版本"')
expect(pageSource).not.toContain('{{ detail.version }}')
})
it('does not offer directly paid costs as payable settlement sources', () => {
expect(pageSource).toContain("item.settlementMode?.toUpperCase() !== 'DIRECT_PAID'")
})
})
@@ -49,6 +49,7 @@ const router = useRouter()
const access = useAccessPreviewStore()
const customers = ref<PlatformTenantResource[]>([])
const filters = ref<QueryModel>({ customerId: '', supplierName: '', settlementNo: '', status: '' })
const appliedFilters = ref<QueryModel>({ ...filters.value })
const rows = ref<SettlementRow[]>([])
const page = ref(1)
const pageSize = ref(20)
@@ -113,9 +114,9 @@ const fields = computed<QueryField[]>(() => [
{ key: 'settlementNo', type: 'input', label: '结算单号', width: 220, placeholder: '请输入结算单号' },
])
const filteredRows = computed(() => {
const status = String(filters.value.status || '')
const settlementNo = String(filters.value.settlementNo || '').trim().toLowerCase()
const supplierName = String(filters.value.supplierName || '')
const status = String(appliedFilters.value.status || '')
const settlementNo = String(appliedFilters.value.settlementNo || '').trim().toLowerCase()
const supplierName = String(appliedFilters.value.supplierName || '')
return rows.value.filter((item) => (!status || item.status === status)
&& (!settlementNo || item.settlementNo.toLowerCase().includes(settlementNo))
&& (!supplierName || ('supplierName' in item && item.supplierName === supplierName)))
@@ -233,13 +234,14 @@ async function loadCustomers() {
async function load(model?: QueryModel) {
if (model) {
filters.value = { ...model }
appliedFilters.value = { ...model }
page.value = 1
}
rows.value = []
error.value = null
loading.value = true
try {
const customerId = String(filters.value.customerId || '').trim() || undefined
const customerId = String(appliedFilters.value.customerId || '').trim() || undefined
rows.value = props.kind === 'receivable'
? await listFinanceReceivableSettlements(customerId)
: await listFinancePayableSettlements(customerId)
@@ -304,8 +306,13 @@ async function refreshCreateDependencies(customerId?: string) {
return
}
const fees = await listFinanceFeePool(selectedCustomerId, 'PAYABLE')
feeOptions.value = fees.filter((item) => item.feeSourceId && item.status === 'AVAILABLE')
const selectedSupplierId = String(createForm.value.supplierId || '').trim() || undefined
if (!selectedSupplierId) {
createForm.value.feeSourceIds = []
return
}
const fees = await listFinanceFeePool(selectedCustomerId, 'PAYABLE', selectedSupplierId)
feeOptions.value = fees.filter((item) => item.feeSourceId && item.status === 'AVAILABLE' && item.settlementMode?.toUpperCase() !== 'DIRECT_PAID')
if (createForm.value.supplierId && !supplierOptions.value.find((supplier) => String(supplier.id) === String(createForm.value.supplierId))) {
createForm.value.supplierId = ''
}
@@ -321,6 +328,12 @@ async function refreshCreateDependencies(customerId?: string) {
watch(() => createForm.value.customerId, (customerId) => {
if (createOpen.value) void refreshCreateDependencies(customerId)
})
watch(() => createForm.value.supplierId, () => {
if (!createOpen.value) return
createForm.value.feeSourceIds = []
feeOptions.value = []
if (createForm.value.customerId) void refreshCreateDependencies(createForm.value.customerId)
})
async function submitCreate() {
const customerId = String(createForm.value.customerId || '')
@@ -580,7 +593,7 @@ onMounted(() => {
</DetailPageLayout>
<ListPageLayout v-else :aria-label="title">
<template #toolbar>
<QueryToolbar v-model="filters" class="surface-panel" :ariaLabel="`${title}筛选`" :fields="fields" :preference-key="`finance-settlement-${kind}-query`" @search="load" />
<QueryToolbar v-model="filters" class="surface-panel" :ariaLabel="`${title}筛选`" :fields="fields" :preference-key="`finance-settlement-${kind}-query`" :search-on-clear="false" @search="load" />
</template>
<template v-if="customerError || error" #state>
<LoadErrorState :title="`${title}加载失败`" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState>
@@ -125,6 +125,25 @@ describe('Finance G3/G4 views', () => {
expect(createFinanceSettlementPayment).toHaveBeenCalledWith('701', expect.objectContaining({ amount: 100, voucherFileIds: ['9001'], expectedVersion: 4 }))
})
it('loads payable sources only for the selected supplier and clears them when the supplier changes', async () => {
const wrapper = await mountWithAccess(FinanceFormalSettlementPage, ['platform:finance:settlement-ap:list', 'platform:finance:settlement-ap:create'], { kind: 'payable' })
const vm = wrapper.vm as unknown as { openCreate(): Promise<void>; createForm: { customerId: string; supplierId: string; feeSourceIds: string[] } }
await vm.openCreate()
vm.createForm.customerId = '10001'
await flushPromises()
expect(listFinanceFeePool).not.toHaveBeenCalled()
vm.createForm.supplierId = '301'
await flushPromises()
expect(listFinanceFeePool).toHaveBeenCalledWith('10001', 'PAYABLE', '301')
vm.createForm.feeSourceIds = ['81']
vm.createForm.supplierId = ''
await flushPromises()
expect(vm.createForm.feeSourceIds).toEqual([])
})
it('renders request and trace ids when G3 detail loading fails', async () => {
vi.mocked(getReconciliationStatement).mockRejectedValueOnce(new ApiResponseError('对账单不可用', 'FIN_404', 404, undefined, 'req-g3-error', 'trace-g3-error'))
const wrapper = await mountWithAccess(FinanceCustomerStatementsView, ['platform:finance:customer-statement:list'])
@@ -149,4 +168,19 @@ describe('Finance G3/G4 views', () => {
expect(listFinanceReceivableSettlements).toHaveBeenLastCalledWith('10001')
expect(listFinancePayableSettlements).toHaveBeenLastCalledWith('10001')
})
it('applies settlement list text filters only after an explicit query', async () => {
const wrapper = await mountWithAccess(FinanceFormalSettlementPage, ['platform:finance:settlement-ap:list'], { kind: 'payable' })
const vm = wrapper.vm as unknown as {
filters: Record<string, unknown>
filteredRows: FinancePayableSettlement[]
load(model: Record<string, unknown>): Promise<void>
}
vm.filters.settlementNo = 'NOT-FOUND'
await flushPromises()
expect(vm.filteredRows).toHaveLength(1)
await vm.load({ customerId: '', supplierName: '', settlementNo: 'NOT-FOUND', status: '' })
await flushPromises()
expect(vm.filteredRows).toHaveLength(0)
})
})
@@ -75,11 +75,13 @@ describe('finance presentation governance', () => {
expect(arap).toContain("placeholder: '请选择客户'")
expect(arap).not.toContain("placeholder: '全部客户'")
expect(statements).toContain("placeholder: '请选择客户'")
expect(statements).toContain("placeholder: '请选择审核状态'")
expect(statements).toContain("placeholder: '请选择对账单状态'")
expect(statements).toContain(':search-on-clear="false"')
expect(statements).toContain("placeholder: '请选择结清状态'")
expect(statements).toContain("startPlaceholder: '请选择开始日期'")
expect(statements).toContain("endPlaceholder: '请选择结束日期'")
expect(statements).not.toContain("placeholder: '全部客户'")
expect(settlement).toContain(':search-on-clear="false"')
})
it('uses active-customer name selectors across all formal finance pages', () => {
+23 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { financeBusinessTypeLabel, financeDirectionLabel, financeDocumentTypeLabel, financeOperationActionLabel, financeOperationResultLabel, financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from './status'
import { financeArapStatusOptions, financeBillStatusOptions, financeBusinessTypeLabel, financeDirectionLabel, financeDocumentTypeLabel, financeOperationActionLabel, financeOperationResultLabel, financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from './status'
describe('finance status presentation', () => {
it.each([
@@ -47,4 +47,26 @@ describe('finance status presentation', () => {
expect(financeDocumentTypeLabel('NEW_DOCUMENT')).toBe('未知单据类型')
expect(financeOperationActionLabel('NEW_ACTION')).toBe('未知操作')
})
it('uses review-status values for the customer AR/AP review filter', () => {
expect(financeArapStatusOptions).toEqual([
{ label: '草稿', value: 'DRAFT' },
{ label: '待审核', value: 'PENDING' },
{ label: '已审核', value: 'APPROVED' },
{ label: '已驳回', value: 'REJECTED' },
])
})
it('uses the complete reconciliation statement lifecycle for statement filters', () => {
expect(financeBillStatusOptions).toEqual([
{ label: '草稿', value: 'DRAFT' },
{ label: '待审核', value: 'PENDING_REVIEW' },
{ label: '已审核', value: 'APPROVED' },
{ label: '已驳回', value: 'REJECTED' },
{ label: '已出账', value: 'ISSUED' },
{ label: '部分结清', value: 'PARTIALLY_PAID' },
{ label: '已结清', value: 'PAID' },
{ label: '已作废', value: 'VOIDED' },
])
})
})
+6 -7
View File
@@ -31,20 +31,19 @@ const financeStatuses: Record<string, FinanceStatusDefinition> = {
export const financeBillStatusOptions = [
{ label: '草稿', value: 'DRAFT' },
{ label: '待审核', value: 'PENDING_REVIEW' },
{ label: '已出账', value: 'ISSUED' },
{ label: '已审核', value: 'APPROVED' },
{ label: '已驳回', value: 'REJECTED' },
{ label: '已出账', value: 'ISSUED' },
{ label: '部分结清', value: 'PARTIALLY_PAID' },
{ label: '已结清', value: 'PAID' },
{ label: '已作废', value: 'VOIDED' },
]
export const financeArapStatusOptions = [
...financeBillStatusOptions,
{ label: '待结算', value: 'AVAILABLE' },
{ label: '结算锁定', value: 'LOCKED' },
{ label: '已入账', value: 'BILLED' },
{ label: '已登记', value: 'RECORDED' },
{ label: '已确认', value: 'CONFIRMED' },
{ label: '草稿', value: 'DRAFT' },
{ label: '待审核', value: 'PENDING' },
{ label: '已审核', value: 'APPROVED' },
{ label: '已驳回', value: 'REJECTED' },
]
function normalizeStatus(status: unknown) {
+2 -1
View File
@@ -1,4 +1,4 @@
export type FinanceBillStatus = 'DRAFT' | 'ISSUED' | 'PARTIALLY_PAID' | 'PAID' | 'VOIDED'
export type FinanceBillStatus = 'DRAFT' | 'PENDING_REVIEW' | 'APPROVED' | 'REJECTED' | 'ISSUED' | 'PARTIALLY_PAID' | 'PAID' | 'VOIDED'
export type FinanceCostType = 'SINGLE_ORDER' | 'CONTAINER_SHARED'
export type FinancePaymentMethod = 'BANK_TRANSFER' | 'CASH' | 'OTHER'
export type FinanceFeeDirection = 'RECEIVABLE' | 'PAYABLE'
@@ -59,6 +59,7 @@ export interface FinanceFeePoolItem {
feeItemName: string
amount: number
currency: 'CNY'
settlementMode?: string | null
status: string
createdAt: string
}