| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- """OA:审批人去重 — 同一实例内已同意的人,后续节点系统自动通过。"""
- import json
- import unittest
- from datetime import datetime
- from unittest.mock import patch
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- def _resolve_from_config(_db, *, assignee_config, **_kwargs):
- ids = list(assignee_config.get("userIds") or [])
- return ids, {"resolve_source": "users"}
- class TestApproverDedup(unittest.TestCase):
- def setUp(self):
- from app.models.models import OaAppVersion, OaInstance, OaTask
- self.engine = create_engine("sqlite:///:memory:")
- OaAppVersion.__table__.create(self.engine)
- OaInstance.__table__.create(self.engine)
- OaTask.__table__.create(self.engine)
- Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
- self.db = Session()
- self.OaAppVersion = OaAppVersion
- self.OaInstance = OaInstance
- self.OaTask = OaTask
- self.now = datetime.now()
- def tearDown(self):
- self.db.close()
- self.engine.dispose()
- def _seed(self, *, flow, advanced, initiator="u0"):
- self.db.add(
- self.OaAppVersion(
- id="v1",
- app_id="app1",
- version_no=1,
- status="published",
- is_latest_published="1",
- form_schema_json="{}",
- flow_tree_json=json.dumps(flow),
- advanced_settings_json=json.dumps(advanced),
- tenant_no="zkxw",
- create_dept_id="",
- create_time=self.now,
- update_time=self.now,
- del_flag=0,
- )
- )
- self.db.add(
- self.OaInstance(
- id="i1",
- app_id="app1",
- app_version_id="v1",
- title="t",
- initiator_user_id=initiator,
- initiator_dept_id="d1",
- form_data_json="{}",
- status="running",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=self.now,
- update_time=self.now,
- del_flag=0,
- )
- )
- self.db.commit()
- def _load(self):
- inst = self.db.query(self.OaInstance).filter_by(id="i1").first()
- ver = self.db.query(self.OaAppVersion).filter_by(id="v1").first()
- return inst, ver
- def _tasks(self):
- self.db.flush()
- return self.db.query(self.OaTask).filter_by(instance_id="i1").all()
- def _pending(self, user_id):
- return next(
- t
- for t in self._tasks()
- if t.assignee_user_id == user_id and t.status == "pending"
- )
- def _approve(self, task):
- from app.services.oa_engine import _approve_task_core
- inst, ver = self._load()
- _approve_task_core(
- self.db,
- task=task,
- inst=inst,
- ver=ver,
- user_id=task.assignee_user_id,
- comment="同意",
- )
- def _chain_flow(self):
- """第 1 步 A,第 2 步 B,第 3、4 步再是 A。"""
- def approval(node_id, name, user_ids, child):
- return {
- "id": node_id,
- "type": "approval",
- "name": name,
- "approverConfig": {
- "assigneeType": "users",
- "userIds": user_ids,
- "signMode": "or",
- },
- "child": child,
- }
- return {
- "id": "s",
- "type": "start",
- "child": approval(
- "a1",
- "第一步",
- ["A"],
- approval(
- "a2",
- "第二步",
- ["B"],
- approval(
- "a3",
- "第三步",
- ["A"],
- approval("a4", "第四步", ["A"], {"id": "e1", "type": "end"}),
- ),
- ),
- ),
- }
- def test_later_nodes_auto_pass_after_same_person_approved(self):
- """A 在第 1 步同意后,第 3、4 步不再待办,系统记为自动通过。"""
- from app.services.oa_engine import advance_from
- self._seed(flow=self._chain_flow(), advanced={"approverDedup": True})
- inst, ver = self._load()
- with patch(
- "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
- ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
- advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
- self._approve(self._pending("A"))
- self._approve(self._pending("B"))
- by_node = {t.node_key: t for t in self._tasks()}
- self.assertEqual(by_node["a1"].status, "approved")
- self.assertEqual(by_node["a1"].comment_text, "同意")
- self.assertEqual(by_node["a3"].status, "approved")
- self.assertEqual(by_node["a3"].comment_text, "审批人去重,系统自动通过")
- self.assertEqual(by_node["a4"].status, "approved")
- self.assertEqual(by_node["a4"].comment_text, "审批人去重,系统自动通过")
- self.assertEqual(inst.status, "completed")
- self.assertFalse(any(t.status == "pending" for t in by_node.values()))
- def test_switch_off_keeps_repeat_approver_pending(self):
- from app.services.oa_engine import advance_from
- self._seed(flow=self._chain_flow(), advanced={"approverDedup": False})
- inst, ver = self._load()
- with patch(
- "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
- ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
- advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
- self._approve(self._pending("A"))
- self._approve(self._pending("B"))
- step3 = next(t for t in self._tasks() if t.node_key == "a3")
- self.assertEqual(step3.assignee_user_id, "A")
- self.assertEqual(step3.status, "pending")
- self.assertEqual(inst.status, "running")
- self.assertFalse(any(t.node_key == "a4" for t in self._tasks()))
- def test_or_sign_auto_pass_closes_whole_node(self):
- from app.services.oa_engine import advance_from
- flow = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "第一步",
- "approverConfig": {
- "assigneeType": "users",
- "userIds": ["A"],
- "signMode": "or",
- },
- "child": {
- "id": "a2",
- "type": "approval",
- "name": "或签",
- "approverConfig": {
- "assigneeType": "users",
- "userIds": ["A", "C"],
- "signMode": "or",
- },
- "child": {"id": "e1", "type": "end"},
- },
- },
- }
- self._seed(flow=flow, advanced={"approverDedup": True})
- inst, ver = self._load()
- with patch(
- "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
- ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
- advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
- self._approve(self._pending("A"))
- by_key = {(t.node_key, t.assignee_user_id): t for t in self._tasks()}
- self.assertEqual(by_key[("a2", "A")].status, "approved")
- self.assertEqual(by_key[("a2", "A")].comment_text, "审批人去重,系统自动通过")
- self.assertEqual(by_key[("a2", "C")].status, "done")
- self.assertEqual(inst.status, "completed")
- def test_and_sign_only_auto_passes_repeat_person(self):
- from app.services.oa_engine import advance_from
- flow = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "第一步",
- "approverConfig": {
- "assigneeType": "users",
- "userIds": ["A"],
- "signMode": "or",
- },
- "child": {
- "id": "a2",
- "type": "approval",
- "name": "会签",
- "approverConfig": {
- "assigneeType": "users",
- "userIds": ["A", "C"],
- "signMode": "and",
- },
- "child": {"id": "e1", "type": "end"},
- },
- },
- }
- self._seed(flow=flow, advanced={"approverDedup": True})
- inst, ver = self._load()
- with patch(
- "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
- ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
- advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
- self._approve(self._pending("A"))
- by_user = {
- t.assignee_user_id: t for t in self._tasks() if t.node_key == "a2"
- }
- self.assertEqual(by_user["A"].status, "approved")
- self.assertEqual(by_user["A"].comment_text, "审批人去重,系统自动通过")
- self.assertEqual(by_user["C"].status, "pending")
- self.assertEqual(inst.status, "running")
- def test_non_approved_history_does_not_dedup(self):
- """仅统计已同意;拒绝等状态不触发后续自动通过。"""
- from app.services.oa_engine import advance_from
- flow = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "当前",
- "approverConfig": {
- "assigneeType": "users",
- "userIds": ["A"],
- "signMode": "or",
- },
- "child": {"id": "e1", "type": "end"},
- },
- }
- self._seed(flow=flow, advanced={"approverDedup": True})
- self.db.add(
- self.OaTask(
- id="old-reject",
- instance_id="i1",
- node_key="old",
- node_name="历史",
- task_type="approval",
- assignee_user_id="A",
- status="rejected",
- sign_mode="or",
- round_no=1,
- is_read=0,
- tenant_no="zkxw",
- order_num=0,
- create_dept_id="",
- create_time=self.now,
- update_time=self.now,
- del_flag=0,
- )
- )
- self.db.commit()
- inst, ver = self._load()
- with patch(
- "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
- ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
- advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
- current = next(t for t in self._tasks() if t.node_key == "a1")
- self.assertEqual(current.status, "pending")
- self.assertEqual(inst.status, "running")
|