feat(claim): 重构报销审批流并收敛风险标记
- 直属领导兼任部门 P8 预算审批人时合并预算审批,直接流转至财务审批 - 预算超过警戒值时强制要求预算管理者填写审批意见 - 新增风险标记去重工具,消除各审核阶段重复风险卡片 - 新增工作流修复 Mixin,纠正重复预算审批阶段的历史数据 - 收紧单据删除权限至 admin,放宽预算分析可见范围至当前审核人 - 提交校验放宽已上传票据条目的 OCR 字段缺失并忽略尾部占位条目
This commit is contained in:
@@ -2,9 +2,11 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from app.api.deps import CurrentUserContext
|
||||
from app.services.budget import BudgetService
|
||||
from app.services.expense_claim_workflow_constants import (
|
||||
APPLICATION_LINK_STATUS_STAGE,
|
||||
BUDGET_MANAGER_APPROVAL_STAGE,
|
||||
@@ -76,7 +78,16 @@ class ExpenseClaimApprovalFlowMixin:
|
||||
next_stage = APPLICATION_LINK_STATUS_STAGE
|
||||
default_message = "{operator} 已确认直属领导审核,系统判断预算充足且无风险,申请流程完成并生成报销草稿。"
|
||||
else:
|
||||
if requires_budget_review:
|
||||
merged_budget_approval = (
|
||||
requires_budget_review
|
||||
and self._access_policy.is_department_p8_budget_monitor(current_user, claim)
|
||||
)
|
||||
if merged_budget_approval:
|
||||
label = "领导及预算审核通过"
|
||||
next_status = "submitted"
|
||||
next_stage = FINANCE_APPROVAL_STAGE
|
||||
default_message = "{operator} 已完成直属领导和预算管理者审核,流转至{next_stage}。"
|
||||
elif requires_budget_review:
|
||||
next_budget_manager = self._access_policy.resolve_department_budget_manager(claim)
|
||||
if next_budget_manager is None:
|
||||
raise ValueError("未找到同部门 P8 预算审批人,无法流转预算审批。请先配置预算审批人。")
|
||||
@@ -120,6 +131,19 @@ class ExpenseClaimApprovalFlowMixin:
|
||||
raise ValueError("当前节点不支持审批通过。")
|
||||
|
||||
approval_opinion = str(opinion or "").strip()
|
||||
if (
|
||||
previous_stage == BUDGET_MANAGER_APPROVAL_STAGE
|
||||
and self._budget_approval_opinion_required(claim)
|
||||
and not approval_opinion
|
||||
):
|
||||
raise ValueError("预算已超过警戒值,预算管理者需填写审批意见后才能通过。")
|
||||
if (
|
||||
previous_stage == DIRECT_MANAGER_APPROVAL_STAGE
|
||||
and merged_budget_approval
|
||||
and self._budget_approval_opinion_required(claim)
|
||||
and not approval_opinion
|
||||
):
|
||||
raise ValueError("预算已超过警戒值,预算管理者需填写审批意见后才能通过。")
|
||||
if previous_stage in {DIRECT_MANAGER_APPROVAL_STAGE, BUDGET_MANAGER_APPROVAL_STAGE} and not approval_opinion:
|
||||
approval_opinion = "同意"
|
||||
|
||||
@@ -327,3 +351,28 @@ class ExpenseClaimApprovalFlowMixin:
|
||||
if opinion:
|
||||
return opinion
|
||||
return ""
|
||||
|
||||
def _budget_approval_opinion_required(self, claim) -> bool:
|
||||
budget_result = BudgetService(self.db).analyze_claim_budget(claim)
|
||||
metrics = budget_result.get("metrics") if isinstance(budget_result.get("metrics"), dict) else {}
|
||||
context = (
|
||||
budget_result.get("budget_context")
|
||||
if isinstance(budget_result.get("budget_context"), dict)
|
||||
else {}
|
||||
)
|
||||
|
||||
over_budget_amount = self._budget_decimal(metrics.get("over_budget_amount"))
|
||||
if over_budget_amount > Decimal("0.00"):
|
||||
return True
|
||||
|
||||
after_usage_rate = self._budget_decimal(metrics.get("after_usage_rate"))
|
||||
claim_amount_ratio = self._budget_decimal(metrics.get("claim_amount_ratio"))
|
||||
warning_threshold = self._budget_decimal(context.get("warning_threshold") or "80.00")
|
||||
return max(after_usage_rate, claim_amount_ratio) >= warning_threshold
|
||||
|
||||
@staticmethod
|
||||
def _budget_decimal(value: Any) -> Decimal:
|
||||
try:
|
||||
return Decimal(str(value if value is not None else "0")).quantize(Decimal("0.01"))
|
||||
except (InvalidOperation, ValueError):
|
||||
return Decimal("0.00")
|
||||
|
||||
Reference in New Issue
Block a user