feat: 财务看板口径重构与半年模拟数据及报销状态注册表
- 重构 finance_dashboard 口径计算,新增模拟公司画像数据生成与筛选 - 引入 expense_claim_status_registry 统一报销状态流转 - 完善报销草稿流程、Item Sync 与本体解析器 - 优化总览页趋势图、分页组件与请求进度步骤 - 增强报销申请快速预览、本体工具与详情展示 - 新增半年报销模拟数据种子脚本与状态审计工具 - 补充财务看板、报销状态注册与模拟数据测试覆盖
This commit is contained in:
188
server/tests/test_demo_company_simulation_seed.py
Normal file
188
server/tests/test_demo_company_simulation_seed.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, date, datetime
|
||||
|
||||
from sqlalchemy import create_engine, func, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.db.base import Base
|
||||
from app.models.budget import BudgetAllocation, BudgetReservation, BudgetTransaction
|
||||
from app.models.employee import Employee
|
||||
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
|
||||
from app.models.organization import OrganizationUnit
|
||||
from app.models.risk_observation import RiskObservation
|
||||
from app.services.budget import BudgetService
|
||||
from app.services.demo_company_simulation_seed import (
|
||||
SIM_CLAIM_PREFIX,
|
||||
SIM_EMPLOYEE_PREFIX,
|
||||
HalfYearExpenseSimulationSeeder,
|
||||
SimulationConfig,
|
||||
)
|
||||
|
||||
|
||||
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 seed_company(db: Session) -> None:
|
||||
tech = OrganizationUnit(
|
||||
id="dept-tech",
|
||||
unit_code="TECH-DEPT",
|
||||
name="技术部",
|
||||
unit_type="department",
|
||||
cost_center="CC-6100",
|
||||
location="北京",
|
||||
)
|
||||
market = OrganizationUnit(
|
||||
id="dept-market",
|
||||
unit_code="MARKET-DEPT",
|
||||
name="市场部",
|
||||
unit_type="department",
|
||||
cost_center="CC-4100",
|
||||
location="上海",
|
||||
)
|
||||
db.add_all([tech, market])
|
||||
for index in range(3):
|
||||
db.add(
|
||||
Employee(
|
||||
id=f"emp-existing-{index}",
|
||||
employee_no=f"E-EXISTING-{index}",
|
||||
name=f"现有员工{index}",
|
||||
email=f"existing-{index}@xf.com",
|
||||
grade="P5",
|
||||
position="主管",
|
||||
organization_unit=tech if index % 2 == 0 else market,
|
||||
cost_center="CC-6100" if index % 2 == 0 else "CC-4100",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_half_year_simulation_preview_and_apply_are_idempotent() -> None:
|
||||
with build_session() as db:
|
||||
seed_company(db)
|
||||
config = SimulationConfig(target_employees=8, start_date=date(2026, 1, 1), months=6, seed=7)
|
||||
|
||||
preview = HalfYearExpenseSimulationSeeder(db, config).preview()
|
||||
|
||||
assert preview.mode == "dry-run"
|
||||
assert preview.current_employee_count == 3
|
||||
assert preview.employees_to_create == 5
|
||||
assert preview.claims_to_create >= 24
|
||||
assert preview.budget_allocations_to_create > 0
|
||||
assert preview.budget_transactions_to_create > 0
|
||||
|
||||
applied = HalfYearExpenseSimulationSeeder(db, config).apply()
|
||||
db.commit()
|
||||
|
||||
assert applied.mode == "apply"
|
||||
assert applied.employees_to_create == 5
|
||||
assert db.scalar(select(func.count()).select_from(Employee)) == 8
|
||||
assert db.scalar(select(func.count()).select_from(ExpenseClaim)) == applied.claims_to_create
|
||||
assert (
|
||||
db.scalar(select(func.count()).select_from(ExpenseClaimItem))
|
||||
== applied.claim_items_to_create
|
||||
)
|
||||
assert (
|
||||
db.scalar(select(func.count()).select_from(BudgetAllocation))
|
||||
== applied.budget_allocations_to_create
|
||||
)
|
||||
assert (
|
||||
db.scalar(select(func.count()).select_from(BudgetTransaction))
|
||||
== applied.budget_transactions_to_create
|
||||
)
|
||||
assert (
|
||||
db.scalar(select(func.count()).select_from(BudgetReservation))
|
||||
== applied.budget_reservations_to_create
|
||||
)
|
||||
assert (
|
||||
db.scalar(select(func.count()).select_from(RiskObservation))
|
||||
== applied.risk_observations_to_create
|
||||
)
|
||||
|
||||
repeated = HalfYearExpenseSimulationSeeder(db, config).apply()
|
||||
db.commit()
|
||||
|
||||
assert repeated.employees_to_create == 0
|
||||
assert repeated.claims_to_create == 0
|
||||
assert repeated.budget_allocations_to_create == 0
|
||||
assert repeated.budget_transactions_to_create == 0
|
||||
assert repeated.budget_reservations_to_create == 0
|
||||
assert repeated.risk_observations_to_create == 0
|
||||
|
||||
|
||||
def test_half_year_simulation_feeds_budget_summary() -> None:
|
||||
with build_session() as db:
|
||||
seed_company(db)
|
||||
config = SimulationConfig(
|
||||
target_employees=10,
|
||||
start_date=date(2026, 1, 1),
|
||||
months=6,
|
||||
seed=11,
|
||||
)
|
||||
HalfYearExpenseSimulationSeeder(db, config).apply()
|
||||
db.commit()
|
||||
|
||||
summary = BudgetService(db).get_summary(fiscal_year=2026, period_key="2026Q2")
|
||||
sim_claim_count = db.scalar(
|
||||
select(func.count()).select_from(ExpenseClaim).where(ExpenseClaim.claim_no.like(f"{SIM_CLAIM_PREFIX}%"))
|
||||
)
|
||||
sim_employee_count = db.scalar(
|
||||
select(func.count()).select_from(Employee).where(Employee.employee_no.like(f"{SIM_EMPLOYEE_PREFIX}%"))
|
||||
)
|
||||
|
||||
assert sim_claim_count and sim_claim_count >= 30
|
||||
assert sim_employee_count == 7
|
||||
assert summary.trend
|
||||
assert {item.period_key for item in summary.trend} == {"2026Q1", "2026Q2"}
|
||||
assert summary.warning_count + summary.over_budget_count > 0
|
||||
|
||||
|
||||
def test_half_year_simulation_excludes_admin_and_visible_month_has_real_volume() -> None:
|
||||
with build_session() as db:
|
||||
seed_company(db)
|
||||
db.add(
|
||||
Employee(
|
||||
id="emp-admin",
|
||||
employee_no="ADMIN",
|
||||
name="admin",
|
||||
email="admin@xf.com",
|
||||
grade="P8",
|
||||
position="admin",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
config = SimulationConfig(
|
||||
target_employees=100,
|
||||
start_date=date(2026, 1, 1),
|
||||
months=6,
|
||||
seed=20260602,
|
||||
)
|
||||
HalfYearExpenseSimulationSeeder(db, config).apply()
|
||||
db.commit()
|
||||
|
||||
admin_claim_count = db.scalar(
|
||||
select(func.count())
|
||||
.select_from(ExpenseClaim)
|
||||
.where(ExpenseClaim.employee_name == "admin")
|
||||
)
|
||||
visible_claim_count = db.scalar(
|
||||
select(func.count())
|
||||
.select_from(ExpenseClaim)
|
||||
.where(ExpenseClaim.claim_no.like(f"{SIM_CLAIM_PREFIX}%"))
|
||||
.where(ExpenseClaim.occurred_at >= datetime(2026, 6, 1, tzinfo=UTC))
|
||||
.where(ExpenseClaim.occurred_at < datetime(2026, 6, 3, tzinfo=UTC))
|
||||
)
|
||||
|
||||
assert admin_claim_count == 0
|
||||
assert visible_claim_count is not None
|
||||
assert 400 <= visible_claim_count <= 500
|
||||
Reference in New Issue
Block a user