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