feat(oa): resolve CC tasks with multi-strategy soft union
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
ef91e189dd
commit
835c1c0836
2 files changed
+143
-50
No files matched your search
@@ -22,7 +22,12 @@ from app.models.models import (
|
||||
SysDept,
|
||||
SysUser,
|
||||
)
|
||||
from app.services.oa_assignee import AssigneeResolveError, resolve_assignees
|
||||
from app.services.oa_assignee import (
|
||||
AssigneeResolveError,
|
||||
normalize_cc_assignee_types,
|
||||
resolve_assignees,
|
||||
resolve_cc_assignees,
|
||||
)
|
||||
from app.services.oa_formula import FormulaError, apply_formulas_and_linkages, eval_formula
|
||||
|
||||
|
||||
@@ -309,7 +314,33 @@ def _pending_people_for_node(
|
||||
node_select_ids = _ids_for_initiator_select_node(
|
||||
initiator_select_by_node, node.get("id") or ""
|
||||
)
|
||||
if assignee_type == "initiator_select":
|
||||
resolve_source = assignee_type
|
||||
if ntype == "cc":
|
||||
types = normalize_cc_assignee_types(cfg)
|
||||
assignees, meta = resolve_cc_assignees(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
assignee_config=cfg,
|
||||
initiator_user_id=initiator_user_id,
|
||||
initiator_dept_id=initiator_dept_id,
|
||||
advanced=advanced,
|
||||
initiator_select_user_ids=node_select_ids,
|
||||
)
|
||||
resolve_source = str(meta.get("resolve_source") or "")
|
||||
if not resolve_source:
|
||||
resolve_source = "multi" if len(types) > 1 else (types[0] if types else "")
|
||||
for uid in assignees:
|
||||
people.append(
|
||||
{
|
||||
"id": uid,
|
||||
"nickName": "",
|
||||
"status": "not_reached",
|
||||
"timestamp": "",
|
||||
"commentText": "",
|
||||
"taskId": "",
|
||||
}
|
||||
)
|
||||
elif assignee_type == "initiator_select":
|
||||
for uid in node_select_ids:
|
||||
people.append(
|
||||
{
|
||||
@@ -349,7 +380,7 @@ def _pending_people_for_node(
|
||||
name_map = _user_name_map(db, [p["id"] for p in people if p.get("id")])
|
||||
for p in people:
|
||||
p["nickName"] = name_map.get(p["id"], "") or p["id"]
|
||||
return people, sign_mode, assignee_type
|
||||
return people, sign_mode, resolve_source
|
||||
|
||||
|
||||
def _iter_resolved_path_nodes(
|
||||
@@ -1046,6 +1077,17 @@ def _assignee_meta(db: Session, tenant_no: str, cfg: Dict[str, Any]) -> str:
|
||||
return base
|
||||
|
||||
|
||||
def _assignee_meta_for_cc(
|
||||
db: Session, tenant_no: str, cfg: Dict[str, Any], types: List[str]
|
||||
) -> str:
|
||||
if not types:
|
||||
return "未配置抄送人"
|
||||
if len(types) == 1:
|
||||
return _assignee_meta(db, tenant_no, {**cfg, "assigneeType": types[0]})
|
||||
labels = [_ASSIGNEE_TYPE_LABEL.get(t, t) for t in types]
|
||||
return " + ".join(labels)
|
||||
|
||||
|
||||
def _preview_people(db: Session, user_ids: List[str]) -> List[Dict[str, str]]:
|
||||
name_map = _user_name_map(db, user_ids)
|
||||
out: List[Dict[str, str]] = []
|
||||
@@ -1216,48 +1258,86 @@ def _walk_preview_nodes(
|
||||
|
||||
if ntype in ("approval", "cc"):
|
||||
cfg = cur.get("approverConfig") or {}
|
||||
assignee_type = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip()
|
||||
sign_mode = (cfg.get("signMode") or cfg.get("sign_mode") or "").strip()
|
||||
if ntype == "cc":
|
||||
sign_mode = "cc"
|
||||
elif not sign_mode and assignee_type != "initiator_select":
|
||||
sign_mode = "or"
|
||||
meta = _assignee_meta(db, tenant_no, cfg)
|
||||
people: List[Dict[str, str]] = []
|
||||
resolve_error = ""
|
||||
node_select_ids = _ids_for_initiator_select_node(
|
||||
initiator_select_by_node, cur.get("id") or ""
|
||||
)
|
||||
if assignee_type == "initiator_select":
|
||||
people = _preview_people(db, [str(x) for x in node_select_ids if str(x).strip()])
|
||||
if not people:
|
||||
meta = meta or "发起人自选"
|
||||
if ntype == "cc":
|
||||
types = normalize_cc_assignee_types(cfg)
|
||||
if "initiator_select" in types:
|
||||
display_type = "initiator_select"
|
||||
elif len(types) == 1:
|
||||
display_type = types[0]
|
||||
elif types:
|
||||
display_type = "multi"
|
||||
else:
|
||||
display_type = ""
|
||||
sign_mode = "cc"
|
||||
meta = _assignee_meta_for_cc(db, tenant_no, cfg, types)
|
||||
assignees, _meta = resolve_cc_assignees(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
assignee_config=cfg,
|
||||
initiator_user_id=initiator_user_id,
|
||||
initiator_dept_id=initiator_dept_id,
|
||||
advanced=advanced,
|
||||
initiator_select_user_ids=node_select_ids,
|
||||
)
|
||||
people = _preview_people(db, assignees)
|
||||
steps.append(
|
||||
{
|
||||
"id": cur.get("id") or "",
|
||||
"type": ntype,
|
||||
"name": cur.get("name") or "抄送人",
|
||||
"signMode": sign_mode,
|
||||
"assigneeType": display_type,
|
||||
"assigneeTypes": types,
|
||||
"meta": meta,
|
||||
"people": people,
|
||||
"resolveError": resolve_error,
|
||||
}
|
||||
)
|
||||
else:
|
||||
try:
|
||||
assignees, _meta = resolve_assignees(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
assignee_config=cfg,
|
||||
initiator_user_id=initiator_user_id,
|
||||
initiator_dept_id=initiator_dept_id,
|
||||
advanced=advanced,
|
||||
initiator_select_user_ids=node_select_ids,
|
||||
assignee_type = (
|
||||
cfg.get("assigneeType") or cfg.get("assignee_type") or ""
|
||||
).strip()
|
||||
if not sign_mode and assignee_type != "initiator_select":
|
||||
sign_mode = "or"
|
||||
meta = _assignee_meta(db, tenant_no, cfg)
|
||||
if assignee_type == "initiator_select":
|
||||
people = _preview_people(
|
||||
db, [str(x) for x in node_select_ids if str(x).strip()]
|
||||
)
|
||||
people = _preview_people(db, assignees)
|
||||
except AssigneeResolveError as e:
|
||||
resolve_error = str(e)
|
||||
steps.append(
|
||||
{
|
||||
"id": cur.get("id") or "",
|
||||
"type": ntype,
|
||||
"name": cur.get("name") or ("抄送人" if ntype == "cc" else "审批人"),
|
||||
"signMode": sign_mode,
|
||||
"assigneeType": assignee_type,
|
||||
"meta": meta,
|
||||
"people": people,
|
||||
"resolveError": resolve_error,
|
||||
}
|
||||
)
|
||||
if not people:
|
||||
meta = meta or "发起人自选"
|
||||
else:
|
||||
try:
|
||||
assignees, _meta = resolve_assignees(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
assignee_config=cfg,
|
||||
initiator_user_id=initiator_user_id,
|
||||
initiator_dept_id=initiator_dept_id,
|
||||
advanced=advanced,
|
||||
initiator_select_user_ids=node_select_ids,
|
||||
)
|
||||
people = _preview_people(db, assignees)
|
||||
except AssigneeResolveError as e:
|
||||
resolve_error = str(e)
|
||||
steps.append(
|
||||
{
|
||||
"id": cur.get("id") or "",
|
||||
"type": ntype,
|
||||
"name": cur.get("name") or "审批人",
|
||||
"signMode": sign_mode,
|
||||
"assigneeType": assignee_type,
|
||||
"meta": meta,
|
||||
"people": people,
|
||||
"resolveError": resolve_error,
|
||||
}
|
||||
)
|
||||
cur = cur.get("child")
|
||||
continue
|
||||
|
||||
@@ -1353,19 +1433,13 @@ def _create_tasks_for_node(
|
||||
return created
|
||||
|
||||
cfg = node.get("approverConfig") or {}
|
||||
assignee_type = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip()
|
||||
node_select_ids = _ids_for_initiator_select_node(
|
||||
initiator_select_by_node, node.get("id") or ""
|
||||
)
|
||||
if assignee_type == "initiator_select":
|
||||
if ntype == "cc" and not node_select_ids:
|
||||
return created
|
||||
if ntype == "approval" and not node_select_ids:
|
||||
raise OaEngineError("发起人未选择审批人")
|
||||
|
||||
advanced = _advanced(ver)
|
||||
try:
|
||||
assignees, meta = resolve_assignees(
|
||||
|
||||
if ntype == "cc":
|
||||
assignees, meta = resolve_cc_assignees(
|
||||
db,
|
||||
tenant_no=inst.tenant_no,
|
||||
assignee_config=cfg,
|
||||
@@ -1374,8 +1448,24 @@ def _create_tasks_for_node(
|
||||
advanced=advanced,
|
||||
initiator_select_user_ids=node_select_ids,
|
||||
)
|
||||
except AssigneeResolveError as e:
|
||||
raise OaEngineError(str(e)) from e
|
||||
if not assignees:
|
||||
return created
|
||||
else:
|
||||
assignee_type = (cfg.get("assigneeType") or cfg.get("assignee_type") or "").strip()
|
||||
if assignee_type == "initiator_select" and not node_select_ids:
|
||||
raise OaEngineError("发起人未选择审批人")
|
||||
try:
|
||||
assignees, meta = resolve_assignees(
|
||||
db,
|
||||
tenant_no=inst.tenant_no,
|
||||
assignee_config=cfg,
|
||||
initiator_user_id=inst.initiator_user_id,
|
||||
initiator_dept_id=inst.initiator_dept_id,
|
||||
advanced=advanced,
|
||||
initiator_select_user_ids=node_select_ids,
|
||||
)
|
||||
except AssigneeResolveError as e:
|
||||
raise OaEngineError(str(e)) from e
|
||||
|
||||
sign_mode = (cfg.get("signMode") or cfg.get("sign_mode") or "or").strip() or "or"
|
||||
resolve_source = meta.get("resolve_source")
|
||||
|
||||
@@ -43,7 +43,10 @@ class TestCreateTasksInitiatorSelect(unittest.TestCase):
|
||||
}
|
||||
ver = MagicMock()
|
||||
ver.advanced_settings_json = "{}"
|
||||
with patch("app.services.oa_engine.resolve_assignees") as resolve:
|
||||
with patch(
|
||||
"app.services.oa_engine.resolve_cc_assignees",
|
||||
return_value=([], {"resolve_source": "", "skipped": [{"type": "initiator_select"}]}),
|
||||
) as resolve:
|
||||
created = _create_tasks_for_node(
|
||||
db,
|
||||
inst=self._inst(),
|
||||
@@ -52,7 +55,7 @@ class TestCreateTasksInitiatorSelect(unittest.TestCase):
|
||||
user_id="init",
|
||||
initiator_select_by_node={},
|
||||
)
|
||||
resolve.assert_not_called()
|
||||
resolve.assert_called_once()
|
||||
self.assertEqual(created, [])
|
||||
|
||||
def test_approval_empty_raises(self):
|
||||
|
||||
Reference in new issue
Block a user