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

@@ -41,6 +41,7 @@ from app.services.expense_claim_application_handoff import ExpenseClaimApplicati
from app.services.expense_claim_attachment_analysis import ExpenseClaimAttachmentAnalysisMixin
from app.services.expense_claim_attachment_document import ExpenseClaimAttachmentDocumentMixin
from app.services.expense_claim_attachment_operations import ExpenseClaimAttachmentOperationsMixin
from app.services.expense_claim_budget_flow import ExpenseClaimBudgetFlowMixin
from app.services.expense_claim_document_item_builder import ExpenseClaimDocumentItemBuilderMixin
from app.services.expense_claim_document_parsing import ExpenseClaimDocumentParsingMixin
from app.services.expense_claim_draft_flow import ExpenseClaimDraftFlowMixin
@@ -127,6 +128,7 @@ from app.services.ocr import OcrService
class ExpenseClaimService(
ExpenseClaimApplicationHandoffMixin,
ExpenseClaimBudgetFlowMixin,
ExpenseClaimAttachmentOperationsMixin,
ExpenseClaimReviewPreviewMixin,
ExpenseClaimDraftFlowMixin,
@@ -437,6 +439,11 @@ class ExpenseClaimService(
if missing_fields:
raise ExpenseClaimSubmissionBlockedError(missing_fields)
budget_flags = self._reserve_budget_for_submission(
claim,
current_user,
is_application_claim=is_application_claim,
)
before_json = self._serialize_claim(claim)
if is_application_claim:
submitted_at = datetime.now(UTC)
@@ -453,7 +460,7 @@ class ExpenseClaimService(
"event_type": "expense_application_submission",
"severity": "info",
"label": "申请提交",
"message": "费用申请已提交至直属领导审批,并同步纳入预算管理口径",
"message": "费用申请已提交至直属领导审批,请等待审核结果",
"previous_status": str(claim.status or "").strip(),
"previous_approval_stage": str(claim.approval_stage or "").strip(),
"next_status": "submitted",
@@ -462,9 +469,10 @@ class ExpenseClaimService(
}
claim.status = "submitted"
claim.approval_stage = "直属领导审批"
claim.risk_flags_json = [*preserved_flags, submit_flag]
claim.risk_flags_json = self._append_budget_flags([*preserved_flags, submit_flag], budget_flags)
claim.submitted_at = submitted_at
else:
claim.risk_flags_json = self._append_budget_flags(claim.risk_flags_json, budget_flags)
review_result = self._run_ai_submission_review(claim)
claim.status = str(review_result.get("status") or "supplement")
@@ -520,11 +528,12 @@ class ExpenseClaimService(
if not self._access_policy.has_claim_delete_access(current_user):
self._ensure_draft_claim(claim)
if not self._access_policy.is_claim_owned_by_current_user(claim, current_user):
raise ValueError("只有高级管理人员可以删除非本人单据,申请人仅可删除自己的草稿、待补充或退回单据。")
raise ValueError("只有高级财务人员可以删除非本人单据,申请人仅可删除自己的草稿、待补充或退回单据。")
before_json = self._serialize_claim(claim)
resource_id = claim.id
self._release_budget_for_delete(claim, current_user)
self._attachment_storage.delete_claim_files(claim)
self.db.delete(claim)
self.db.commit()
@@ -554,7 +563,7 @@ class ExpenseClaimService(
return None
if not self._access_policy.can_return_claim(current_user, claim):
raise ValueError("只有财务人员、高级管理人员或当前审批人可以退回报销单。")
raise ValueError("只有财务人员、高级财务人员或当前审批人可以退回报销单。")
normalized_status = str(claim.status or "").strip().lower()
if normalized_status == "draft":
@@ -619,10 +628,18 @@ class ExpenseClaimService(
if unknown_reason_codes:
return_flag["unknown_reason_codes"] = unknown_reason_codes
budget_flags = self._release_budget_for_return(
claim,
current_user,
reason=message,
)
claim.status = "returned"
claim.approval_stage = "待提交"
claim.submitted_at = None
claim.risk_flags_json = [*list(claim.risk_flags_json or []), return_flag]
claim.risk_flags_json = self._append_budget_flags(
[*list(claim.risk_flags_json or []), return_flag],
budget_flags,
)
self.db.commit()
self.db.refresh(claim)
@@ -691,6 +708,11 @@ class ExpenseClaimService(
before_json = self._serialize_claim(claim)
operator = self._access_policy.resolve_current_user_display_name(current_user)
budget_flags: list[dict[str, Any]] = []
if approval_source == "finance_approval" and not is_application_claim:
consumed_budget_flag = self._consume_budget_for_finance_approval(claim, current_user)
if consumed_budget_flag is not None:
budget_flags.append(consumed_budget_flag)
approval_flag = {
"source": approval_source,
"event_type": event_type,
@@ -723,7 +745,21 @@ class ExpenseClaimService(
approval_flag=approval_flag,
operator=operator,
)
claim.risk_flags_json = [*list(claim.risk_flags_json or []), approval_flag]
transferred_budget_flag = self._transfer_application_budget_to_reimbursement(
application_claim=claim,
draft_claim=generated_draft,
current_user=current_user,
)
if transferred_budget_flag is not None:
budget_flags.append(transferred_budget_flag)
generated_draft.risk_flags_json = self._append_budget_flags(
generated_draft.risk_flags_json,
transferred_budget_flag,
)
claim.risk_flags_json = self._append_budget_flags(
[*list(claim.risk_flags_json or []), approval_flag],
budget_flags,
)
self.db.commit()
self.db.refresh(claim)