feat: 新增预算后端服务与差旅风险规则库
后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额 查询,清理旧生成规则文件并替换为按严重等级分类的差旅风 险规则库,优化认证权限和报销单访问策略,新增财务规则目 录和演示数据构建脚本,前端预算中心增加对话框交互,完善 审计页面运行时模型和元数据展示,补充单元测试。
This commit is contained in:
@@ -10,7 +10,12 @@ from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentReviewStatus
|
||||
from app.core.agent_enums import (
|
||||
AgentAssetDomain,
|
||||
AgentAssetStatus,
|
||||
AgentAssetType,
|
||||
AgentReviewStatus,
|
||||
)
|
||||
from app.db.base import Base
|
||||
from app.models.agent_asset import AgentAsset
|
||||
from app.models.employee import Employee
|
||||
@@ -27,6 +32,7 @@ from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
|
||||
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
|
||||
from app.services.agent_assets import AgentAssetService
|
||||
from app.services.agent_foundation_risk_rules import AgentFoundationRiskRuleMixin
|
||||
from app.services.expense_claim_platform_risk import ExpenseClaimPlatformRiskMixin
|
||||
from app.services.risk_rule_flow_diagram import (
|
||||
RiskRuleFlowDiagramRenderer,
|
||||
RiskRuleFlowDiagramSpec,
|
||||
@@ -384,6 +390,92 @@ def test_platform_risk_sync_skips_natural_language_drafts() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_stale_demo_risk_rules_are_marked_deprecated() -> None:
|
||||
class FoundationRiskSyncProbe(AgentFoundationRiskRuleMixin):
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
|
||||
with build_session() as db:
|
||||
stale_asset = AgentAsset(
|
||||
asset_type=AgentAssetType.RULE.value,
|
||||
code="risk.standard.training_per_capita_over_limit",
|
||||
name="培训费人均超标准",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
owner="风控与审计部",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
config_json={
|
||||
"enabled": True,
|
||||
"tag": "风险规则",
|
||||
"source_ref": "费用管控 Demo 风险规则库",
|
||||
},
|
||||
)
|
||||
kept_asset = AgentAsset(
|
||||
asset_type=AgentAssetType.RULE.value,
|
||||
code="risk.standard.software_contract_missing",
|
||||
name="软件服务费缺少合同",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
owner="风控与审计部",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
config_json={
|
||||
"enabled": True,
|
||||
"tag": "风险规则",
|
||||
"source_ref": "费用管控 Demo 风险规则库",
|
||||
},
|
||||
)
|
||||
db.add_all([stale_asset, kept_asset])
|
||||
db.flush()
|
||||
|
||||
FoundationRiskSyncProbe(db)._hide_stale_demo_risk_rules(
|
||||
{"risk.standard.software_contract_missing"}
|
||||
)
|
||||
|
||||
assert stale_asset.status == AgentAssetStatus.DISABLED.value
|
||||
assert stale_asset.config_json["tag"] == "废弃风险规则"
|
||||
assert stale_asset.config_json["enabled"] is False
|
||||
assert kept_asset.status == AgentAssetStatus.ACTIVE.value
|
||||
assert kept_asset.config_json["tag"] == "风险规则"
|
||||
|
||||
|
||||
def test_platform_risk_applies_to_chinese_expense_type_labels() -> None:
|
||||
class PlatformRiskProbe(ExpenseClaimPlatformRiskMixin):
|
||||
pass
|
||||
|
||||
claim = ExpenseClaim(
|
||||
claim_no="TEST-MARKETING-RISK",
|
||||
employee_name="测试员工",
|
||||
department_name="市场部",
|
||||
expense_type="市场推广费",
|
||||
reason="品牌投放活动",
|
||||
amount=Decimal("20000.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime.now(UTC),
|
||||
status="draft",
|
||||
)
|
||||
manifest = {
|
||||
"applies_to": {
|
||||
"domains": ["expense"],
|
||||
"expense_types": ["marketing"],
|
||||
}
|
||||
}
|
||||
|
||||
assert PlatformRiskProbe()._risk_manifest_applies_to_claim(
|
||||
manifest,
|
||||
claim=claim,
|
||||
contexts=[],
|
||||
)
|
||||
|
||||
manifest["applies_to"]["expense_types"] = ["software"]
|
||||
|
||||
assert not PlatformRiskProbe()._risk_manifest_applies_to_claim(
|
||||
manifest,
|
||||
claim=claim,
|
||||
contexts=[],
|
||||
)
|
||||
|
||||
|
||||
def test_risk_rule_flow_diagram_uses_risk_level_palette() -> None:
|
||||
renderer = RiskRuleFlowDiagramRenderer()
|
||||
|
||||
@@ -449,7 +541,10 @@ def test_current_keyword_city_consistency_rule_hits_ticket_city_mismatch() -> No
|
||||
"attachment.route_cities",
|
||||
"item.item_location",
|
||||
],
|
||||
"natural_language": "差旅报销时,交通票或住宿票据中的城市必须与申报目的地一致;未说明绕行、跨城或改签原因时标记风险。",
|
||||
"natural_language": (
|
||||
"差旅报销时,交通票或住宿票据中的城市必须与申报目的地一致;"
|
||||
"未说明绕行、跨城或改签原因时标记风险。"
|
||||
),
|
||||
"condition_summary": "检查住宿城市、申报地点、行程城市是否一致",
|
||||
"keywords": ["绕行", "跨城", "改签", "变更"],
|
||||
},
|
||||
@@ -659,7 +754,10 @@ def test_travel_route_rule_hits_unexpected_intermediate_city_even_when_returning
|
||||
"home_city_fields": ["employee.location"],
|
||||
"exception_fields": ["claim.reason"],
|
||||
"exception_keywords": ["绕行", "跨城办事", "临时改签"],
|
||||
"condition_summary": "A=票据路线城市,B=申报城市,C=员工常驻地,A中出现B∪C之外城市则命中。",
|
||||
"condition_summary": (
|
||||
"A=票据路线城市,B=申报城市,C=员工常驻地,"
|
||||
"A中出现B∪C之外城市则命中。"
|
||||
),
|
||||
},
|
||||
"outcomes": {"fail": {"severity": "high"}},
|
||||
}
|
||||
@@ -700,7 +798,11 @@ def test_travel_route_rule_hits_unexpected_intermediate_city_even_when_returning
|
||||
"document_info": {
|
||||
"route_cities": ["上海", "北京", "武汉"],
|
||||
"fields": [
|
||||
{"key": "route_cities", "label": "行程城市", "value": ["上海", "北京", "武汉"]}
|
||||
{
|
||||
"key": "route_cities",
|
||||
"label": "行程城市",
|
||||
"value": ["上海", "北京", "武汉"],
|
||||
}
|
||||
],
|
||||
},
|
||||
"ocr_text": "上海 到 北京 到 武汉",
|
||||
|
||||
Reference in New Issue
Block a user