Files
X-Financial/server/tests/test_finance_dashboard_service.py
caoxiaozhu 0c74b4ab4a feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选
- 引入 expense_claim_status_registry 统一报销状态流转
- 完善报销草稿流程、Item Sync 与本体解析器
- 优化总览页趋势图、分页组件与请求进度步骤
- 增强报销申请快速预览、本体工具与详情展示
- 新增半年报销模拟数据种子脚本与状态审计工具
- 补充财务看板、报销状态注册与模拟数据测试覆盖
2026-06-02 16:22:59 +08:00

337 lines
14 KiB
Python

from __future__ import annotations
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.db.base import Base
from app.models.budget import BudgetAllocation, BudgetTransaction
from app.models.financial_record import ExpenseClaim
from app.models.risk_observation import RiskObservation
from app.services.finance_dashboard import FinanceDashboardService
def build_session() -> Session:
engine = create_engine(
"sqlite+pysqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=engine)
session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
return session_factory()
def test_finance_dashboard_service_aggregates_claim_budget_and_payment_data() -> None:
now = datetime.now(UTC)
with build_session() as db:
db.add_all(
[
ExpenseClaim(
claim_no="CLM-DASH-001",
employee_name="陈雨晴",
department_name="财务部",
expense_type="travel",
reason="项目差旅",
location="广州",
amount=Decimal("1200.00"),
invoice_count=2,
occurred_at=now - timedelta(hours=4),
submitted_at=now - timedelta(hours=3),
status="submitted",
approval_stage="finance_review",
risk_flags_json=[],
hermes_risk_flag=False,
created_at=now - timedelta(hours=4),
updated_at=now - timedelta(hours=3),
),
ExpenseClaim(
claim_no="CLM-DASH-002",
employee_name="顾成宇",
department_name="研发中心",
expense_type="meal",
reason="客户招待",
location="深圳",
amount=Decimal("800.00"),
invoice_count=1,
occurred_at=now - timedelta(days=1, hours=2),
submitted_at=now - timedelta(days=1, hours=1),
status="paid",
approval_stage="payment",
risk_flags_json=[{"label": "招待费超标"}],
hermes_risk_flag=False,
created_at=now - timedelta(days=1, hours=2),
updated_at=now - timedelta(days=1),
),
ExpenseClaim(
claim_no="CLM-DASH-003",
employee_name="李文静",
department_name="行政部",
expense_type="office",
reason="办公用品",
location="珠海",
amount=Decimal("5000.00"),
invoice_count=3,
occurred_at=now - timedelta(hours=1),
submitted_at=None,
status="draft",
approval_stage=None,
risk_flags_json=[],
hermes_risk_flag=False,
created_at=now - timedelta(hours=1),
updated_at=now - timedelta(hours=1),
),
ExpenseClaim(
claim_no="AP-DASH-ADMIN-001",
employee_name="admin",
department_name="Finance",
expense_type="travel_application",
reason="admin pre-approval should not enter reimbursement metrics",
location="Shanghai",
amount=Decimal("999999.00"),
invoice_count=1,
occurred_at=now - timedelta(minutes=20),
submitted_at=now - timedelta(minutes=10),
status="paid",
approval_stage="payment",
risk_flags_json=[],
hermes_risk_flag=False,
created_at=now - timedelta(minutes=20),
updated_at=now - timedelta(minutes=10),
),
]
)
db.add(
RiskObservation(
observation_key="risk-dashboard-001",
subject_type="expense_claim",
subject_key="CLM-DASH-002",
subject_label="CLM-DASH-002",
claim_no="CLM-DASH-002",
risk_type="policy",
risk_signal="amount_outlier",
title="金额异常",
risk_level="high",
status="pending_review",
created_at=now - timedelta(hours=2),
updated_at=now - timedelta(hours=2),
)
)
allocation = BudgetAllocation(
budget_no="BUD-DASH-001",
fiscal_year=now.year,
period_type="year",
period_key=f"{now.year}",
department_name="财务部",
subject_code="travel",
subject_name="差旅费",
original_amount=Decimal("10000.00"),
adjusted_amount=Decimal("0.00"),
status="active",
warning_threshold=Decimal("80.00"),
control_action="warn",
)
db.add(allocation)
db.flush()
db.add(
BudgetTransaction(
transaction_no="BTX-DASH-001",
allocation_id=allocation.id,
source_type="expense_claim",
source_id="CLM-DASH-002",
source_no="CLM-DASH-002",
transaction_type="consume",
amount=Decimal("4000.00"),
before_available_amount=Decimal("10000.00"),
after_available_amount=Decimal("6000.00"),
operator="finance",
reason="测试消耗",
created_at=now - timedelta(hours=1),
)
)
db.commit()
dashboard = FinanceDashboardService(db).build_dashboard(
range_key="近10日",
trend_range="近7天",
department_range="本月",
)
assert dashboard.has_real_data is True
assert dashboard.totals["reimbursementCount"] == 2
assert dashboard.totals["reimbursementAmount"] == 2000.0
assert dashboard.totals["pendingPaymentAmount"] == 0.0
assert dashboard.trend["applications"][-1] >= 1
assert "AP-DASH-ADMIN-001" not in str(dashboard.trend)
assert dashboard.spend_by_category[0]["value"] == 1200.0
assert dashboard.department_ranking[0]["name"] == "财务部"
assert dashboard.department_ranking[0]["amount"] == 1200.0
assert dashboard.employee_ranking[0]["name"] == "陈雨晴"
assert dashboard.top_claims[0]["claimNo"] == "CLM-DASH-001"
assert "AP-DASH-ADMIN-001" not in str(dashboard.top_claims)
assert dashboard.budget_summary["ratio"] == 40.0
assert dashboard.budget_summary["used"] == "¥4,000"
metric_labels = {item["label"] for item in dashboard.budget_metrics}
assert {"预算池数量", "总预算", "已用预算", "可用预算", "预警预算池"}.issubset(
metric_labels
)
def test_finance_dashboard_uses_financial_terms_instead_of_approval_terms() -> None:
now = datetime.now(UTC)
with build_session() as db:
db.add_all(
[
ExpenseClaim(
claim_no="CLM-DASH-LABEL-001",
employee_name="林嘉宁",
department_name="市场部",
expense_type="travel_application",
reason="客户拜访差旅",
location="上海",
amount=Decimal("700.00"),
invoice_count=1,
occurred_at=now - timedelta(hours=2),
submitted_at=now - timedelta(hours=1),
status="submitted",
approval_stage="finance_review",
risk_flags_json=[{"type": "budget_pressure"}],
hermes_risk_flag=False,
created_at=now - timedelta(hours=2),
updated_at=now - timedelta(hours=1),
),
ExpenseClaim(
claim_no="CLM-DASH-LABEL-002",
employee_name="周思远",
department_name="财务部",
expense_type="meal",
reason="客户沟通",
location="杭州",
amount=Decimal("300.00"),
invoice_count=1,
occurred_at=now - timedelta(days=1),
submitted_at=now - timedelta(days=1),
status="paid",
approval_stage="payment",
risk_flags_json=[],
hermes_risk_flag=False,
created_at=now - timedelta(days=1),
updated_at=now - timedelta(days=1),
),
ExpenseClaim(
claim_no="CLM-DASH-LABEL-003",
employee_name="reimbursement-user",
department_name="甯傚満閮?,
expense_type="travel",
reason="real travel reimbursement",
location="Shanghai",
amount=Decimal("700.00"),
invoice_count=1,
occurred_at=now - timedelta(hours=2),
submitted_at=now - timedelta(hours=1),
status="submitted",
approval_stage="finance_review",
risk_flags_json=[],
hermes_risk_flag=False,
created_at=now - timedelta(hours=2),
updated_at=now - timedelta(hours=1),
),
]
)
db.add_all(
[
RiskObservation(
observation_key="risk-dashboard-label-001",
subject_type="expense_claim",
subject_key="CLM-DASH-LABEL-001",
subject_label="CLM-DASH-LABEL-001",
claim_no="CLM-DASH-LABEL-001",
risk_type="policy",
risk_signal="missing_material",
title="材料不完整",
risk_level="medium",
status="pending_review",
created_at=now - timedelta(minutes=30),
updated_at=now - timedelta(minutes=30),
),
RiskObservation(
observation_key="risk-dashboard-label-002",
subject_type="expense_claim",
subject_key="CLM-DASH-LABEL-001",
subject_label="CLM-DASH-LABEL-001",
claim_no="CLM-DASH-LABEL-001",
risk_type="budget",
risk_signal="budget_pressure",
title="预算压力偏高",
risk_level="high",
status="pending_review",
created_at=now - timedelta(minutes=20),
updated_at=now - timedelta(minutes=20),
),
]
)
allocation = BudgetAllocation(
budget_no="BUD-DASH-LABEL-001",
fiscal_year=now.year,
period_type="year",
period_key=f"{now.year}",
department_name="市场部",
subject_code="travel",
subject_name="差旅费",
original_amount=Decimal("1000.00"),
adjusted_amount=Decimal("0.00"),
status="active",
warning_threshold=Decimal("80.00"),
control_action="warn",
)
db.add(allocation)
db.flush()
db.add(
BudgetTransaction(
transaction_no="BTX-DASH-LABEL-001",
allocation_id=allocation.id,
source_type="expense_claim",
source_id="CLM-DASH-LABEL-003",
source_no="CLM-DASH-LABEL-003",
transaction_type="consume",
amount=Decimal("1250.00"),
before_available_amount=Decimal("1000.00"),
after_available_amount=Decimal("-250.00"),
operator="finance",
reason="测试超支",
created_at=now - timedelta(minutes=10),
)
)
db.commit()
dashboard = FinanceDashboardService(db).build_dashboard(
range_key="近10日",
trend_range="近7天",
department_range="本月",
)
spend_names = {item["name"] for item in dashboard.spend_by_category}
focus_names = {item["name"] for item in dashboard.bottlenecks}
assert "差旅" in spend_names
assert "travel_application" not in str(dashboard.spend_by_category)
assert "风险" not in str(dashboard.exception_mix)
assert "异常" not in str(dashboard.exception_mix)
assert "missing material" not in str(dashboard.exception_mix).lower()
assert "budget pressure" not in str(dashboard.exception_mix).lower()
assert dashboard.trend["claimCount"][-1] == 1
assert dashboard.trend["claimAmount"][-1] == 700.0
assert dashboard.trend["applications"] == dashboard.trend["claimCount"]
assert dashboard.department_ranking[0]["name"] == "市场部"
assert dashboard.department_ranking[0]["amount"] == 700.0
assert {"预算超支", "待付款", "高额单据"}.issubset(focus_names)
assert "风险金额" not in focus_names
assert "材料待补" not in focus_names
assert all(item["role"] != "审批节点" for item in dashboard.bottlenecks)
assert len(dashboard.budget_metrics) == 6