feat: 新增预算后端服务与差旅风险规则库
后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额 查询,清理旧生成规则文件并替换为按严重等级分类的差旅风 险规则库,优化认证权限和报销单访问策略,新增财务规则目 录和演示数据构建脚本,前端预算中心增加对话框交互,完善 审计页面运行时模型和元数据展示,补充单元测试。
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user