fix(admin): clarify finance operating funds

This commit is contained in:
2026-08-31 20:37:08 +08:00
parent 4d5fd5cfe6
commit 623dfce255
5 changed files with 36 additions and 23 deletions
+1 -1
View File
@@ -126,7 +126,7 @@ export async function listFinanceAccounts(params: { page?: number; size?: number
const query = { page: params.page ?? 1, pageSize: params.size ?? 20, customerId: params.customerId || undefined, status: params.status || undefined }
const response = await http.get<ApiEnvelope<RawResource[]> & { page?: RawResource }, ApiEnvelope<RawResource[]> & { page?: RawResource }>(`${basePath}/accounts`, { params: query })
const data = unwrapApiResponse(response)
const records = data.map((item) => ({ customerId: stringValue(item.customerId), customerCode: stringValue(item.customerCode), customerName: stringValue(item.customerName), contactName: stringValue(item.contactName), contactPhone: stringValue(item.contactPhone), outstandingReceivable: numberValue(item.outstandingReceivable), pendingBilling: numberValue(item.pendingBilling), receivedInPeriod: numberValue(item.receivedInPeriod), totalCost: numberValue(item.totalCost), paidCost: numberValue(item.paidCost), outstandingPayable: numberValue(item.outstandingPayable), status: stringValue(item.status), lastChangedAt: stringValue(item.lastChangedAt) }))
const records: FinanceAccountSummary[] = data.map((item) => ({ customerId: stringValue(item.customerId), customerCode: stringValue(item.customerCode), customerName: stringValue(item.customerName), contactName: stringValue(item.contactName), contactPhone: stringValue(item.contactPhone), outstandingReceivable: numberValue(item.outstandingReceivable), pendingBilling: numberValue(item.pendingBilling), receivedInPeriod: numberValue(item.receivedInPeriod), totalCost: numberValue(item.totalCost), paidCost: numberValue(item.paidCost), outstandingPayable: numberValue(item.outstandingPayable), status: stringValue(item.status), lastChangedAt: stringValue(item.lastChangedAt) }))
const total = numberValue(response.page?.total ?? records.length)
const pageSize = numberValue(response.page?.pageSize ?? query.pageSize) || query.pageSize
return { records, total, pages: Math.max(1, Math.ceil(total / pageSize)) }
@@ -52,6 +52,7 @@ describe('finance six-page delivery contract', () => {
it('connects G1 customer accounts to the formal list and ledger operations', () => {
const page = source('src/modules/finance/pages/FinanceCustomerAccountsView.vue')
const types = source('src/modules/finance/types.ts')
expect(api).toContain('`${basePath}/accounts`')
expect(api).toContain('`${basePath}/accounts/${customerId}/ledger`')
expect(page).toContain('listFinanceAccounts')
@@ -62,6 +63,11 @@ describe('finance six-page delivery contract', () => {
expect(page).toContain('options: customers.value.filter')
expect(page).toContain('label: item.tenantName')
expect(page).not.toContain('tenantCode')
expect(types).toContain('export interface FinanceOperatingFundsSummary')
expect(types).toContain('export interface FinanceAccountSummary extends FinanceOperatingFundsSummary')
expect(api).toContain('const records: FinanceAccountSummary[]')
for (const metric of ['待收客户款', '待付供应商款', '已线下支付供应商款']) expect(page).toContain(metric)
for (const forbidden of ['客户线下支付', '已收客户款', '已付成本']) expect(page).not.toContain(forbidden)
const columns = page.slice(page.indexOf('const columns: DataTableColumn[]'))
expect(columns.indexOf("key: 'actions'")).toBeLessThan(columns.indexOf("key: 'customerName'"))
for (const state of ['客户账户加载失败', '暂无客户账户', '无权访问客户账户', '账户流水加载失败']) expect(page).toContain(state)
@@ -54,8 +54,8 @@ describe('FinanceCustomerAccountsView delivery', () => {
expect(page).toContain('返回客户账户')
expect(page).toContain('<DetailPageLayout')
expect(page).toContain('class="account-metrics"')
expect(page).toContain('待收金额')
expect(page).toContain('待开账金额')
for (const metric of ['待收客户款', '待付供应商款', '已线下支付供应商款']) expect(page).toContain(metric)
expect(page.match(/class="account-metric /g)).toHaveLength(3)
expect(page).toContain('class="ledger-query"')
expect(page).toContain('@search="ledgerPage = 1; void loadLedger($event)"')
expect(page).toContain('class="ledger-table"')
@@ -72,7 +72,8 @@ describe('FinanceCustomerAccountsView delivery', () => {
it('keeps term editing aligned with the account summary and nearby actions', () => {
for (const marker of ['class="term-section"', 'class="term-editor"', '账期天数', '#footer', '保存账期']) expect(page).toContain(marker)
expect(page).toContain('grid-template-columns: repeat(3, minmax(0, 1fr))')
for (const metric of ['总成本(CNY)', '已付成本(CNY)', '总成本', '已付成本', '待付金额']) expect(page).toContain(metric)
for (const column of ['总成本(CNY)', '待收客户款(CNY)', '待付供应商款(CNY)', '已线下支付供应商款(CNY)']) expect(page).toContain(column)
expect(page).not.toContain('已付成本')
expect(page).toContain('grid-template-columns: 120px minmax(0, 220px)')
expect(page).toContain('justify-content: start')
expect(page).toContain('class="surface-panel finance-account-detail__surface"')
@@ -103,8 +104,8 @@ describe('FinanceCustomerAccountsView delivery', () => {
})
it('shows only derived CNY summaries and no wallet actions', () => {
for (const marker of ['未收', '待出账', '本期已收', '应付', 'CNY']) expect(page).toContain(marker)
for (const forbidden of ['手动充值', '钱包', '外币', '发票', '利润']) expect(page).not.toContain(forbidden)
for (const marker of ['运营资金:待收客户款', '待付供应商款', '已线下支付供应商款', 'CNY']) expect(page).toContain(marker)
for (const forbidden of ['客户线下支付', '已收客户款', '手动充值', '钱包', '外币', '发票', '利润']) expect(page).not.toContain(forbidden)
})
it('selects an active customer by name from master data', () => {
@@ -151,6 +152,12 @@ describe('FinanceCustomerAccountsView delivery', () => {
expect(wrapper.text()).toContain('C-001')
expect(wrapper.text()).toContain('张三')
expect(wrapper.text()).toContain('13800000000')
const metrics = wrapper.find('.account-metrics')
expect(metrics.findAll('.account-metric')).toHaveLength(3)
expect(metrics.text()).toContain('待收客户款100.00CNY')
expect(metrics.text()).toContain('待付供应商款40.00CNY')
expect(metrics.text()).toContain('已线下支付供应商款50.00CNY')
for (const auxiliary of ['待开账金额', '本期已收', '总成本', '已付成本', '已收客户款', '客户线下支付']) expect(metrics.text()).not.toContain(auxiliary)
expect(listFinanceAccounts).toHaveBeenCalledWith({ page: 1, size: 200 })
expect(listFinanceAccountLedger).toHaveBeenCalledWith('10001', expect.objectContaining({ page: 1, size: 20 }))
wrapper.unmount()
@@ -18,7 +18,7 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/compone
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { getFinanceCustomerTerms, listFinanceAccountLedger, listFinanceAccounts, updateFinanceCustomerTerms } from '../api/finance'
import { financeBusinessTypeLabel } from '../status'
import type { FinanceAccountSummary, FinanceCustomerTerms, FinanceLedgerEntry } from '../types'
import type { FinanceAccountSummary, FinanceCustomerTerms, FinanceLedgerEntry, FinanceOperatingFundsSummary } from '../types'
const access = useAccessPreviewStore()
const route = useRoute()
@@ -65,12 +65,12 @@ const columns: DataTableColumn[] = [
{ key: 'customerCode', label: '客户编号', width: 150, slotName: 'customerCode' },
{ key: 'contactName', label: '联系人', width: 130, slotName: 'contactName' },
{ key: 'contactPhone', label: '联系电话', width: 150, slotName: 'contactPhone' },
{ key: 'outstandingReceivable', label: '待收(CNY)', width: 130 },
{ key: 'outstandingReceivable', label: '待收客户款(CNY)', width: 155 },
{ key: 'pendingBilling', label: '待开账(CNY)', width: 140 },
{ key: 'receivedInPeriod', label: '本期已收(CNY)', width: 140 },
{ key: 'totalCost', label: '总成本(CNY)', width: 140 },
{ key: 'paidCost', label: '已付成本(CNY)', width: 150 },
{ key: 'outstandingPayable', label: '待付(CNY)', width: 130 },
{ key: 'paidCost', label: '已线下支付供应商款(CNY)', width: 210 },
{ key: 'outstandingPayable', label: '待付供应商款(CNY)', width: 165 },
{ key: 'status', label: '客户状态', width: 110, slotName: 'status' },
{ key: 'lastChangedAt', label: '最近变动', width: 170, slotName: 'lastChangedAt' },
]
@@ -87,14 +87,11 @@ const ledgerColumns: DataTableColumn[] = [
{ key: 'documentNo', label: '关联单据', width: 220, slotName: 'documentNo' }, { key: 'receivableChange', label: '应收变动(CNY)', width: 145 }, { key: 'payableChange', label: '应付变动(CNY)', width: 145 },
{ key: 'outstandingReceivableAfter', label: '变动后未收(CNY)', width: 160 }, { key: 'operatorName', label: '操作人', width: 140, slotName: 'operatorName' }, { key: 'remark', label: '备注', minWidth: 180 },
]
const summary = computed(() => rows.value.reduce((result, row) => ({
receivable: result.receivable + row.outstandingReceivable,
pending: result.pending + row.pendingBilling,
received: result.received + row.receivedInPeriod,
totalCost: result.totalCost + row.totalCost,
const operatingFundsSummary = computed<FinanceOperatingFundsSummary>(() => rows.value.reduce((result, row) => ({
outstandingReceivable: result.outstandingReceivable + row.outstandingReceivable,
outstandingPayable: result.outstandingPayable + row.outstandingPayable,
paidCost: result.paidCost + row.paidCost,
payable: result.payable + row.outstandingPayable,
}), { receivable: 0, pending: 0, received: 0, totalCost: 0, paidCost: 0, payable: 0 }))
}), { outstandingReceivable: 0, outstandingPayable: 0, paidCost: 0 }))
async function load(model: QueryModel = filters.value) {
Object.assign(filters.value, model)
@@ -267,7 +264,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
<template #customerCode="{ row }">{{ customerField(row.customerCode) }}</template><template #contactName="{ row }">{{ customerField(row.contactName) }}</template><template #contactPhone="{ row }">{{ customerField(row.contactPhone) }}</template>
<template #lastChangedAt="{ row }">{{ formatLastChangedAt(row.lastChangedAt) }}</template>
<template #status="{ row }"><el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'">{{ row.status === 'ACTIVE' ? '正常' : '停用' }}</el-tag></template>
<template #pagination-left><span>汇总:未收 {{ money(summary.receivable) }} · 待出账 {{ money(summary.pending) }} · 本期已收 {{ money(summary.received) }} · 总成本 {{ money(summary.totalCost) }} · 已付成本 {{ money(summary.paidCost) }} · 待付 {{ money(summary.payable) }} CNY</span></template>
<template #pagination-left><span>运营资金:待收客户款 {{ money(operatingFundsSummary.outstandingReceivable) }} · 待付供应商款 {{ money(operatingFundsSummary.outstandingPayable) }} · 已线下支付供应商款 {{ money(operatingFundsSummary.paidCost) }} CNY</span></template>
</DataTable>
</section>
</ListPageLayout>
@@ -275,7 +272,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
<section class="surface-panel finance-account-detail__surface">
<DetailPageLayout class="finance-account-detail__layout" :aria-label="editing ? '维护客户账期' : '客户账户详情'">
<template #header><div class="detail-heading"><div class="detail-title"><h2>{{ editing ? '维护客户账期' : '客户账户详情' }}</h2><p v-if="selected">{{ customerName(selected.customerId, selected.customerName) }} · 客户财务往来总览</p></div><el-button class="detail-back" @click="closeDetail"><ArrowLeft :size="16" />返回客户账户</el-button></div></template>
<template v-if="selected && !ledgerError" #summary><section class="account-metrics" aria-label="客户账户汇总"><div class="account-metric is-receivable"><span>待收金额</span><strong>{{ money(selected.outstandingReceivable) }}</strong><small>CNY</small></div><div class="account-metric is-pending"><span>待开账金额</span><strong>{{ money(selected.pendingBilling) }}</strong><small>CNY</small></div><div class="account-metric is-received"><span>本期已收</span><strong>{{ money(selected.receivedInPeriod) }}</strong><small>CNY</small></div><div class="account-metric"><span>总成本</span><strong>{{ money(selected.totalCost) }}</strong><small>CNY</small></div><div class="account-metric is-received"><span>已付成本</span><strong>{{ money(selected.paidCost) }}</strong><small>CNY</small></div><div class="account-metric is-payable"><span>待付金额</span><strong>{{ money(selected.outstandingPayable) }}</strong><small>CNY</small></div></section></template>
<template v-if="selected && !ledgerError" #summary><section class="account-metrics" aria-label="客户账户运营资金"><div class="account-metric is-receivable"><span>待收客户款</span><strong>{{ money(selected.outstandingReceivable) }}</strong><small>CNY</small></div><div class="account-metric is-payable"><span>待付供应商款</span><strong>{{ money(selected.outstandingPayable) }}</strong><small>CNY</small></div><div class="account-metric is-received"><span>已线下支付供应商款</span><strong>{{ money(selected.paidCost) }}</strong><small>CNY</small></div></section></template>
<LoadErrorState v-if="ledgerError" :title="editing ? '客户账期加载失败' : '账户流水加载失败'" :message="ledgerError.message" :loading="ledgerLoading" @retry="retryDrawer"><template #details><ApiErrorMeta :details="ledgerError" /></template></LoadErrorState>
<template v-else-if="selected">
<FormSection class="customer-profile-section" title="客户信息" :bordered="false"><el-descriptions :column="3"><el-descriptions-item label="客户编号">{{ customerField(selected.customerCode) }}</el-descriptions-item><el-descriptions-item label="联系人">{{ customerField(selected.contactName) }}</el-descriptions-item><el-descriptions-item label="联系电话">{{ customerField(selected.contactPhone) }}</el-descriptions-item></el-descriptions></FormSection>
+7 -4
View File
@@ -175,18 +175,21 @@ export interface FinancePayableSettlement {
version: number
}
export interface FinanceAccountSummary {
export interface FinanceOperatingFundsSummary {
outstandingReceivable: number
outstandingPayable: number
paidCost: number
}
export interface FinanceAccountSummary extends FinanceOperatingFundsSummary {
customerId: string
customerCode: string
customerName: string
contactName: string
contactPhone: string
outstandingReceivable: number
pendingBilling: number
receivedInPeriod: number
totalCost: number
paidCost: number
outstandingPayable: number
status: string
lastChangedAt: string
}