diff --git a/backend/app/services/oa_engine.py b/backend/app/services/oa_engine.py index 63f81821..926e34d7 100644 --- a/backend/app/services/oa_engine.py +++ b/backend/app/services/oa_engine.py @@ -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") diff --git a/backend/tests/test_oa_initiator_select_by_node.py b/backend/tests/test_oa_initiator_select_by_node.py index 68c08792..3fb58b16 100644 --- a/backend/tests/test_oa_initiator_select_by_node.py +++ b/backend/tests/test_oa_initiator_select_by_node.py @@ -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):