feat: 新增预算后端服务与差旅风险规则库
后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额 查询,清理旧生成规则文件并替换为按严重等级分类的差旅风 险规则库,优化认证权限和报销单访问策略,新增财务规则目 录和演示数据构建脚本,前端预算中心增加对话框交互,完善 审计页面运行时模型和元数据展示,补充单元测试。
This commit is contained in:
@@ -10,9 +10,11 @@ from app.models.agent_asset import AgentAsset
|
||||
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
|
||||
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
|
||||
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
|
||||
from app.services.budget import BudgetService
|
||||
from app.services.expense_rule_runtime import (
|
||||
RuntimeTravelPolicy,
|
||||
)
|
||||
from app.services.expense_type_keywords import resolve_expense_type_code_from_text
|
||||
from app.services.risk_rule_manifest_normalizer import normalize_risk_rule_manifest
|
||||
from app.services.risk_rule_template_executor import RiskRuleTemplateExecutor
|
||||
|
||||
@@ -29,6 +31,16 @@ class ExpenseClaimPlatformRiskMixin:
|
||||
return {"flags": [], "blocking_reasons": []}
|
||||
|
||||
contexts = self._build_claim_attachment_contexts(claim)
|
||||
contexts.append(
|
||||
{
|
||||
"index": len(contexts) + 1,
|
||||
"item": None,
|
||||
"document_info": {},
|
||||
"ocr_text": "",
|
||||
"ocr_summary": "",
|
||||
"budget_context": BudgetService(self.db).build_claim_budget_context(claim),
|
||||
}
|
||||
)
|
||||
flags: list[dict[str, Any]] = []
|
||||
blocking_reasons: list[str] = []
|
||||
|
||||
@@ -163,24 +175,18 @@ class ExpenseClaimPlatformRiskMixin:
|
||||
if min_attachments and int(claim.invoice_count or 0) < min_attachments and not contexts:
|
||||
return False
|
||||
|
||||
expense_types = {
|
||||
str(claim.expense_type or "").strip().lower(),
|
||||
*{
|
||||
str(item.item_type or "").strip().lower()
|
||||
for item in list(claim.items or [])
|
||||
if str(item.item_type or "").strip()
|
||||
},
|
||||
}
|
||||
expense_types = self._normalize_expense_type_values(
|
||||
str(claim.expense_type or ""),
|
||||
*[str(item.item_type or "") for item in list(claim.items or [])],
|
||||
)
|
||||
domains = {
|
||||
str(value or "").strip().lower()
|
||||
for value in list(applies_to.get("domains") or [])
|
||||
if str(value or "").strip()
|
||||
}
|
||||
configured_expense_types = {
|
||||
str(value or "").strip().lower()
|
||||
for value in list(applies_to.get("expense_types") or [])
|
||||
if str(value or "").strip()
|
||||
}
|
||||
configured_expense_types = self._normalize_expense_type_values(
|
||||
*[str(value or "") for value in list(applies_to.get("expense_types") or [])]
|
||||
)
|
||||
|
||||
if configured_expense_types and not (expense_types & configured_expense_types):
|
||||
return False
|
||||
@@ -193,6 +199,19 @@ class ExpenseClaimPlatformRiskMixin:
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _normalize_expense_type_values(*values: str) -> set[str]:
|
||||
normalized: set[str] = set()
|
||||
for value in values:
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
continue
|
||||
normalized.add(raw.lower())
|
||||
resolved = resolve_expense_type_code_from_text(raw)
|
||||
if resolved:
|
||||
normalized.add(resolved)
|
||||
return normalized
|
||||
|
||||
def _risk_domains_match_claim(
|
||||
self,
|
||||
domains: set[str],
|
||||
@@ -213,6 +232,9 @@ class ExpenseClaimPlatformRiskMixin:
|
||||
}
|
||||
)
|
||||
|
||||
if "expense" in domains:
|
||||
return True
|
||||
|
||||
if "travel" in domains:
|
||||
if expense_types & {"travel", "hotel", "transport"}:
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user