feat: 本体字段治理与风险规则模板执行器重构
- 新增本体字段注册表与字段治理审计脚本 - 重构风险规则模板执行器、DSL 验证与清单分类器 - 完善票据夹服务与差旅请求详情页交互 - 优化趋势图表与总览页数据展示 - 增强报销平台风险分级与模拟公司筛选 - 补充本体字段、风险规则生成与票据夹服务测试覆盖
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user