feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选 - 引入 expense_claim_status_registry 统一报销状态流转 - 完善报销草稿流程、Item Sync 与本体解析器 - 优化总览页趋势图、分页组件与请求进度步骤 - 增强报销申请快速预览、本体工具与详情展示 - 新增半年报销模拟数据种子脚本与状态审计工具 - 补充财务看板、报销状态注册与模拟数据测试覆盖
This commit is contained in:
@@ -42,6 +42,14 @@ const SYSTEM_GENERATED_EXPENSE_TYPES = new Set(['travel_allowance'])
|
||||
const LONG_DISTANCE_TRAVEL_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket'])
|
||||
const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket', 'ship_ticket', 'ferry_ticket', 'ride_ticket'])
|
||||
const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set(['hotel_ticket'])
|
||||
const DOCUMENT_BACKED_EXPENSE_TYPES = new Set([
|
||||
'train_ticket',
|
||||
'flight_ticket',
|
||||
'ship_ticket',
|
||||
'ferry_ticket',
|
||||
'hotel_ticket',
|
||||
'ride_ticket'
|
||||
])
|
||||
const DOCUMENT_TYPE_APPLICATION = 'application'
|
||||
const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement'
|
||||
const RELATED_APPLICATION_STEP_LABEL = '关联单据'
|
||||
@@ -258,6 +266,83 @@ function resolveAttachmentDisplayName(value) {
|
||||
return normalized.split('/').filter(Boolean).pop() || normalized
|
||||
}
|
||||
|
||||
function hasRelatedApplicationContext(claim) {
|
||||
return Boolean(findRelatedApplicationEvent(claim))
|
||||
}
|
||||
|
||||
function isDocumentBackedRawExpenseItem(item) {
|
||||
const invoiceId = normalizeText(item?.invoice_id || item?.invoiceId)
|
||||
if (invoiceId) {
|
||||
return true
|
||||
}
|
||||
|
||||
return DOCUMENT_BACKED_EXPENSE_TYPES.has(normalizeExpenseType(item?.item_type || item?.itemType))
|
||||
}
|
||||
|
||||
function extractTravelDayCount(value) {
|
||||
const matched = normalizeText(value).replace(/\s+/g, '').match(/(\d{1,2})天/)
|
||||
return matched ? parseNumber(matched[1]) : 0
|
||||
}
|
||||
|
||||
function isStaleApplicationAllowanceRawItem(item, claim) {
|
||||
const itemType = normalizeExpenseType(item?.item_type || item?.itemType)
|
||||
if (itemType !== 'travel_allowance') {
|
||||
return false
|
||||
}
|
||||
|
||||
const related = resolveRelatedApplicationInfo(claim)
|
||||
const applicationDays = extractTravelDayCount(related?.days)
|
||||
const itemDays = extractTravelDayCount(item?.item_reason || item?.itemReason)
|
||||
return applicationDays > 0 && itemDays > 0 && applicationDays !== itemDays
|
||||
}
|
||||
|
||||
function isApplicationLinkPlaceholderRawItem(item, claim) {
|
||||
const itemType = normalizeExpenseType(item?.item_type || item?.itemType)
|
||||
if (SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const claimType = normalizeExpenseType(claim?.expense_type || claim?.expenseType)
|
||||
if (itemType && claimType && itemType !== claimType) {
|
||||
return false
|
||||
}
|
||||
|
||||
const reason = normalizeText(item?.item_reason || item?.itemReason)
|
||||
if (!reason || reason === '待补充') {
|
||||
return true
|
||||
}
|
||||
|
||||
const related = resolveRelatedApplicationInfo(claim)
|
||||
const linkedReasons = new Set([
|
||||
normalizeText(claim?.reason),
|
||||
normalizeText(related?.reason)
|
||||
].filter(Boolean))
|
||||
return linkedReasons.has(reason)
|
||||
}
|
||||
|
||||
function filterVisibleExpenseRawItems(items, claim) {
|
||||
const rawItems = Array.isArray(items) ? items : []
|
||||
if (!rawItems.length || !hasRelatedApplicationContext(claim)) {
|
||||
return rawItems
|
||||
}
|
||||
|
||||
const hasRealExpenseItem = rawItems.some((item) => (
|
||||
isDocumentBackedRawExpenseItem(item)
|
||||
&& !SYSTEM_GENERATED_EXPENSE_TYPES.has(normalizeExpenseType(item?.item_type || item?.itemType))
|
||||
))
|
||||
if (!hasRealExpenseItem) {
|
||||
return rawItems.filter((item) => !isApplicationLinkPlaceholderRawItem(item, claim))
|
||||
}
|
||||
|
||||
return rawItems.filter((item) => {
|
||||
const itemType = normalizeExpenseType(item?.item_type || item?.itemType)
|
||||
if (SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType)) {
|
||||
return !isStaleApplicationAllowanceRawItem(item, claim)
|
||||
}
|
||||
return !isApplicationLinkPlaceholderRawItem(item, claim)
|
||||
})
|
||||
}
|
||||
|
||||
function resolveApprovalMeta(status) {
|
||||
const normalized = String(status || '').trim().toLowerCase()
|
||||
|
||||
@@ -617,6 +702,33 @@ function resolveApplicationField(flag = {}, detail = {}, snakeKey, camelKey = ''
|
||||
)
|
||||
}
|
||||
|
||||
function resolveApplicationValue(flag = {}, detail = {}, keys = []) {
|
||||
for (const key of keys) {
|
||||
const detailValue = normalizeText(detail?.[key])
|
||||
if (detailValue) {
|
||||
return detailValue
|
||||
}
|
||||
const flagValue = normalizeText(flag?.[key])
|
||||
if (flagValue) {
|
||||
return flagValue
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function extractDateRange(value) {
|
||||
const dates = normalizeText(value).match(/\d{4}-\d{2}-\d{2}/g) || []
|
||||
if (!dates.length) {
|
||||
return { startDate: '', endDate: '' }
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: dates[0],
|
||||
endDate: dates[dates.length - 1]
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRelatedApplicationClaimNo(flag = {}) {
|
||||
const detail = normalizeApplicationHandoffDetail(flag)
|
||||
return resolveApplicationField(flag, detail, 'application_claim_no', 'applicationClaimNo')
|
||||
@@ -694,15 +806,41 @@ function resolveRelatedApplicationInfo(claim, typeLabel = '') {
|
||||
const rawTime = normalizeText(
|
||||
detail.application_time
|
||||
|| detail.applicationTime
|
||||
|| detail.application_business_time
|
||||
|| detail.applicationBusinessTime
|
||||
|| detail.business_time
|
||||
|| detail.businessTime
|
||||
|| detail.time_range
|
||||
|| detail.timeRange
|
||||
|| detail.time
|
||||
|| detail.application_date
|
||||
|| detail.applicationDate
|
||||
|| relatedEvent.application_time
|
||||
|| relatedEvent.applicationTime
|
||||
|| relatedEvent.application_business_time
|
||||
|| relatedEvent.applicationBusinessTime
|
||||
|| relatedEvent.business_time
|
||||
|| relatedEvent.businessTime
|
||||
|| relatedEvent.time_range
|
||||
|| relatedEvent.timeRange
|
||||
|| relatedEvent.application_date
|
||||
|| relatedEvent.applicationDate
|
||||
|| claim?.occurred_at
|
||||
)
|
||||
const displayTime = formatDate(rawTime) || rawTime
|
||||
const dateRange = extractDateRange(rawTime || displayTime)
|
||||
const ruleName = resolveApplicationValue(relatedEvent, detail, [
|
||||
'application_rule_name',
|
||||
'applicationRuleName',
|
||||
'rule_name',
|
||||
'ruleName'
|
||||
])
|
||||
const ruleVersion = resolveApplicationValue(relatedEvent, detail, [
|
||||
'application_rule_version',
|
||||
'applicationRuleVersion',
|
||||
'rule_version',
|
||||
'ruleVersion'
|
||||
])
|
||||
|
||||
return {
|
||||
id: resolveApplicationField(relatedEvent, detail, 'application_claim_id', 'applicationClaimId'),
|
||||
@@ -717,7 +855,9 @@ function resolveRelatedApplicationInfo(claim, typeLabel = '') {
|
||||
|| relatedEvent.applicationDays
|
||||
),
|
||||
location,
|
||||
time: formatDate(rawTime) || rawTime,
|
||||
time: displayTime,
|
||||
tripStartDate: dateRange.startDate,
|
||||
tripEndDate: dateRange.endDate,
|
||||
amountLabel: resolveRelatedApplicationAmountLabel(relatedEvent, detail, claim),
|
||||
statusLabel: resolveApplicationField(relatedEvent, detail, 'application_status_label', 'applicationStatusLabel'),
|
||||
transportMode: normalizeText(
|
||||
@@ -726,7 +866,34 @@ function resolveRelatedApplicationInfo(claim, typeLabel = '') {
|
||||
|| detail.transport_mode
|
||||
|| relatedEvent.application_transport_mode
|
||||
|| relatedEvent.applicationTransportMode
|
||||
)
|
||||
),
|
||||
lodgingDailyCap: resolveApplicationValue(relatedEvent, detail, [
|
||||
'application_lodging_daily_cap',
|
||||
'applicationLodgingDailyCap',
|
||||
'lodging_daily_cap',
|
||||
'lodgingDailyCap'
|
||||
]),
|
||||
subsidyDailyCap: resolveApplicationValue(relatedEvent, detail, [
|
||||
'application_subsidy_daily_cap',
|
||||
'applicationSubsidyDailyCap',
|
||||
'subsidy_daily_cap',
|
||||
'subsidyDailyCap'
|
||||
]),
|
||||
transportPolicy: resolveApplicationValue(relatedEvent, detail, [
|
||||
'application_transport_policy',
|
||||
'applicationTransportPolicy',
|
||||
'transport_policy',
|
||||
'transportPolicy'
|
||||
]),
|
||||
policyEstimate: resolveApplicationValue(relatedEvent, detail, [
|
||||
'application_policy_estimate',
|
||||
'applicationPolicyEstimate',
|
||||
'policy_estimate',
|
||||
'policyEstimate'
|
||||
]),
|
||||
ruleName,
|
||||
ruleVersion,
|
||||
ruleLabel: [ruleName, ruleVersion].filter(Boolean).join(' / ')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1056,7 +1223,8 @@ function buildExpenseItems(claim, riskSummary) {
|
||||
return []
|
||||
}
|
||||
|
||||
const sortedItems = [...claim.items].sort((left, right) => {
|
||||
const visibleItems = filterVisibleExpenseRawItems(claim.items, claim)
|
||||
const sortedItems = [...visibleItems].sort((left, right) => {
|
||||
const leftType = normalizeExpenseType(left?.item_type)
|
||||
const rightType = normalizeExpenseType(right?.item_type)
|
||||
return Number(SYSTEM_GENERATED_EXPENSE_TYPES.has(leftType)) - Number(SYSTEM_GENERATED_EXPENSE_TYPES.has(rightType))
|
||||
@@ -1121,9 +1289,17 @@ export function mapExpenseClaimToRequest(claim) {
|
||||
const workflowNode = resolveWorkflowNode(claim, approvalMeta, isApplicationDocument)
|
||||
const invoiceCount = Math.max(0, parseNumber(claim?.invoice_count))
|
||||
const riskSummary = buildRiskSummary(claim?.risk_flags_json)
|
||||
const expenseItems = buildExpenseItems(claim, riskSummary)
|
||||
const applyDateTime = claim?.submitted_at || claim?.created_at
|
||||
const relatedApplication = isApplicationDocument ? null : resolveRelatedApplicationInfo(claim, typeLabel)
|
||||
const expenseItems = buildExpenseItems(claim, riskSummary)
|
||||
const visibleExpenseAmount = expenseItems.reduce((sum, item) => sum + parseNumber(item.itemAmount), 0)
|
||||
const amountValue = relatedApplication
|
||||
? expenseItems.length
|
||||
? visibleExpenseAmount
|
||||
: invoiceCount === 0
|
||||
? 0
|
||||
: parseNumber(claim?.amount)
|
||||
: parseNumber(claim?.amount)
|
||||
const applyDateTime = claim?.submitted_at || claim?.created_at
|
||||
const employeeId = String(claim?.employee_id || claim?.employeeId || '').trim()
|
||||
const employeeName = String(claim?.employee_name || claim?.employeeName || '').trim()
|
||||
|
||||
@@ -1162,7 +1338,7 @@ export function mapExpenseClaimToRequest(claim) {
|
||||
submittedAt: applyDateTime || '',
|
||||
createdAt: claim?.created_at || '',
|
||||
updatedAt: claim?.updated_at || '',
|
||||
amount: parseNumber(claim?.amount),
|
||||
amount: amountValue,
|
||||
riskFlags: Array.isArray(claim?.risk_flags_json) ? claim.risk_flags_json : [],
|
||||
invoiceCount,
|
||||
workflowNode,
|
||||
|
||||
Reference in New Issue
Block a user