feat(claim): 重构报销审批流并收敛风险标记
- 直属领导兼任部门 P8 预算审批人时合并预算审批,直接流转至财务审批 - 预算超过警戒值时强制要求预算管理者填写审批意见 - 新增风险标记去重工具,消除各审核阶段重复风险卡片 - 新增工作流修复 Mixin,纠正重复预算审批阶段的历史数据 - 收紧单据删除权限至 admin,放宽预算分析可见范围至当前审核人 - 提交校验放宽已上传票据条目的 OCR 字段缺失并忽略尾部占位条目
This commit is contained in:
@@ -49,6 +49,7 @@ from app.services.expense_claim_attachment_document import ExpenseClaimAttachmen
|
||||
from app.services.expense_claim_attachment_operations import ExpenseClaimAttachmentOperationsMixin
|
||||
from app.services.expense_claim_budget_flow import ExpenseClaimBudgetFlowMixin
|
||||
from app.services.expense_claim_workflow_constants import DIRECT_MANAGER_APPROVAL_STAGE
|
||||
from app.services.expense_claim_workflow_repair import ExpenseClaimWorkflowRepairMixin
|
||||
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
|
||||
@@ -58,6 +59,7 @@ from app.services.expense_claim_pagination import ExpenseClaimPaginationMixin
|
||||
from app.services.expense_claim_pre_review import ExpenseClaimPreReviewMixin
|
||||
from app.services.expense_claim_ontology_resolvers import ExpenseClaimOntologyResolverMixin
|
||||
from app.services.expense_claim_read_model import ExpenseClaimReadModelMixin
|
||||
from app.services.expense_claim_risk_flags import dedupe_claim_risk_flags
|
||||
from app.services.expense_claim_risk_stage import with_risk_business_stage
|
||||
from app.services.expense_claim_review_preview import ExpenseClaimReviewPreviewMixin
|
||||
from app.services.receipt_folder import ReceiptFolderService
|
||||
@@ -156,6 +158,7 @@ class ExpenseClaimService(
|
||||
ExpenseClaimAttachmentAnalysisMixin,
|
||||
ExpenseClaimReadModelMixin,
|
||||
ExpenseClaimRiskReviewMixin,
|
||||
ExpenseClaimWorkflowRepairMixin,
|
||||
):
|
||||
def __init__(self, db: Session) -> None:
|
||||
self.db = db
|
||||
@@ -210,7 +213,9 @@ class ExpenseClaimService(
|
||||
.order_by(ExpenseClaim.created_at.desc(), ExpenseClaim.occurred_at.desc())
|
||||
)
|
||||
stmt = self._access_policy.apply_claim_scope(stmt, current_user)
|
||||
return self._access_policy.attach_budget_approval_snapshots(list(self.db.scalars(stmt).all()))
|
||||
claims = list(self.db.scalars(stmt).all())
|
||||
self._repair_duplicate_budget_approval_stages(claims)
|
||||
return self._access_policy.attach_budget_approval_snapshots(claims)
|
||||
|
||||
def list_approval_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]:
|
||||
stmt = (
|
||||
@@ -224,7 +229,9 @@ class ExpenseClaimService(
|
||||
.order_by(ExpenseClaim.submitted_at.desc(), ExpenseClaim.created_at.desc())
|
||||
)
|
||||
stmt = self._access_policy.apply_approval_claim_scope(stmt, current_user)
|
||||
return self._access_policy.attach_budget_approval_snapshots(list(self.db.scalars(stmt).all()))
|
||||
claims = list(self.db.scalars(stmt).all())
|
||||
self._repair_duplicate_budget_approval_stages(claims)
|
||||
return self._access_policy.attach_budget_approval_snapshots(claims)
|
||||
|
||||
def list_archived_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]:
|
||||
stmt = (
|
||||
@@ -252,7 +259,10 @@ class ExpenseClaimService(
|
||||
.where(ExpenseClaim.id == claim_id)
|
||||
)
|
||||
stmt = self._access_policy.apply_claim_scope(stmt, current_user, include_approval_scope=True)
|
||||
return self._access_policy.attach_approval_snapshot(self.db.scalar(stmt))
|
||||
claim = self.db.scalar(stmt)
|
||||
if claim is not None:
|
||||
self._repair_duplicate_budget_approval_stages([claim])
|
||||
return self._access_policy.attach_approval_snapshot(claim)
|
||||
|
||||
def can_view_budget_analysis(self, current_user: CurrentUserContext, claim: ExpenseClaim | None = None) -> bool:
|
||||
if claim is None:
|
||||
@@ -262,6 +272,13 @@ class ExpenseClaimService(
|
||||
role_codes = self._access_policy.normalize_role_codes(current_user)
|
||||
if "executive" in role_codes:
|
||||
return True
|
||||
if (
|
||||
self._access_policy.has_privileged_claim_access(current_user)
|
||||
and not self._access_policy.is_claim_owned_by_current_user(claim, current_user)
|
||||
):
|
||||
return True
|
||||
if self._access_policy.can_approve_claim(current_user, claim):
|
||||
return True
|
||||
if self._access_policy.is_claim_owned_by_current_user(claim, current_user):
|
||||
return False
|
||||
return self._access_policy.is_department_p8_budget_monitor(current_user, claim)
|
||||
@@ -545,7 +562,7 @@ class ExpenseClaimService(
|
||||
and str(flag.get("source") or "").strip() == STANDARD_ADJUSTMENT_RISK_SOURCE
|
||||
)
|
||||
]
|
||||
claim.risk_flags_json = [*preserved_flags, *adjustment_flags]
|
||||
claim.risk_flags_json = dedupe_claim_risk_flags([*preserved_flags, *adjustment_flags])
|
||||
self._sync_claim_from_items(claim)
|
||||
|
||||
self.db.commit()
|
||||
@@ -805,6 +822,7 @@ class ExpenseClaimService(
|
||||
claim.approval_stage = DIRECT_MANAGER_APPROVAL_STAGE
|
||||
claim.submitted_at = datetime.now(UTC)
|
||||
|
||||
claim.risk_flags_json = dedupe_claim_risk_flags(claim.risk_flags_json)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(claim)
|
||||
@@ -829,9 +847,7 @@ class ExpenseClaimService(
|
||||
|
||||
def delete_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None:
|
||||
claim = self.get_claim(claim_id, current_user)
|
||||
if claim is None and (
|
||||
current_user.is_admin or self._access_policy.has_archive_center_access(current_user)
|
||||
):
|
||||
if claim is None and current_user.is_admin:
|
||||
candidate_claim = self.db.scalar(
|
||||
select(ExpenseClaim)
|
||||
.options(
|
||||
@@ -841,13 +857,14 @@ class ExpenseClaimService(
|
||||
)
|
||||
.where(ExpenseClaim.id == claim_id)
|
||||
)
|
||||
if candidate_claim is not None and (
|
||||
current_user.is_admin or self._access_policy.is_archived_claim(candidate_claim)
|
||||
):
|
||||
if candidate_claim is not None:
|
||||
claim = candidate_claim
|
||||
if claim is None:
|
||||
return None
|
||||
|
||||
if not self._access_policy.has_claim_delete_access(current_user):
|
||||
raise ValueError("只有 admin 管理员可以删除单据。")
|
||||
|
||||
if self._access_policy.is_archived_claim(claim) and not current_user.is_admin:
|
||||
raise ValueError("已归档单据不能删除,只有高级管理员可以执行删除。")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user