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

@@ -0,0 +1,96 @@
from __future__ import annotations
from typing import Any
from app.api.deps import CurrentUserContext
from app.models.financial_record import ExpenseClaim
from app.services.budget import BudgetService
class ExpenseClaimBudgetFlowMixin:
def _reserve_budget_for_submission(
self,
claim: ExpenseClaim,
current_user: CurrentUserContext,
*,
is_application_claim: bool,
) -> list[dict[str, Any]]:
source_type = "application" if is_application_claim else "claim"
return BudgetService(self.db).reserve_for_claim(
claim,
source_type=source_type,
operator=self._resolve_budget_operator(current_user),
)
def _release_budget_for_return(
self,
claim: ExpenseClaim,
current_user: CurrentUserContext,
*,
reason: str,
) -> list[dict[str, Any]]:
is_application_claim = self._is_expense_application_claim(claim)
source_type = "application" if is_application_claim else "claim"
return BudgetService(self.db).release_for_claim(
claim,
source_type=source_type,
operator=self._resolve_budget_operator(current_user),
reason=reason,
)
def _release_budget_for_delete(
self,
claim: ExpenseClaim,
current_user: CurrentUserContext,
) -> None:
is_application_claim = self._is_expense_application_claim(claim)
source_type = "application" if is_application_claim else "claim"
BudgetService(self.db).release_for_claim(
claim,
source_type=source_type,
operator=self._resolve_budget_operator(current_user),
reason="单据删除释放预算预占",
)
def _consume_budget_for_finance_approval(
self,
claim: ExpenseClaim,
current_user: CurrentUserContext,
) -> dict[str, Any] | None:
return BudgetService(self.db).consume_for_claim(
claim,
operator=self._resolve_budget_operator(current_user),
reason="财务终审通过核销预算",
)
def _transfer_application_budget_to_reimbursement(
self,
*,
application_claim: ExpenseClaim,
draft_claim: ExpenseClaim,
current_user: CurrentUserContext,
) -> dict[str, Any] | None:
return BudgetService(self.db).transfer_application_reservation(
application_claim=application_claim,
draft_claim=draft_claim,
operator=self._resolve_budget_operator(current_user),
)
@staticmethod
def _append_budget_flags(
risk_flags: list[Any] | None,
budget_flags: list[dict[str, Any]] | dict[str, Any] | None,
) -> list[Any]:
if budget_flags is None:
return list(risk_flags or [])
if isinstance(budget_flags, dict):
next_flags = [budget_flags]
else:
next_flags = list(budget_flags or [])
if not next_flags:
return list(risk_flags or [])
return [*list(risk_flags or []), *next_flags]
@staticmethod
def _resolve_budget_operator(current_user: CurrentUserContext) -> str:
return current_user.name or current_user.username or "system"