Files
zkxw/backend/app/services/oa_assignee.py
T
zlwuandCursor db31990ebe feat: 细胞存储缴费催缴、统一样本信息与协议支付能力增强
汇总近期业务改动:催缴菜单与后端、样本信息快捷登记与进度、运费关联、客户档案扩展、协议优惠/期次与押金划转,并同步 SQL 迁移与管理端页面收敛。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 21:20:37 +08:00

249 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OA 审批人解析(不读 sys_role;职位成员走 sys_user_post)"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy.orm import Session
from app.crud import user as user_crud
from app.models.models import SysDept, SysUser
class AssigneeResolveError(ValueError):
"""审批人解析失败"""
VALID_ASSIGNEE_TYPES = frozenset(
{
"users",
"posts",
"dept_leader",
"direct_supervisor",
"initiator_select",
}
)
def normalize_cc_assignee_types(assignee_config: Optional[Dict[str, Any]]) -> List[str]:
cfg = assignee_config or {}
raw = cfg.get("assigneeTypes") or cfg.get("assignee_types")
out: List[str] = []
seen = set()
if isinstance(raw, list) and raw:
for item in raw:
t = str(item or "").strip()
if t in VALID_ASSIGNEE_TYPES and t not in seen:
seen.add(t)
out.append(t)
return out
single = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip()
if single in VALID_ASSIGNEE_TYPES:
return [single]
return []
def resolve_cc_assignees(
db: Session,
*,
tenant_no: str,
assignee_config: Dict[str, Any],
initiator_user_id: str,
initiator_dept_id: Optional[str] = None,
advanced: Optional[Dict[str, Any]] = None,
initiator_select_user_ids: Optional[List[str]] = None,
) -> Tuple[List[str], Dict[str, Any]]:
"""抄送多策略软并集:单策略失败/空则跳过;全部无人返回 [],不抛错。"""
cfg = assignee_config or {}
types = normalize_cc_assignee_types(cfg)
merged: List[str] = []
seen = set()
sources: List[str] = []
skipped: List[Dict[str, str]] = []
for t in types:
single_cfg = {**cfg, "assigneeType": t}
try:
users, meta = resolve_assignees(
db,
tenant_no=tenant_no,
assignee_config=single_cfg,
initiator_user_id=initiator_user_id,
initiator_dept_id=initiator_dept_id,
advanced=advanced,
initiator_select_user_ids=initiator_select_user_ids,
)
except AssigneeResolveError as e:
skipped.append({"type": t, "reason": str(e)})
continue
if not users:
skipped.append({"type": t, "reason": "empty"})
continue
src = str((meta or {}).get("resolve_source") or t)
sources.append(src)
for uid in users:
if uid and uid not in seen:
seen.add(uid)
merged.append(uid)
if len(sources) == 1:
resolve_source = sources[0]
elif len(sources) > 1:
resolve_source = "multi"
else:
resolve_source = ""
return merged, {
"resolve_source": resolve_source,
"sources": sources,
"skipped": skipped,
"assigneeTypes": types,
}
def _active_user_id(db: Session, user_id: Optional[str], tenant_no: str) -> Optional[str]:
uid = (user_id or "").strip()
if not uid:
return None
u = (
db.query(SysUser)
.filter(
SysUser.id == uid,
SysUser.tenant_no == tenant_no,
SysUser.del_flag == 0,
SysUser.status == "1",
)
.first()
)
return u.id if u else None
def resolve_dept_leader(
db: Session, *, tenant_no: str, initiator_dept_id: Optional[str]
) -> Tuple[List[str], Dict[str, Any]]:
dept_id = (initiator_dept_id or "").strip()
if not dept_id:
raise AssigneeResolveError("发起人部门为空,无法解析部门负责人")
dept = (
db.query(SysDept)
.filter(SysDept.id == dept_id, SysDept.tenant_no == tenant_no, SysDept.del_flag == 0)
.first()
)
if not dept:
raise AssigneeResolveError("发起人部门不存在")
leader_uid = _active_user_id(db, getattr(dept, "leader_user_id", None), tenant_no)
if not leader_uid:
raise AssigneeResolveError("部门负责人未配置或用户无效,请在组织管理中维护负责人")
return [leader_uid], {"resolve_source": "dept_leader"}
def resolve_direct_supervisor(
db: Session,
*,
tenant_no: str,
initiator_user_id: str,
initiator_dept_id: Optional[str],
fallback_to_dept_leader: bool = True,
) -> Tuple[List[str], Dict[str, Any]]:
initiator = (
db.query(SysUser)
.filter(
SysUser.id == initiator_user_id,
SysUser.tenant_no == tenant_no,
SysUser.del_flag == 0,
)
.first()
)
if not initiator:
raise AssigneeResolveError("发起人不存在")
supervisor_uid = _active_user_id(
db, getattr(initiator, "supervisor_user_id", None), tenant_no
)
if supervisor_uid:
# 允许主管配置为本人(如部门经理),由高级设置 autoPassWhenApproverIsInitiator 决定是否自动通过
return [supervisor_uid], {"resolve_source": "direct_supervisor"}
if fallback_to_dept_leader:
users, meta = resolve_dept_leader(
db, tenant_no=tenant_no, initiator_dept_id=initiator_dept_id or initiator.dept_id
)
meta = {**meta, "resolve_source": "supervisor_fallback_dept_leader"}
return users, meta
raise AssigneeResolveError("未配置直属主管,且未开启回退部门负责人")
def resolve_assignees(
db: Session,
*,
tenant_no: str,
assignee_config: Dict[str, Any],
initiator_user_id: str,
initiator_dept_id: Optional[str] = None,
advanced: Optional[Dict[str, Any]] = None,
initiator_select_user_ids: Optional[List[str]] = None,
) -> Tuple[List[str], Dict[str, Any]]:
"""
按节点 approverConfig 解析处理人。
assigneeType: users | posts | dept_leader | direct_supervisor | initiator_select
"""
cfg = assignee_config or {}
assignee_type = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip()
advanced = advanced or {}
fallback = advanced.get("supervisorFallbackToDeptLeader", True)
if isinstance(fallback, str):
fallback = fallback.lower() in ("1", "true", "yes")
if assignee_type == "users":
ids = cfg.get("userIds") or cfg.get("user_ids") or []
out: List[str] = []
seen = set()
for uid in ids:
real = _active_user_id(db, uid, tenant_no)
if real and real not in seen:
seen.add(real)
out.append(real)
if not out:
raise AssigneeResolveError("指定成员为空或均无效")
return out, {"resolve_source": "users"}
if assignee_type == "posts":
post_ids = cfg.get("postIds") or cfg.get("post_ids") or []
users = user_crud.list_user_ids_bound_to_posts(db, list(post_ids), tenant_no)
out = []
seen = set()
for uid in users:
real = _active_user_id(db, uid, tenant_no)
if real and real not in seen:
seen.add(real)
out.append(real)
if not out:
raise AssigneeResolveError("职位成员为空或均无效")
return out, {"resolve_source": "posts"}
if assignee_type == "dept_leader":
return resolve_dept_leader(
db, tenant_no=tenant_no, initiator_dept_id=initiator_dept_id
)
if assignee_type == "direct_supervisor":
return resolve_direct_supervisor(
db,
tenant_no=tenant_no,
initiator_user_id=initiator_user_id,
initiator_dept_id=initiator_dept_id,
fallback_to_dept_leader=bool(fallback),
)
if assignee_type == "initiator_select":
ids = initiator_select_user_ids or cfg.get("userIds") or []
out = []
seen = set()
for uid in ids:
real = _active_user_id(db, uid, tenant_no)
if real and real not in seen:
seen.add(real)
out.append(real)
if not out:
raise AssigneeResolveError("发起人未选择审批人")
return out, {"resolve_source": "initiator_select"}
raise AssigneeResolveError(f"不支持的审批人类型: {assignee_type or '(空)'}")