feat: 新增预算后端服务与差旅风险规则库

后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额
查询,清理旧生成规则文件并替换为按严重等级分类的差旅风
险规则库,优化认证权限和报销单访问策略,新增财务规则目
录和演示数据构建脚本,前端预算中心增加对话框交互,完善
审计页面运行时模型和元数据展示,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-26 17:29:35 +08:00
parent e1e515ecae
commit e7bef0883d
85 changed files with 6443 additions and 1497 deletions

View File

@@ -1,66 +1,115 @@
from __future__ import annotations
import hashlib
import json
from datetime import UTC, date, datetime
from decimal import Decimal
from pathlib import Path
from sqlalchemy import inspect, select, text
from sqlalchemy import select
from app.core.agent_enums import (
AgentAssetContentType,
AgentAssetDomain,
AgentAssetStatus,
AgentAssetType,
AgentName,
AgentPermissionLevel,
AgentReviewStatus,
AgentRunSource,
AgentRunStatus,
AgentToolType,
)
from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion
from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog
from app.models.audit_log import AuditLog
from app.models.financial_record import (
AccountsPayableRecord,
AccountsReceivableRecord,
ExpenseClaim,
ExpenseClaimItem,
)
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.core.logging import get_logger
from app.models.agent_asset import AgentAsset
from app.services.agent_asset_spreadsheet import (
AgentAssetSpreadsheetManager,
COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME,
COMPANY_TRAVEL_EXPENSE_RULE_CODE,
COMPANY_TRAVEL_EXPENSE_RULE_FILENAME,
FINANCE_RULES_LIBRARY,
RISK_RULES_LIBRARY,
)
from app.services.expense_rule_runtime import (
build_scene_submission_standard_markdown,
build_travel_risk_control_standard_markdown,
AgentAssetSpreadsheetManager,
)
from app.services.agent_foundation_constants import (
ATTACHMENT_RULE_ASSET_CODE,
ATTACHMENT_RULE_RUNTIME_CONFIG,
COMPANY_COMMUNICATION_RULE_SCENARIO_JSON,
COMPANY_COMMUNICATION_RULE_VERSION,
COMPANY_TRAVEL_RULE_SCENARIO_JSON,
COMPANY_TRAVEL_RULE_VERSION,
DEMO_EXPENSE_CLAIM_SIGNATURES,
DEMO_PAYABLE_SIGNATURES,
DEMO_RECEIVABLE_SIGNATURES,
LEGACY_RULE_CODES,
PLATFORM_DESTINATION_LOCATION_RULE_FILENAME,
)
from app.core.logging import get_logger
from app.services.finance_rule_catalog import (
DEPRECATED_FINANCE_RULE_CODES,
DEPRECATED_FINANCE_RULE_REPLACEMENTS,
)
logger = get_logger("app.services.agent_foundation")
class AgentFoundationSpreadsheetMixin:
def sync_finance_rule_assets_from_catalog(self) -> int:
synced_count = self._ensure_core_finance_rule_asset_metadata()
self._hide_deprecated_finance_rule_assets()
self.db.flush()
return synced_count
def _ensure_core_finance_rule_asset_metadata(self) -> int:
synced_count = 0
synced_count += int(
self._ensure_core_finance_rule_asset(
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
scenario_category=COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
finance_rule_sheet="差旅住宿费标准",
expense_types=["travel", "hotel", "transport"],
)
)
synced_count += int(
self._ensure_core_finance_rule_asset(
code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
scenario_category=COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
finance_rule_sheet="通信费报销标准",
expense_types=["communication"],
)
)
return synced_count
def _ensure_core_finance_rule_asset(
self,
*,
code: str,
scenario_category: str,
finance_rule_sheet: str,
expense_types: list[str],
) -> bool:
asset = self.db.scalar(select(AgentAsset).where(AgentAsset.code == code))
if asset is None:
return False
asset.scenario_json = [scenario_category]
asset.config_json = {
**(asset.config_json or {}),
"enabled": True,
"tag": "财务规则",
"detail_mode": "spreadsheet",
"rule_library": FINANCE_RULES_LIBRARY,
"scenario_category": scenario_category,
"ai_review_category": scenario_category,
"finance_rule_code": code,
"finance_rule_sheet": finance_rule_sheet,
"expense_types": expense_types,
"business_stage": ["expense_application", "reimbursement"],
"budget_required": True,
}
return True
def _hide_deprecated_finance_rule_assets(self) -> None:
for code in DEPRECATED_FINANCE_RULE_CODES:
asset = self.db.scalar(select(AgentAsset).where(AgentAsset.code == code))
if asset is None:
continue
asset.status = AgentAssetStatus.DISABLED.value
asset.scenario_json = ["已废弃"]
replacement = DEPRECATED_FINANCE_RULE_REPLACEMENTS.get(code)
deprecated_reason = (
"交通/住宿细分并入公司差旅费报销规则,不再作为独立财务规则展示。"
if replacement
else (
"该费用类型没有独立职务金额分档,额度控制转入预算中心,"
"不再作为独立财务规则表展示。"
)
)
asset.config_json = {
**(asset.config_json or {}),
"enabled": False,
"tag": "废弃规则",
"deprecated": True,
"deprecated_reason": deprecated_reason,
}
if replacement:
asset.config_json["replaced_by"] = replacement
def _ensure_company_travel_rule_spreadsheet_seed(
self,
@@ -251,6 +300,8 @@ class AgentFoundationSpreadsheetMixin:
fallback_sheet_name: str,
workbook_sheets: list[tuple[str, list[list[object]]]] | None = None,
):
manager = AgentAssetSpreadsheetManager()
@@ -271,6 +322,8 @@ class AgentFoundationSpreadsheetMixin:
fallback_sheet_name=fallback_sheet_name,
workbook_sheets=workbook_sheets,
),
actor_name=actor_name,
@@ -379,6 +432,8 @@ class AgentFoundationSpreadsheetMixin:
fallback_sheet_name: str,
workbook_sheets: list[tuple[str, list[list[object]]]] | None = None,
) -> bytes:
live_key = (
@@ -397,4 +452,8 @@ class AgentFoundationSpreadsheetMixin:
return live_path.read_bytes()
if workbook_sheets is not None:
return AgentAssetSpreadsheetManager.build_rule_workbook(workbook_sheets)
return AgentAssetSpreadsheetManager.build_blank_rule_workbook(fallback_sheet_name)