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
|
||||
@@ -416,6 +416,13 @@ def test_upsert_linked_application_draft_without_receipts_has_no_placeholder_ite
|
||||
"application_amount": "3000",
|
||||
"application_amount_label": "¥3,000",
|
||||
"application_business_time": "2026-02-20 至 2026-02-23",
|
||||
"application_date": "2026-06-02T00:58:00Z",
|
||||
"application_days": "4 天",
|
||||
"application_transport_mode": "火车",
|
||||
"application_lodging_daily_cap": "600元/天",
|
||||
"application_subsidy_daily_cap": "120元/天",
|
||||
"application_transport_policy": "按真实票据复核",
|
||||
"application_policy_estimate": "交通 1,160元 + 住宿 2,400元 + 补贴 480元",
|
||||
},
|
||||
"expense_scene_selection": {
|
||||
"expense_type": "travel",
|
||||
@@ -432,6 +439,7 @@ def test_upsert_linked_application_draft_without_receipts_has_no_placeholder_ite
|
||||
assert claim.location == "上海"
|
||||
assert claim.amount == Decimal("0.00")
|
||||
assert claim.invoice_count == 0
|
||||
assert claim.occurred_at.date() == date(2026, 2, 20)
|
||||
assert claim.items == []
|
||||
link_flag = next(
|
||||
flag
|
||||
@@ -439,7 +447,221 @@ def test_upsert_linked_application_draft_without_receipts_has_no_placeholder_ite
|
||||
if isinstance(flag, dict) and flag.get("source") == "application_link"
|
||||
)
|
||||
assert link_flag["application_claim_no"] == "AP-202606-001"
|
||||
assert link_flag["application_detail"]["application_time"] == "2026-02-20 至 2026-02-23"
|
||||
assert link_flag["application_detail"]["application_business_time"] == "2026-02-20 至 2026-02-23"
|
||||
assert link_flag["application_detail"]["application_date"] == "2026-06-02T00:58:00Z"
|
||||
assert link_flag["application_detail"]["application_amount"] == "3000"
|
||||
assert link_flag["application_detail"]["application_days"] == "4 天"
|
||||
assert link_flag["application_detail"]["application_transport_mode"] == "火车"
|
||||
assert link_flag["application_detail"]["application_lodging_daily_cap"] == "600元/天"
|
||||
assert link_flag["application_detail"]["application_subsidy_daily_cap"] == "120元/天"
|
||||
|
||||
|
||||
def test_upsert_linked_application_draft_clears_existing_placeholder_item() -> None:
|
||||
user_id = "linked-application-existing-placeholder@example.com"
|
||||
message = (
|
||||
"报销类型:差旅费\n"
|
||||
"关联申请单:AP-202606-002 / 支撑国网仿生产服务器部署 / 上海 / ¥3,000\n"
|
||||
"报销票据:草稿生成后在详情中上传"
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
employee_no="E5105",
|
||||
name="关联员工",
|
||||
email=user_id,
|
||||
grade="P5",
|
||||
)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
existing_claim = ExpenseClaim(
|
||||
claim_no="RE-202606020001-PLACEHOLDER",
|
||||
employee_id=employee.id,
|
||||
employee_name="关联员工",
|
||||
department_name="技术部",
|
||||
project_code=None,
|
||||
expense_type="travel",
|
||||
reason="支撑国网仿生产服务器部署",
|
||||
location="上海",
|
||||
amount=Decimal("3000.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 2, 20, tzinfo=UTC),
|
||||
status="draft",
|
||||
approval_stage="待提交",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
existing_claim.items = [
|
||||
ExpenseClaimItem(
|
||||
claim_id=existing_claim.id,
|
||||
item_date=date(2026, 2, 20),
|
||||
item_type="travel",
|
||||
item_reason="支撑国网仿生产服务器部署",
|
||||
item_location="上海",
|
||||
item_amount=Decimal("3000.00"),
|
||||
invoice_id=None,
|
||||
)
|
||||
]
|
||||
db.add(existing_claim)
|
||||
db.commit()
|
||||
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query=message,
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
result = ExpenseClaimService(db).upsert_draft_from_ontology(
|
||||
run_id=ontology.run_id,
|
||||
user_id=user_id,
|
||||
message=message,
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"name": "关联员工",
|
||||
"draft_claim_id": existing_claim.id,
|
||||
"user_input_text": message,
|
||||
"review_action": "save_draft",
|
||||
"review_form_values": {
|
||||
"expense_type": "差旅费",
|
||||
"amount": "¥3,000",
|
||||
"reason": "支撑国网仿生产服务器部署",
|
||||
"location": "上海",
|
||||
"business_location": "上海",
|
||||
"application_claim_id": "application-linked-existing-placeholder",
|
||||
"application_claim_no": "AP-202606-002",
|
||||
"application_reason": "支撑国网仿生产服务器部署",
|
||||
"application_location": "上海",
|
||||
"application_amount": "3000",
|
||||
"application_amount_label": "¥3,000",
|
||||
},
|
||||
"expense_scene_selection": {
|
||||
"expense_type": "travel",
|
||||
"application_claim_id": "application-linked-existing-placeholder",
|
||||
"application_claim_no": "AP-202606-002",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
claim = db.get(ExpenseClaim, result["claim_id"])
|
||||
assert claim is not None
|
||||
assert claim.id == existing_claim.id
|
||||
assert claim.amount == Decimal("0.00")
|
||||
assert claim.invoice_count == 0
|
||||
assert claim.items == []
|
||||
|
||||
|
||||
def test_sync_travel_allowance_uses_linked_application_range_days() -> None:
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
employee_no="E5106",
|
||||
name="关联差旅员工",
|
||||
email="linked-application-allowance@example.com",
|
||||
grade="P4",
|
||||
)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
|
||||
claim = build_claim(expense_type="travel", location="上海")
|
||||
claim.employee_id = employee.id
|
||||
claim.employee_name = employee.name
|
||||
claim.amount = Decimal("354.00")
|
||||
claim.items[0].item_date = date(2026, 2, 20)
|
||||
claim.items[0].item_type = "train_ticket"
|
||||
claim.items[0].item_reason = "武汉-上海"
|
||||
claim.items[0].item_location = "上海"
|
||||
claim.items[0].item_amount = Decimal("354.00")
|
||||
claim.risk_flags_json = [
|
||||
{
|
||||
"source": "application_link",
|
||||
"application_claim_no": "AP-202606-003",
|
||||
"application_detail": {
|
||||
"application_time": "2026-02-20 至 2026-02-23",
|
||||
"application_days": "4 天",
|
||||
"application_location": "上海",
|
||||
"application_transport_mode": "火车",
|
||||
},
|
||||
}
|
||||
]
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
|
||||
service = ExpenseClaimService(db)
|
||||
service._sync_claim_from_items(claim)
|
||||
db.commit()
|
||||
db.refresh(claim)
|
||||
|
||||
allowance_item = next(item for item in claim.items if item.item_type == "travel_allowance")
|
||||
assert "4天" in allowance_item.item_reason
|
||||
assert allowance_item.item_date == date(2026, 2, 23)
|
||||
|
||||
|
||||
def test_sync_travel_allowance_backfills_range_from_linked_application_claim() -> None:
|
||||
with build_session() as db:
|
||||
employee = Employee(
|
||||
employee_no="E5107",
|
||||
name="旧关联差旅员工",
|
||||
email="linked-application-allowance-backfill@example.com",
|
||||
grade="P4",
|
||||
)
|
||||
db.add(employee)
|
||||
db.flush()
|
||||
application_claim = ExpenseClaim(
|
||||
claim_no="AP-202606-004",
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
department_name="技术部",
|
||||
project_code=None,
|
||||
expense_type="travel_application",
|
||||
reason="支撑国网仿生产环境部署",
|
||||
location="上海",
|
||||
amount=Decimal("3000.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 2, 20, tzinfo=UTC),
|
||||
status="approved",
|
||||
approval_stage="审批完成",
|
||||
risk_flags_json=[
|
||||
{
|
||||
"source": "application_detail",
|
||||
"application_detail": {
|
||||
"time": "2026-02-20 至 2026-02-23",
|
||||
"days": "4 天",
|
||||
"location": "上海",
|
||||
"reason": "支撑国网仿生产环境部署",
|
||||
"transport_mode": "火车",
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
db.add(application_claim)
|
||||
db.flush()
|
||||
|
||||
claim = build_claim(expense_type="travel", location="上海")
|
||||
claim.employee_id = employee.id
|
||||
claim.employee_name = employee.name
|
||||
claim.amount = Decimal("354.00")
|
||||
claim.items[0].item_date = date(2026, 2, 20)
|
||||
claim.items[0].item_type = "train_ticket"
|
||||
claim.items[0].item_reason = "武汉-上海"
|
||||
claim.items[0].item_location = "上海"
|
||||
claim.items[0].item_amount = Decimal("354.00")
|
||||
claim.risk_flags_json = [
|
||||
{
|
||||
"source": "application_link",
|
||||
"application_claim_no": "AP-202606-004",
|
||||
}
|
||||
]
|
||||
db.add(claim)
|
||||
db.commit()
|
||||
|
||||
service = ExpenseClaimService(db)
|
||||
service._sync_claim_from_items(claim)
|
||||
db.commit()
|
||||
db.refresh(claim)
|
||||
|
||||
allowance_item = next(item for item in claim.items if item.item_type == "travel_allowance")
|
||||
assert "4天" in allowance_item.item_reason
|
||||
assert allowance_item.item_date == date(2026, 2, 23)
|
||||
|
||||
|
||||
def test_unsaved_conversation_expires_after_retention_but_saved_conversation_stays() -> None:
|
||||
@@ -2858,6 +3080,83 @@ def test_list_claims_limits_finance_to_personal_records() -> None:
|
||||
assert claims[0].claim_no == "EXP-FIN-OWN"
|
||||
|
||||
|
||||
def test_list_claims_returns_company_reimbursements_for_finance_document_center() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="finance@example.com",
|
||||
name="财务",
|
||||
role_codes=["finance"],
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
with build_session() as db:
|
||||
db.add_all(
|
||||
[
|
||||
ExpenseClaim(
|
||||
claim_no="EXP-FIN-COMPANY-SUBMITTED",
|
||||
employee_name="乙",
|
||||
department_name="市场部",
|
||||
project_code="PRJ-MKT",
|
||||
expense_type="travel",
|
||||
reason="客户拜访差旅",
|
||||
location="上海",
|
||||
amount=Decimal("1200.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 11, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 11, 10, 0, tzinfo=UTC),
|
||||
status="submitted",
|
||||
approval_stage="finance_review",
|
||||
risk_flags_json=[],
|
||||
),
|
||||
ExpenseClaim(
|
||||
claim_no="EXP-FIN-COMPANY-DRAFT",
|
||||
employee_name="丙",
|
||||
department_name="技术部",
|
||||
project_code="PRJ-TECH",
|
||||
expense_type="office",
|
||||
reason="办公用品",
|
||||
location="北京",
|
||||
amount=Decimal("300.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 11, 12, 0, tzinfo=UTC),
|
||||
submitted_at=None,
|
||||
status="draft",
|
||||
approval_stage="待提交",
|
||||
risk_flags_json=[],
|
||||
),
|
||||
ExpenseClaim(
|
||||
claim_no="EXP-FIN-COMPANY-PAID",
|
||||
employee_name="丁",
|
||||
department_name="财务部",
|
||||
project_code="PRJ-FIN",
|
||||
expense_type="meal",
|
||||
reason="客户沟通",
|
||||
location="杭州",
|
||||
amount=Decimal("500.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 5, 12, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 5, 12, 10, 0, tzinfo=UTC),
|
||||
status="paid",
|
||||
approval_stage="payment",
|
||||
risk_flags_json=[],
|
||||
),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
claim_nos = {claim.claim_no for claim in ExpenseClaimService(db).list_claims(current_user)}
|
||||
archived_nos = {
|
||||
claim.claim_no for claim in ExpenseClaimService(db).list_archived_claims(current_user)
|
||||
}
|
||||
|
||||
assert "EXP-FIN-COMPANY-SUBMITTED" in claim_nos
|
||||
assert "EXP-FIN-COMPANY-DRAFT" not in claim_nos
|
||||
assert "EXP-FIN-COMPANY-PAID" not in claim_nos
|
||||
assert "EXP-FIN-COMPANY-PAID" in archived_nos
|
||||
|
||||
|
||||
def test_list_claims_limits_executive_to_personal_records() -> None:
|
||||
current_user = CurrentUserContext(
|
||||
username="executive@example.com",
|
||||
@@ -3822,6 +4121,12 @@ def test_direct_manager_can_route_application_claim_to_budget_approval_then_budg
|
||||
"reason": "支撑国网服务器上线部署",
|
||||
"days": "3 天",
|
||||
"transport_mode": "高铁",
|
||||
"lodging_daily_cap": "600元/天",
|
||||
"subsidy_daily_cap": "120元/天",
|
||||
"transport_policy": "按真实票据复核",
|
||||
"policy_estimate": "交通按真实票据 + 住宿 1,800元 + 补贴 360元",
|
||||
"rule_name": "差旅标准规则",
|
||||
"rule_version": "2026.05",
|
||||
"amount": "12000.00",
|
||||
},
|
||||
},
|
||||
@@ -3899,6 +4204,13 @@ def test_direct_manager_can_route_application_claim_to_budget_approval_then_budg
|
||||
and flag.get("application_detail", {}).get("application_reason") == "支撑国网服务器上线部署"
|
||||
and flag.get("application_detail", {}).get("application_days") == "3 天"
|
||||
and flag.get("application_detail", {}).get("application_transport_mode") == "高铁"
|
||||
and flag.get("application_detail", {}).get("application_lodging_daily_cap") == "600元/天"
|
||||
and flag.get("application_detail", {}).get("application_subsidy_daily_cap") == "120元/天"
|
||||
and flag.get("application_detail", {}).get("application_transport_policy") == "按真实票据复核"
|
||||
and flag.get("application_detail", {}).get("application_policy_estimate")
|
||||
== "交通按真实票据 + 住宿 1,800元 + 补贴 360元"
|
||||
and flag.get("application_detail", {}).get("application_rule_name") == "差旅标准规则"
|
||||
and flag.get("application_detail", {}).get("application_rule_version") == "2026.05"
|
||||
and flag.get("leader_opinion") == "业务必要,同意申请。"
|
||||
and flag.get("budget_opinion") == "预算额度可承接,同意。"
|
||||
for flag in generated_draft.risk_flags_json
|
||||
|
||||
52
server/tests/test_expense_claim_status_registry.py
Normal file
52
server/tests/test_expense_claim_status_registry.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from app.services.expense_claim_status_registry import (
|
||||
claim_status_code,
|
||||
normalize_expense_claim_state,
|
||||
)
|
||||
from app.services.expense_claim_workflow_constants import (
|
||||
APPROVAL_DONE_STAGE,
|
||||
ARCHIVE_ACCOUNTING_STAGE,
|
||||
FINANCE_APPROVAL_STAGE,
|
||||
PAYMENT_PAID_STAGE,
|
||||
PAYMENT_PENDING_STAGE,
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_legacy_finance_review_to_submitted_finance_stage() -> None:
|
||||
state = normalize_expense_claim_state(
|
||||
"finance_review",
|
||||
"finance_review",
|
||||
claim_no="SIM-EXP-2026-0001",
|
||||
expense_type="travel",
|
||||
)
|
||||
|
||||
assert state.status == "submitted"
|
||||
assert state.approval_stage == FINANCE_APPROVAL_STAGE
|
||||
assert state.status_code == 20
|
||||
assert state.changed is True
|
||||
|
||||
|
||||
def test_normalize_reimbursement_archive_stage_differs_from_application_done() -> None:
|
||||
reimbursement_state = normalize_expense_claim_state(
|
||||
"approved",
|
||||
"completed",
|
||||
claim_no="SIM-EXP-2026-0002",
|
||||
expense_type="travel",
|
||||
)
|
||||
application_state = normalize_expense_claim_state(
|
||||
"approved",
|
||||
"completed",
|
||||
claim_no="AP-20260602-0001",
|
||||
expense_type="travel_application",
|
||||
)
|
||||
|
||||
assert reimbursement_state.approval_stage == ARCHIVE_ACCOUNTING_STAGE
|
||||
assert application_state.approval_stage == APPROVAL_DONE_STAGE
|
||||
|
||||
|
||||
def test_normalize_payment_stages_by_status() -> None:
|
||||
pending_state = normalize_expense_claim_state("pending_payment", "payment")
|
||||
paid_state = normalize_expense_claim_state("paid", "payment")
|
||||
|
||||
assert pending_state.approval_stage == PAYMENT_PENDING_STAGE
|
||||
assert paid_state.approval_stage == PAYMENT_PAID_STAGE
|
||||
assert claim_status_code("paid") == 50
|
||||
@@ -25,7 +25,7 @@ def build_session() -> Session:
|
||||
return session_factory()
|
||||
|
||||
|
||||
def test_finance_dashboard_service_aggregates_claim_risk_and_budget_data() -> None:
|
||||
def test_finance_dashboard_service_aggregates_claim_budget_and_payment_data() -> None:
|
||||
now = datetime.now(UTC)
|
||||
|
||||
with build_session() as db:
|
||||
@@ -85,6 +85,24 @@ def test_finance_dashboard_service_aggregates_claim_risk_and_budget_data() -> No
|
||||
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(
|
||||
@@ -144,12 +162,175 @@ def test_finance_dashboard_service_aggregates_claim_risk_and_budget_data() -> No
|
||||
)
|
||||
|
||||
assert dashboard.has_real_data is True
|
||||
assert dashboard.totals["pendingCount"] == 1
|
||||
assert dashboard.totals["pendingAmount"] == 1200.0
|
||||
assert dashboard.totals["riskCount"] == 1
|
||||
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
|
||||
|
||||
@@ -710,7 +710,8 @@ def test_orchestrator_application_session_does_not_use_reimbursement_scene_promp
|
||||
assert response.status == "blocked"
|
||||
assert response.trace_summary.scenario == "expense"
|
||||
assert "费用申请" in result["answer"]
|
||||
assert "| 行程时间 | 2026-05-25" in result["answer"]
|
||||
assert "| 出发时间 | 2026-05-25 |" in result["answer"]
|
||||
assert "| 返回时间 | 2026-05-27 |" in result["answer"]
|
||||
assert "请先在下面选择报销场景" not in result["answer"]
|
||||
assert result.get("review_payload") is None
|
||||
|
||||
@@ -773,8 +774,10 @@ def test_orchestrator_application_session_guides_transport_estimate_and_submit(
|
||||
assert "这是费用申请核对结果" in second.result["answer"]
|
||||
assert "| 事由 | 支持上海国网服务器部署 |" in second.result["answer"]
|
||||
assert "| 系统预估费用 |" in second.result["answer"]
|
||||
assert "按 2026-05-25 参考票价" in second.result["answer"]
|
||||
assert "| 交通费用口径 | 预估交通费用 2,330元 |" in second.result["answer"]
|
||||
assert "2,330元" in second.result["answer"]
|
||||
assert "参考票价" not in second.result["answer"]
|
||||
assert "查询耗时" not in second.result["answer"]
|
||||
assert "请核对上述信息无误" in second.result["answer"]
|
||||
assert "[确认](#application-submit)" in second.result["answer"]
|
||||
assert second.status == "blocked"
|
||||
|
||||
@@ -209,7 +209,8 @@ def test_user_agent_application_context_uses_application_language() -> None:
|
||||
|
||||
assert "费用申请" in response.answer
|
||||
assert "| 字段 | 内容 |" in response.answer
|
||||
assert "| 行程时间 | 2026-05-25 至 2026-05-27 |" in response.answer
|
||||
assert "| 出发时间 | 2026-05-25 |" in response.answer
|
||||
assert "| 返回时间 | 2026-05-27 |" in response.answer
|
||||
assert "支持上海国网服务器部署" in response.answer
|
||||
assert "当前还需要补充:出行方式" in response.answer
|
||||
assert "请先在下面选择报销场景" not in response.answer
|
||||
@@ -224,7 +225,8 @@ def test_user_agent_application_infers_natural_reason_and_expands_single_date()
|
||||
with session_factory() as db:
|
||||
response = build_application_user_agent_response(db, message)
|
||||
|
||||
assert "| 行程时间 | 2026-05-25 至 2026-05-27 |" in response.answer
|
||||
assert "| 出发时间 | 2026-05-25 |" in response.answer
|
||||
assert "| 返回时间 | 2026-05-27 |" in response.answer
|
||||
assert "| 地点 | 上海市 |" in response.answer
|
||||
assert "| 事由 | 支撑上海国网服务器部署 |" in response.answer
|
||||
assert "当前还需要先补充:申请事由" not in response.answer
|
||||
@@ -250,7 +252,8 @@ def test_user_agent_application_normalizes_location_to_region_city() -> None:
|
||||
yili_response = build_application_user_agent_response(db, yili_message)
|
||||
beijing_response = build_application_user_agent_response(db, beijing_message)
|
||||
|
||||
assert "| 行程时间 | 2026-05-25 至 2026-05-27 |" in yili_response.answer
|
||||
assert "| 出发时间 | 2026-05-25 |" in yili_response.answer
|
||||
assert "| 返回时间 | 2026-05-27 |" in yili_response.answer
|
||||
assert "| 地点 | 新疆,伊犁 |" in yili_response.answer
|
||||
assert "| 事由 | 支撑新疆电力仿生产部署 |" in yili_response.answer
|
||||
assert "伊犁出差" not in yili_response.answer
|
||||
@@ -289,7 +292,8 @@ def test_user_agent_application_uses_selected_time_and_natural_language_fields()
|
||||
)
|
||||
)
|
||||
|
||||
assert "| 行程时间 | 2026-05-25 |" in response.answer
|
||||
assert "| 出发时间 | 2026-05-25 |" in response.answer
|
||||
assert "| 返回时间 | 2026-05-25 |" in response.answer
|
||||
assert "| 地点 | 上海市 |" in response.answer
|
||||
assert "| 事由 | 支撑国网服务器上线部署 |" in response.answer
|
||||
assert "当前还需要补充:出行方式" in response.answer
|
||||
@@ -317,10 +321,10 @@ def test_user_agent_application_builds_system_estimate_after_transport_choice()
|
||||
assert "| 出行方式 | 飞机 |" in response.answer
|
||||
assert "| 系统预估费用 |" in response.answer
|
||||
assert "交通" in response.answer
|
||||
assert "参考票价" in response.answer
|
||||
assert "按 2026-05-25 参考票价" in response.answer
|
||||
assert "| 交通费用口径 | 预估交通费用 2,330元 |" in response.answer
|
||||
assert "2,330元" in response.answer
|
||||
assert "查询耗时" in response.answer
|
||||
assert "参考票价" not in response.answer
|
||||
assert "查询耗时" not in response.answer
|
||||
assert response.requires_confirmation is True
|
||||
assert response.suggested_actions == []
|
||||
|
||||
@@ -362,14 +366,64 @@ def test_user_agent_application_uses_selected_date_range_and_keeps_reason() -> N
|
||||
)
|
||||
)
|
||||
|
||||
assert "| 行程时间 | 2026-02-20 至 2026-02-23 |" in response.answer
|
||||
assert "| 出发时间 | 2026-02-20 |" in response.answer
|
||||
assert "| 返回时间 | 2026-02-23 |" in response.answer
|
||||
assert "| 地点 | 上海市 |" in response.answer
|
||||
assert "| 事由 | 支撑国网仿生产环境部署 |" in response.answer
|
||||
assert "| 天数 | 4天 |" in response.answer
|
||||
assert "| 住宿上限/天 | 450元/天 |" in response.answer
|
||||
assert "| 补贴标准/天 | 100元/天 |" in response.answer
|
||||
assert "| 规则测算参考 | 交通 2,460元 + 住宿 1,800元 + 补贴 400元 = 4,660元(4天) |" in response.answer
|
||||
assert "| 发生时间 |" not in response.answer
|
||||
assert "| 事由 | 2026-02-20 至 2026-02-23 |" not in response.answer
|
||||
|
||||
|
||||
def test_user_agent_application_keeps_labeled_reason_in_structured_travel_form() -> None:
|
||||
session_factory = build_session_factory()
|
||||
message = (
|
||||
"发生时间:2026-02-20 至 2026-02-23\n"
|
||||
"地点:上海\n"
|
||||
"事由:支撑国网仿生产环境建设\n"
|
||||
"天数:4天"
|
||||
)
|
||||
context_json = {
|
||||
"session_type": "application",
|
||||
"entry_source": "application",
|
||||
"name": "曹笑竹",
|
||||
"department_name": "技术部",
|
||||
"position": "财务智能化产品经理",
|
||||
"manager_name": "向万红",
|
||||
"grade": "P5",
|
||||
}
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query=message,
|
||||
user_id="pytest-structured-application-reason@example.com",
|
||||
context_json=context_json,
|
||||
)
|
||||
)
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest-structured-application-reason@example.com",
|
||||
message=message,
|
||||
ontology=ontology,
|
||||
context_json=context_json,
|
||||
tool_payload={"clarification_required": ontology.clarification_required},
|
||||
)
|
||||
)
|
||||
|
||||
assert "| 申请类型 | 差旅费用申请 |" in response.answer
|
||||
assert "| 出发时间 | 2026-02-20 |" in response.answer
|
||||
assert "| 返回时间 | 2026-02-23 |" in response.answer
|
||||
assert "| 地点 | 上海市 |" in response.answer
|
||||
assert "| 事由 | 支撑国网仿生产环境建设 |" in response.answer
|
||||
assert "| 天数 | 4天 |" in response.answer
|
||||
assert "申请事由" not in response.answer
|
||||
assert "当前还需要补充:出行方式" in response.answer
|
||||
|
||||
|
||||
def test_user_agent_application_derives_days_from_selected_date_range() -> None:
|
||||
session_factory = build_session_factory()
|
||||
message = "去上海出差,支撑国网仿生产服务器部署,火车"
|
||||
@@ -418,7 +472,8 @@ def test_user_agent_application_derives_days_from_selected_date_range() -> None:
|
||||
)
|
||||
)
|
||||
|
||||
assert "| 行程时间 | 2026-02-20 至 2026-02-23 |" in response.answer
|
||||
assert "| 出发时间 | 2026-02-20 |" in response.answer
|
||||
assert "| 返回时间 | 2026-02-23 |" in response.answer
|
||||
assert "| 天数 | 4天 |" in response.answer
|
||||
assert "| 天数 | 待补充 |" not in response.answer
|
||||
assert "(4天)" in response.answer
|
||||
@@ -452,7 +507,8 @@ def test_user_agent_application_precomputes_time_from_today_and_days() -> None:
|
||||
)
|
||||
|
||||
assert "这是费用申请核对结果" in response.answer
|
||||
assert "| 行程时间 | 2026-05-29 至 2026-05-31 |" in response.answer
|
||||
assert "| 出发时间 | 2026-05-29 |" in response.answer
|
||||
assert "| 返回时间 | 2026-05-31 |" in response.answer
|
||||
assert response.requires_confirmation is True
|
||||
|
||||
|
||||
@@ -547,7 +603,8 @@ def test_user_agent_application_submit_enters_leader_review() -> None:
|
||||
"| 字段 | 内容 |\n"
|
||||
"| --- | --- |\n"
|
||||
"| 申请类型 | 差旅费用申请 |\n"
|
||||
"| 行程时间 | 2026-05-25 |\n"
|
||||
"| 出发时间 | 2026-05-25 |\n"
|
||||
"| 返回时间 | 2026-05-27 |\n"
|
||||
"| 地点 | 上海市 |\n"
|
||||
"| 事由 | 支持上海国网服务器部署 |\n"
|
||||
"| 天数 | 3天 |\n"
|
||||
@@ -585,7 +642,8 @@ def test_user_agent_application_submit_enters_leader_review() -> None:
|
||||
def test_user_agent_application_submit_blocks_duplicate_business_time() -> None:
|
||||
session_factory = build_session_factory()
|
||||
initial_message = (
|
||||
"行程时间:2026-05-25 至 2026-05-27\n"
|
||||
"出发时间:2026-05-25\n"
|
||||
"返回时间:2026-05-27\n"
|
||||
"地点:上海\n"
|
||||
"事由:支持上海国网服务器部署\n"
|
||||
"天数:3天\n"
|
||||
@@ -597,7 +655,8 @@ def test_user_agent_application_submit_blocks_duplicate_business_time() -> None:
|
||||
"| 字段 | 内容 |\n"
|
||||
"| --- | --- |\n"
|
||||
"| 申请类型 | 差旅费用申请 |\n"
|
||||
"| 行程时间 | 2026-05-25 至 2026-05-27 |\n"
|
||||
"| 出发时间 | 2026-05-25 |\n"
|
||||
"| 返回时间 | 2026-05-27 |\n"
|
||||
"| 地点 | 上海市 |\n"
|
||||
"| 事由 | 支持上海国网服务器部署 |\n"
|
||||
"| 天数 | 3天 |\n"
|
||||
@@ -1385,6 +1444,7 @@ def test_user_agent_uses_linked_application_context_for_review_slots() -> None:
|
||||
"application_location": "北京",
|
||||
"application_amount": "3000元",
|
||||
"application_business_time": "2026-06-01 至 2026-06-03",
|
||||
"application_transport_mode": "火车",
|
||||
},
|
||||
"user_input_text": message,
|
||||
}
|
||||
@@ -1412,6 +1472,15 @@ def test_user_agent_uses_linked_application_context_for_review_slots() -> None:
|
||||
assert slot_map["location"].value == "北京"
|
||||
assert slot_map["amount"].value == "3000.00元"
|
||||
assert slot_map["time_range"].value == "2026-06-01 至 2026-06-03"
|
||||
assert UserAgentService._resolve_review_form_values(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest-linked-application-review@example.com",
|
||||
message=message,
|
||||
ontology=ontology,
|
||||
context_json=context_json,
|
||||
)
|
||||
)["transport_mode"] == "火车"
|
||||
assert "事由说明" not in response.review_payload.missing_slots
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user