feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -185,10 +185,7 @@ class ExpenseClaimAccessPolicy:
|
||||
return False
|
||||
if current_user.is_admin:
|
||||
return True
|
||||
role_codes = self.normalize_role_codes(current_user)
|
||||
if "executive" in role_codes:
|
||||
return True
|
||||
return self.is_department_p8_budget_monitor(current_user, claim)
|
||||
return self.is_department_budget_approver(current_user, claim)
|
||||
|
||||
def is_budget_manager_user(self, current_user: CurrentUserContext) -> bool:
|
||||
if current_user.is_admin:
|
||||
@@ -197,13 +194,16 @@ class ExpenseClaimAccessPolicy:
|
||||
return bool(role_codes & BUDGET_APPROVAL_ROLE_CODES)
|
||||
|
||||
def is_department_p8_budget_monitor(self, current_user: CurrentUserContext, claim: ExpenseClaim) -> bool:
|
||||
role_codes = self.normalize_role_codes(current_user)
|
||||
if BUDGET_MONITOR_ROLE_CODE not in role_codes:
|
||||
return False
|
||||
return self.is_department_budget_approver(current_user, claim)
|
||||
|
||||
def is_department_budget_approver(self, current_user: CurrentUserContext, claim: ExpenseClaim) -> bool:
|
||||
role_codes = self.normalize_role_codes(current_user)
|
||||
current_employee = self.resolve_current_employee(current_user)
|
||||
if current_employee is None:
|
||||
return False
|
||||
role_codes |= self._collect_employee_role_codes(current_employee)
|
||||
if not role_codes & BUDGET_APPROVAL_ROLE_CODES:
|
||||
return False
|
||||
if not self._employee_has_budget_approval_grade(current_employee):
|
||||
return False
|
||||
|
||||
@@ -224,7 +224,7 @@ class ExpenseClaimAccessPolicy:
|
||||
.options(selectinload(Employee.organization_unit), selectinload(Employee.roles))
|
||||
.where(
|
||||
func.upper(func.coalesce(Employee.grade, "")) == BUDGET_MONITOR_APPROVAL_GRADE,
|
||||
Employee.roles.any(Role.role_code == BUDGET_MONITOR_ROLE_CODE),
|
||||
Employee.roles.any(Role.role_code.in_(BUDGET_APPROVAL_ROLE_CODES)),
|
||||
or_(*department_conditions),
|
||||
)
|
||||
.order_by(Employee.name.asc(), Employee.employee_no.asc())
|
||||
@@ -235,6 +235,37 @@ class ExpenseClaimAccessPolicy:
|
||||
stmt = stmt.where(Employee.id != claim_employee_id)
|
||||
return self.db.scalar(stmt)
|
||||
|
||||
def resolve_budget_approval_role_code(self, employee: Employee | None) -> str:
|
||||
role_codes = self._collect_employee_role_codes(employee)
|
||||
for role_code in ("budget_monitor", "executive"):
|
||||
if role_code in role_codes:
|
||||
return role_code
|
||||
return BUDGET_MONITOR_ROLE_CODE
|
||||
|
||||
def attach_budget_approval_snapshot(self, claim: ExpenseClaim | None) -> ExpenseClaim | None:
|
||||
if claim is None:
|
||||
return None
|
||||
if str(claim.approval_stage or "").strip() != BUDGET_MANAGER_APPROVAL_STAGE:
|
||||
return claim
|
||||
|
||||
budget_manager = self.resolve_department_budget_manager(claim)
|
||||
if budget_manager is None:
|
||||
return claim
|
||||
|
||||
setattr(claim, "budget_approver_name", str(budget_manager.name or "").strip())
|
||||
setattr(claim, "budget_approver_grade", str(budget_manager.grade or "").strip())
|
||||
setattr(
|
||||
claim,
|
||||
"budget_approver_role_code",
|
||||
self.resolve_budget_approval_role_code(budget_manager),
|
||||
)
|
||||
return claim
|
||||
|
||||
def attach_budget_approval_snapshots(self, claims: list[ExpenseClaim]) -> list[ExpenseClaim]:
|
||||
for claim in claims:
|
||||
self.attach_budget_approval_snapshot(claim)
|
||||
return claims
|
||||
|
||||
@staticmethod
|
||||
def normalize_role_codes(current_user: CurrentUserContext) -> set[str]:
|
||||
return {
|
||||
@@ -243,6 +274,16 @@ class ExpenseClaimAccessPolicy:
|
||||
if str(item).strip()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _collect_employee_role_codes(employee: Employee | None) -> set[str]:
|
||||
if employee is None:
|
||||
return set()
|
||||
return {
|
||||
str(role.role_code or "").strip().lower()
|
||||
for role in list(employee.roles or [])
|
||||
if str(role.role_code or "").strip()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _employee_has_budget_approval_grade(employee: Employee) -> bool:
|
||||
return str(employee.grade or "").strip().upper() == BUDGET_MONITOR_APPROVAL_GRADE
|
||||
@@ -293,6 +334,7 @@ class ExpenseClaimAccessPolicy:
|
||||
[
|
||||
str(current_user.username or "").strip(),
|
||||
str(current_user.name or "").strip(),
|
||||
str(current_user.employee_no or "").strip(),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -309,9 +351,10 @@ class ExpenseClaimAccessPolicy:
|
||||
return str(current_user.username or current_user.name or "anonymous").strip() or "anonymous"
|
||||
|
||||
def is_claim_owned_by_current_user(self, claim: ExpenseClaim, current_user: CurrentUserContext) -> bool:
|
||||
claim_employee_id = str(claim.employee_id or "").strip()
|
||||
current_employee = self.resolve_current_employee(current_user)
|
||||
if current_employee is not None:
|
||||
if str(claim.employee_id or "").strip() == current_employee.id:
|
||||
if claim_employee_id == current_employee.id:
|
||||
return True
|
||||
identity_values = {
|
||||
str(current_employee.name or "").strip(),
|
||||
@@ -325,9 +368,12 @@ class ExpenseClaimAccessPolicy:
|
||||
{
|
||||
str(current_user.username or "").strip(),
|
||||
str(current_user.name or "").strip(),
|
||||
str(current_user.employee_no or "").strip(),
|
||||
}
|
||||
)
|
||||
identity_values.discard("")
|
||||
if claim_employee_id and claim_employee_id in identity_values:
|
||||
return True
|
||||
return str(claim.employee_name or "").strip() in identity_values
|
||||
|
||||
@staticmethod
|
||||
@@ -490,8 +536,10 @@ class ExpenseClaimAccessPolicy:
|
||||
add_condition("employee_name", employee.name)
|
||||
else:
|
||||
add_condition("employee_id", username)
|
||||
add_condition("employee_id", str(current_user.employee_no or "").strip())
|
||||
add_condition("employee_name", username)
|
||||
add_condition("employee_name", str(current_user.name or "").strip())
|
||||
add_condition("employee_name", str(current_user.employee_no or "").strip())
|
||||
|
||||
return conditions
|
||||
|
||||
@@ -531,10 +579,10 @@ class ExpenseClaimAccessPolicy:
|
||||
return conditions
|
||||
|
||||
def build_budget_approval_claim_conditions(self, current_user: CurrentUserContext) -> list[Any]:
|
||||
role_codes = self.normalize_role_codes(current_user)
|
||||
if BUDGET_MONITOR_ROLE_CODE not in role_codes:
|
||||
return []
|
||||
employee = self.resolve_current_employee(current_user)
|
||||
role_codes = self.normalize_role_codes(current_user) | self._collect_employee_role_codes(employee)
|
||||
if not role_codes & BUDGET_APPROVAL_ROLE_CODES:
|
||||
return []
|
||||
if employee is None or not self._employee_has_budget_approval_grade(employee):
|
||||
return []
|
||||
|
||||
@@ -568,7 +616,7 @@ class ExpenseClaimAccessPolicy:
|
||||
|
||||
def apply_approval_claim_scope(self, stmt: Any, current_user: CurrentUserContext) -> Any:
|
||||
role_codes = self.normalize_role_codes(current_user)
|
||||
if current_user.is_admin or "executive" in role_codes:
|
||||
if current_user.is_admin:
|
||||
return stmt.where(ExpenseClaim.status == "submitted")
|
||||
conditions = []
|
||||
if "finance" in role_codes:
|
||||
|
||||
Reference in New Issue
Block a user