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