feat: 本体字段治理与风险规则模板执行器重构

- 新增本体字段注册表与字段治理审计脚本
- 重构风险规则模板执行器、DSL 验证与清单分类器
- 完善票据夹服务与差旅请求详情页交互
- 优化趋势图表与总览页数据展示
- 增强报销平台风险分级与模拟公司筛选
- 补充本体字段、风险规则生成与票据夹服务测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 15:46:56 +08:00
parent e12b140508
commit 34457f9c3e
81 changed files with 4858 additions and 1073 deletions

View File

@@ -14,11 +14,12 @@ 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,
)
from app.services.demo_company_simulation_catalog import SIM_PROJECT_CODE
from app.services.demo_company_simulation_rebalance import HalfYearExpenseSimulationRebalancer
def build_session() -> Session:
@@ -133,7 +134,7 @@ def test_half_year_simulation_feeds_budget_summary() -> None:
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}%"))
select(func.count()).select_from(ExpenseClaim).where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
)
sim_employee_count = db.scalar(
select(func.count()).select_from(Employee).where(Employee.employee_no.like(f"{SIM_EMPLOYEE_PREFIX}%"))
@@ -178,25 +179,128 @@ def test_half_year_simulation_excludes_admin_and_visible_month_has_real_volume()
visible_claim_count = db.scalar(
select(func.count())
.select_from(ExpenseClaim)
.where(ExpenseClaim.claim_no.like(f"{SIM_CLAIM_PREFIX}%"))
.where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
.where(ExpenseClaim.occurred_at >= datetime(2026, 6, 1, tzinfo=UTC))
.where(ExpenseClaim.occurred_at < datetime(2026, 6, 3, tzinfo=UTC))
)
total_claim_count = db.scalar(
select(func.count())
.select_from(ExpenseClaim)
.where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
)
daily_counts = [
row[0]
for row in db.execute(
select(func.count())
.select_from(ExpenseClaim)
.where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
.where(ExpenseClaim.occurred_at >= datetime(2026, 6, 1, tzinfo=UTC))
.where(ExpenseClaim.occurred_at < datetime(2026, 6, 3, tzinfo=UTC))
.group_by(func.date(ExpenseClaim.occurred_at))
).all()
]
max_daily_count = max(daily_counts) if daily_counts else 0
earliest_claim_day = db.scalar(
select(func.min(ExpenseClaim.occurred_at)).where(
ExpenseClaim.claim_no.like(f"{SIM_CLAIM_PREFIX}%")
ExpenseClaim.project_code == SIM_PROJECT_CODE
)
)
latest_claim_day = db.scalar(
select(func.max(ExpenseClaim.occurred_at)).where(
ExpenseClaim.claim_no.like(f"{SIM_CLAIM_PREFIX}%")
ExpenseClaim.project_code == SIM_PROJECT_CODE
)
)
assert admin_claim_count == 0
assert total_claim_count is not None
assert 400 <= total_claim_count <= 500
assert visible_claim_count is not None
assert 400 <= visible_claim_count <= 500
assert 12 <= visible_claim_count <= 30
assert max_daily_count <= 16
assert earliest_claim_day is not None
assert latest_claim_day is not None
assert earliest_claim_day.date() >= date(2026, 1, 1)
assert latest_claim_day.date() <= date(2026, 6, 2)
def test_half_year_simulation_rebalance_spreads_existing_rows_without_deleting() -> None:
with build_session() as db:
seed_company(db)
config = SimulationConfig(
target_employees=100,
start_date=date(2026, 1, 1),
months=6,
seed=20260602,
)
HalfYearExpenseSimulationSeeder(db, config).apply()
db.commit()
claims = list(
db.scalars(
select(ExpenseClaim)
.where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
.order_by(ExpenseClaim.claim_no.asc())
).all()
)
for claim in claims:
claim.occurred_at = datetime(2026, 6, 1, 10, tzinfo=UTC)
claim.submitted_at = datetime(2026, 6, 1, 11, tzinfo=UTC)
claim.created_at = claim.occurred_at
claim.updated_at = claim.submitted_at
for item in claim.items:
item.item_date = date(2026, 6, 1)
db.commit()
before_count = db.scalar(
select(func.count()).select_from(ExpenseClaim).where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
)
preview = HalfYearExpenseSimulationRebalancer(db).preview()
applied = HalfYearExpenseSimulationRebalancer(db).apply()
db.commit()
after_count = db.scalar(
select(func.count()).select_from(ExpenseClaim).where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
)
daily_counts = [
row[0]
for row in db.execute(
select(func.count())
.select_from(ExpenseClaim)
.where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
.group_by(func.date(ExpenseClaim.occurred_at))
).all()
]
month_keys = {
(claim.occurred_at.year, claim.occurred_at.month)
for claim in db.scalars(
select(ExpenseClaim).where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
).all()
}
sample_claim = db.scalar(
select(ExpenseClaim)
.where(ExpenseClaim.project_code == SIM_PROJECT_CODE)
.where(ExpenseClaim.status != "draft")
.order_by(ExpenseClaim.claim_no.asc())
.limit(1)
)
sample_transaction = db.scalar(
select(BudgetTransaction)
.where(BudgetTransaction.source_id == sample_claim.id)
.limit(1)
)
sample_observation = db.scalar(
select(RiskObservation)
.where(RiskObservation.claim_id == sample_claim.id)
.limit(1)
)
assert before_count == after_count
assert preview.claims == applied.claims == after_count
assert applied.recent_claims <= 24
assert max(daily_counts) <= 16
assert {(2026, month) for month in range(1, 7)}.issubset(month_keys)
if sample_transaction is not None:
assert sample_transaction.source_no == sample_claim.claim_no
assert sample_transaction.created_at.date() == sample_claim.submitted_at.date()
if sample_observation is not None:
assert sample_observation.claim_no == sample_claim.claim_no
assert sample_observation.created_at.date() == sample_claim.submitted_at.date()