feat: 增强风险规则生成引擎与预算中心页面

后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块,
优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强
报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图
组件,重构审计页面和风险规则测试对话框交互,完善文档中心
和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-26 09:15:14 +08:00
parent d0e946cf47
commit 0e861d8fa6
150 changed files with 14953 additions and 4099 deletions

View File

@@ -17,6 +17,7 @@ ARCHIVE_CENTER_ROLE_CODES = {"finance", "executive", "auditor"}
APPROVAL_VISIBLE_CLAIM_ROLE_CODES = {"manager", "approver"}
CLAIM_DELETE_ROLE_CODES = {"executive"}
ARCHIVED_CLAIM_STATUSES = ("approved", "completed", "paid")
APPLICATION_ARCHIVED_STAGES = ("审批完成", "申请归档", "completed")
class ExpenseClaimAccessPolicy:
@@ -39,9 +40,22 @@ class ExpenseClaimAccessPolicy:
def build_archived_claim_condition() -> Any:
normalized_status = func.lower(func.coalesce(ExpenseClaim.status, ""))
stage = func.coalesce(ExpenseClaim.approval_stage, "")
normalized_type = func.lower(func.coalesce(ExpenseClaim.expense_type, ""))
claim_no = func.upper(func.coalesce(ExpenseClaim.claim_no, ""))
application_condition = or_(
claim_no.like("AP-%"),
claim_no.like("APP-%"),
normalized_type == "application",
normalized_type.like("%\\_application", escape="\\"),
)
return or_(
stage == "归档入账",
stage == "completed",
and_(
application_condition,
normalized_status.in_(ARCHIVED_CLAIM_STATUSES),
stage.in_(APPLICATION_ARCHIVED_STAGES),
),
and_(
normalized_status.in_(ARCHIVED_CLAIM_STATUSES),
or_(
@@ -59,6 +73,27 @@ class ExpenseClaimAccessPolicy:
return True
return bool(ExpenseClaimAccessPolicy.normalize_role_codes(current_user) & CLAIM_DELETE_ROLE_CODES)
@staticmethod
def is_archived_claim(claim: ExpenseClaim) -> bool:
normalized_status = str(claim.status or "").strip().lower()
stage = str(claim.approval_stage or "").strip()
if stage in {"归档入账", "completed"}:
return True
normalized_type = str(claim.expense_type or "").strip().lower()
claim_no = str(claim.claim_no or "").strip().upper()
is_application_claim = (
claim_no.startswith(("AP-", "APP-"))
or normalized_type == "application"
or normalized_type.endswith("_application")
)
if (
is_application_claim
and normalized_status in ARCHIVED_CLAIM_STATUSES
and stage in APPLICATION_ARCHIVED_STAGES
):
return True
return normalized_status in ARCHIVED_CLAIM_STATUSES and stage in {"", "归档入账", "completed"}
def can_return_claim(self, current_user: CurrentUserContext, claim: ExpenseClaim) -> bool:
if self.has_privileged_claim_access(current_user):
return True
@@ -338,6 +373,7 @@ class ExpenseClaimAccessPolicy:
else:
add_condition("employee_id", username)
add_condition("employee_name", username)
add_condition("employee_name", str(current_user.name or "").strip())
return conditions
@@ -422,10 +458,14 @@ class ExpenseClaimAccessPolicy:
return stmt.where(or_(*conditions))
def apply_archived_claim_scope(self, stmt: Any, current_user: CurrentUserContext) -> Any:
archived_condition = self.build_archived_claim_condition()
if not self.has_archive_center_access(current_user):
return stmt.where(ExpenseClaim.id == "__no_visible_claim__")
owned_conditions = self.build_personal_claim_conditions(current_user)
if not owned_conditions:
return stmt.where(ExpenseClaim.id == "__no_visible_claim__")
return stmt.where(archived_condition, or_(*owned_conditions))
return stmt.where(self.build_archived_claim_condition())
return stmt.where(archived_condition)
@staticmethod
def resolve_claim_manager_name(claim: ExpenseClaim) -> str: