1
This commit is contained in:
@@ -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 } })
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user