test_oa_approver_dedup.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. """OA:审批人去重 — 同一实例内已同意的人,后续节点系统自动通过。"""
  2. import json
  3. import unittest
  4. from datetime import datetime
  5. from unittest.mock import patch
  6. from sqlalchemy import create_engine
  7. from sqlalchemy.orm import sessionmaker
  8. def _resolve_from_config(_db, *, assignee_config, **_kwargs):
  9. ids = list(assignee_config.get("userIds") or [])
  10. return ids, {"resolve_source": "users"}
  11. class TestApproverDedup(unittest.TestCase):
  12. def setUp(self):
  13. from app.models.models import OaAppVersion, OaInstance, OaTask
  14. self.engine = create_engine("sqlite:///:memory:")
  15. OaAppVersion.__table__.create(self.engine)
  16. OaInstance.__table__.create(self.engine)
  17. OaTask.__table__.create(self.engine)
  18. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  19. self.db = Session()
  20. self.OaAppVersion = OaAppVersion
  21. self.OaInstance = OaInstance
  22. self.OaTask = OaTask
  23. self.now = datetime.now()
  24. def tearDown(self):
  25. self.db.close()
  26. self.engine.dispose()
  27. def _seed(self, *, flow, advanced, initiator="u0"):
  28. self.db.add(
  29. self.OaAppVersion(
  30. id="v1",
  31. app_id="app1",
  32. version_no=1,
  33. status="published",
  34. is_latest_published="1",
  35. form_schema_json="{}",
  36. flow_tree_json=json.dumps(flow),
  37. advanced_settings_json=json.dumps(advanced),
  38. tenant_no="zkxw",
  39. create_dept_id="",
  40. create_time=self.now,
  41. update_time=self.now,
  42. del_flag=0,
  43. )
  44. )
  45. self.db.add(
  46. self.OaInstance(
  47. id="i1",
  48. app_id="app1",
  49. app_version_id="v1",
  50. title="t",
  51. initiator_user_id=initiator,
  52. initiator_dept_id="d1",
  53. form_data_json="{}",
  54. status="running",
  55. tenant_no="zkxw",
  56. create_dept_id="",
  57. create_time=self.now,
  58. update_time=self.now,
  59. del_flag=0,
  60. )
  61. )
  62. self.db.commit()
  63. def _load(self):
  64. inst = self.db.query(self.OaInstance).filter_by(id="i1").first()
  65. ver = self.db.query(self.OaAppVersion).filter_by(id="v1").first()
  66. return inst, ver
  67. def _tasks(self):
  68. self.db.flush()
  69. return self.db.query(self.OaTask).filter_by(instance_id="i1").all()
  70. def _pending(self, user_id):
  71. return next(
  72. t
  73. for t in self._tasks()
  74. if t.assignee_user_id == user_id and t.status == "pending"
  75. )
  76. def _approve(self, task):
  77. from app.services.oa_engine import _approve_task_core
  78. inst, ver = self._load()
  79. _approve_task_core(
  80. self.db,
  81. task=task,
  82. inst=inst,
  83. ver=ver,
  84. user_id=task.assignee_user_id,
  85. comment="同意",
  86. )
  87. def _chain_flow(self):
  88. """第 1 步 A,第 2 步 B,第 3、4 步再是 A。"""
  89. def approval(node_id, name, user_ids, child):
  90. return {
  91. "id": node_id,
  92. "type": "approval",
  93. "name": name,
  94. "approverConfig": {
  95. "assigneeType": "users",
  96. "userIds": user_ids,
  97. "signMode": "or",
  98. },
  99. "child": child,
  100. }
  101. return {
  102. "id": "s",
  103. "type": "start",
  104. "child": approval(
  105. "a1",
  106. "第一步",
  107. ["A"],
  108. approval(
  109. "a2",
  110. "第二步",
  111. ["B"],
  112. approval(
  113. "a3",
  114. "第三步",
  115. ["A"],
  116. approval("a4", "第四步", ["A"], {"id": "e1", "type": "end"}),
  117. ),
  118. ),
  119. ),
  120. }
  121. def test_later_nodes_auto_pass_after_same_person_approved(self):
  122. """A 在第 1 步同意后,第 3、4 步不再待办,系统记为自动通过。"""
  123. from app.services.oa_engine import advance_from
  124. self._seed(flow=self._chain_flow(), advanced={"approverDedup": True})
  125. inst, ver = self._load()
  126. with patch(
  127. "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
  128. ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
  129. advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
  130. self._approve(self._pending("A"))
  131. self._approve(self._pending("B"))
  132. by_node = {t.node_key: t for t in self._tasks()}
  133. self.assertEqual(by_node["a1"].status, "approved")
  134. self.assertEqual(by_node["a1"].comment_text, "同意")
  135. self.assertEqual(by_node["a3"].status, "approved")
  136. self.assertEqual(by_node["a3"].comment_text, "审批人去重,系统自动通过")
  137. self.assertEqual(by_node["a4"].status, "approved")
  138. self.assertEqual(by_node["a4"].comment_text, "审批人去重,系统自动通过")
  139. self.assertEqual(inst.status, "completed")
  140. self.assertFalse(any(t.status == "pending" for t in by_node.values()))
  141. def test_switch_off_keeps_repeat_approver_pending(self):
  142. from app.services.oa_engine import advance_from
  143. self._seed(flow=self._chain_flow(), advanced={"approverDedup": False})
  144. inst, ver = self._load()
  145. with patch(
  146. "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
  147. ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
  148. advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
  149. self._approve(self._pending("A"))
  150. self._approve(self._pending("B"))
  151. step3 = next(t for t in self._tasks() if t.node_key == "a3")
  152. self.assertEqual(step3.assignee_user_id, "A")
  153. self.assertEqual(step3.status, "pending")
  154. self.assertEqual(inst.status, "running")
  155. self.assertFalse(any(t.node_key == "a4" for t in self._tasks()))
  156. def test_or_sign_auto_pass_closes_whole_node(self):
  157. from app.services.oa_engine import advance_from
  158. flow = {
  159. "id": "s",
  160. "type": "start",
  161. "child": {
  162. "id": "a1",
  163. "type": "approval",
  164. "name": "第一步",
  165. "approverConfig": {
  166. "assigneeType": "users",
  167. "userIds": ["A"],
  168. "signMode": "or",
  169. },
  170. "child": {
  171. "id": "a2",
  172. "type": "approval",
  173. "name": "或签",
  174. "approverConfig": {
  175. "assigneeType": "users",
  176. "userIds": ["A", "C"],
  177. "signMode": "or",
  178. },
  179. "child": {"id": "e1", "type": "end"},
  180. },
  181. },
  182. }
  183. self._seed(flow=flow, advanced={"approverDedup": True})
  184. inst, ver = self._load()
  185. with patch(
  186. "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
  187. ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
  188. advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
  189. self._approve(self._pending("A"))
  190. by_key = {(t.node_key, t.assignee_user_id): t for t in self._tasks()}
  191. self.assertEqual(by_key[("a2", "A")].status, "approved")
  192. self.assertEqual(by_key[("a2", "A")].comment_text, "审批人去重,系统自动通过")
  193. self.assertEqual(by_key[("a2", "C")].status, "done")
  194. self.assertEqual(inst.status, "completed")
  195. def test_and_sign_only_auto_passes_repeat_person(self):
  196. from app.services.oa_engine import advance_from
  197. flow = {
  198. "id": "s",
  199. "type": "start",
  200. "child": {
  201. "id": "a1",
  202. "type": "approval",
  203. "name": "第一步",
  204. "approverConfig": {
  205. "assigneeType": "users",
  206. "userIds": ["A"],
  207. "signMode": "or",
  208. },
  209. "child": {
  210. "id": "a2",
  211. "type": "approval",
  212. "name": "会签",
  213. "approverConfig": {
  214. "assigneeType": "users",
  215. "userIds": ["A", "C"],
  216. "signMode": "and",
  217. },
  218. "child": {"id": "e1", "type": "end"},
  219. },
  220. },
  221. }
  222. self._seed(flow=flow, advanced={"approverDedup": True})
  223. inst, ver = self._load()
  224. with patch(
  225. "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
  226. ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
  227. advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
  228. self._approve(self._pending("A"))
  229. by_user = {
  230. t.assignee_user_id: t for t in self._tasks() if t.node_key == "a2"
  231. }
  232. self.assertEqual(by_user["A"].status, "approved")
  233. self.assertEqual(by_user["A"].comment_text, "审批人去重,系统自动通过")
  234. self.assertEqual(by_user["C"].status, "pending")
  235. self.assertEqual(inst.status, "running")
  236. def test_non_approved_history_does_not_dedup(self):
  237. """仅统计已同意;拒绝等状态不触发后续自动通过。"""
  238. from app.services.oa_engine import advance_from
  239. flow = {
  240. "id": "s",
  241. "type": "start",
  242. "child": {
  243. "id": "a1",
  244. "type": "approval",
  245. "name": "当前",
  246. "approverConfig": {
  247. "assigneeType": "users",
  248. "userIds": ["A"],
  249. "signMode": "or",
  250. },
  251. "child": {"id": "e1", "type": "end"},
  252. },
  253. }
  254. self._seed(flow=flow, advanced={"approverDedup": True})
  255. self.db.add(
  256. self.OaTask(
  257. id="old-reject",
  258. instance_id="i1",
  259. node_key="old",
  260. node_name="历史",
  261. task_type="approval",
  262. assignee_user_id="A",
  263. status="rejected",
  264. sign_mode="or",
  265. round_no=1,
  266. is_read=0,
  267. tenant_no="zkxw",
  268. order_num=0,
  269. create_dept_id="",
  270. create_time=self.now,
  271. update_time=self.now,
  272. del_flag=0,
  273. )
  274. )
  275. self.db.commit()
  276. inst, ver = self._load()
  277. with patch(
  278. "app.services.oa_engine.resolve_assignees", side_effect=_resolve_from_config
  279. ), patch("app.services.oa_engine._next_task_order_num", return_value=1):
  280. advance_from(self.db, inst=inst, ver=ver, from_node=None, user_id="u0")
  281. current = next(t for t in self._tasks() if t.node_key == "a1")
  282. self.assertEqual(current.status, "pending")
  283. self.assertEqual(inst.status, "running")