test_oa_engine.py 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430
  1. """OA 引擎单元测试(纯逻辑 + 轻量 mock)"""
  2. import json
  3. import unittest
  4. from datetime import datetime
  5. from unittest.mock import MagicMock, patch
  6. from app.services.oa_engine import (
  7. OaEngineError,
  8. _find_node,
  9. _flow_node_key_order,
  10. _match_condition_group,
  11. _sort_tasks_for_detail,
  12. advance_from,
  13. )
  14. class TestOaEngineHelpers(unittest.TestCase):
  15. def test_find_node(self):
  16. root = {
  17. "id": "s",
  18. "type": "start",
  19. "child": {
  20. "id": "a1",
  21. "type": "approval",
  22. "child": {"id": "e1", "type": "end"},
  23. },
  24. }
  25. self.assertEqual(_find_node(root, "a1")["id"], "a1")
  26. self.assertEqual(_find_node(root, "e1")["id"], "e1")
  27. self.assertIsNone(_find_node(root, "x"))
  28. def test_condition_match(self):
  29. groups = [
  30. {
  31. "name": "gt3",
  32. "when": "{days} > 3",
  33. "child": {"id": "n1", "type": "end"},
  34. },
  35. {"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
  36. ]
  37. g = _match_condition_group(groups, {"days": 5})
  38. self.assertEqual(g["name"], "gt3")
  39. g2 = _match_condition_group(groups, {"days": 1})
  40. self.assertEqual(g2["name"], "default")
  41. def test_condition_match_rules_list(self):
  42. groups = [
  43. {
  44. "name": "gt3",
  45. "rules": [{"field": "days", "op": ">", "value": 3}],
  46. "child": {"id": "n1", "type": "end"},
  47. },
  48. {"name": "default", "isDefault": True, "rules": [], "child": {"id": "n2", "type": "end"}},
  49. ]
  50. self.assertEqual(_match_condition_group(groups, {"days": 5})["name"], "gt3")
  51. self.assertEqual(_match_condition_group(groups, {"days": 1})["name"], "default")
  52. def test_condition_match_rules_and(self):
  53. groups = [
  54. {
  55. "name": "both",
  56. "rules": [
  57. {"field": "days", "op": ">", "value": 3},
  58. {"field": "leaveType", "op": "==", "value": "事假"},
  59. ],
  60. "child": {"id": "n1", "type": "end"},
  61. },
  62. {"name": "default", "rules": [], "isDefault": True, "child": {"id": "n2", "type": "end"}},
  63. ]
  64. self.assertEqual(
  65. _match_condition_group(groups, {"days": 5, "leaveType": "事假"})["name"],
  66. "both",
  67. )
  68. self.assertEqual(
  69. _match_condition_group(groups, {"days": 5, "leaveType": "病假"})["name"],
  70. "default",
  71. )
  72. def test_flow_node_key_order_cc_before_approval(self):
  73. root = {
  74. "id": "s",
  75. "type": "start",
  76. "child": {
  77. "id": "c1",
  78. "type": "cc",
  79. "child": {
  80. "id": "a1",
  81. "type": "approval",
  82. "child": {"id": "e1", "type": "end"},
  83. },
  84. },
  85. }
  86. order = _flow_node_key_order(root)
  87. self.assertLess(order["c1"], order["a1"])
  88. def test_sort_tasks_by_flow_when_same_create_time(self):
  89. """抄送先于审批设计时,即使 create_time / order_num 相同也应按树序展示。"""
  90. root = {
  91. "id": "s",
  92. "type": "start",
  93. "child": {
  94. "id": "c1",
  95. "type": "cc",
  96. "child": {
  97. "id": "a1",
  98. "type": "approval",
  99. "child": {"id": "e1", "type": "end"},
  100. },
  101. },
  102. }
  103. ts = datetime(2026, 7, 15, 14, 5, 7)
  104. approval = MagicMock(node_key="a1", order_num=0, create_time=ts, id="AAAA")
  105. cc = MagicMock(node_key="c1", order_num=0, create_time=ts, id="BBBB")
  106. # 故意把审批放在列表前面,模拟 create_time 同秒时 PK 乱序
  107. sorted_tasks = _sort_tasks_for_detail([approval, cc], flow_root=root)
  108. self.assertEqual([t.node_key for t in sorted_tasks], ["c1", "a1"])
  109. class TestAdvanceFrom(unittest.TestCase):
  110. def test_linear_creates_approval_then_stops(self):
  111. flow = {
  112. "id": "s",
  113. "type": "start",
  114. "child": {
  115. "id": "a1",
  116. "type": "approval",
  117. "name": "负责人",
  118. "approverConfig": {"assigneeType": "dept_leader"},
  119. "child": {"id": "e1", "type": "end"},
  120. },
  121. }
  122. db = MagicMock()
  123. inst = MagicMock(
  124. id="inst1",
  125. tenant_no="zkxw",
  126. initiator_user_id="u0",
  127. initiator_dept_id="d1",
  128. form_data_json="{}",
  129. status="running",
  130. )
  131. ver = MagicMock(
  132. flow_tree_json=json.dumps(flow),
  133. advanced_settings_json=json.dumps({"supervisorFallbackToDeptLeader": True}),
  134. )
  135. fake_task = MagicMock()
  136. with patch(
  137. "app.services.oa_engine._next_task_order_num", return_value=1
  138. ), patch(
  139. "app.services.oa_engine._create_tasks_for_node",
  140. return_value=[fake_task],
  141. ) as mock_create:
  142. advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
  143. mock_create.assert_called_once()
  144. self.assertEqual(inst.status, "running")
  145. self.assertEqual(mock_create.call_args.kwargs.get("order_num_start"), 1)
  146. def test_complete_path_to_end_via_cc(self):
  147. flow = {
  148. "id": "s",
  149. "type": "start",
  150. "child": {
  151. "id": "c1",
  152. "type": "cc",
  153. "name": "抄送",
  154. "approverConfig": {"assigneeType": "users", "userIds": ["u9"]},
  155. "child": {"id": "e1", "type": "end"},
  156. },
  157. }
  158. db = MagicMock()
  159. inst = MagicMock(
  160. id="inst1",
  161. tenant_no="zkxw",
  162. initiator_user_id="u0",
  163. initiator_dept_id="d1",
  164. form_data_json="{}",
  165. status="running",
  166. update_time=datetime.now(),
  167. )
  168. ver = MagicMock(
  169. flow_tree_json=json.dumps(flow),
  170. advanced_settings_json="{}",
  171. )
  172. with patch(
  173. "app.services.oa_engine._next_task_order_num", return_value=1
  174. ), patch(
  175. "app.services.oa_engine._create_tasks_for_node",
  176. return_value=[MagicMock()],
  177. ):
  178. advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
  179. self.assertEqual(inst.status, "completed")
  180. def test_empty_assignee_raises(self):
  181. flow = {
  182. "id": "s",
  183. "type": "start",
  184. "child": {
  185. "id": "a1",
  186. "type": "approval",
  187. "approverConfig": {"assigneeType": "dept_leader"},
  188. "child": {"id": "e1", "type": "end"},
  189. },
  190. }
  191. db = MagicMock()
  192. inst = MagicMock(
  193. id="inst1",
  194. tenant_no="zkxw",
  195. initiator_user_id="u0",
  196. initiator_dept_id="d1",
  197. form_data_json="{}",
  198. status="running",
  199. )
  200. ver = MagicMock(flow_tree_json=json.dumps(flow), advanced_settings_json="{}")
  201. with patch(
  202. "app.services.oa_engine._next_task_order_num", return_value=1
  203. ), patch("app.services.oa_engine._create_tasks_for_node", return_value=[]):
  204. with self.assertRaises(OaEngineError):
  205. advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
  206. class TestAddSign(unittest.TestCase):
  207. def test_add_sign_rejects_when_disabled(self):
  208. from app.services.oa_engine import add_sign
  209. db = MagicMock()
  210. task = MagicMock(
  211. id="t1",
  212. assignee_user_id="u1",
  213. task_type="approval",
  214. status="pending",
  215. instance_id="i1",
  216. node_key="a1",
  217. node_name="审批",
  218. sign_mode="or",
  219. comment_text=None,
  220. )
  221. inst = MagicMock(id="i1", status="running", initiator_dept_id="d1")
  222. ver = MagicMock(
  223. id="v1",
  224. advanced_settings_json=json.dumps({"allowAddSign": False}),
  225. del_flag=0,
  226. )
  227. db.query.return_value.filter.return_value.first.side_effect = [task, inst, ver]
  228. with self.assertRaises(OaEngineError) as ctx:
  229. add_sign(
  230. db,
  231. "t1",
  232. tenant_no="zkxw",
  233. user_id="u1",
  234. user_ids=["u2"],
  235. )
  236. self.assertIn("不允许加签", str(ctx.exception))
  237. def test_add_sign_requires_users(self):
  238. from app.services.oa_engine import add_sign
  239. db = MagicMock()
  240. task = MagicMock(
  241. id="t1",
  242. assignee_user_id="u1",
  243. task_type="approval",
  244. status="pending",
  245. instance_id="i1",
  246. )
  247. inst = MagicMock(id="i1", status="running")
  248. ver = MagicMock(advanced_settings_json="{}", del_flag=0)
  249. db.query.return_value.filter.return_value.first.side_effect = [task, inst, ver]
  250. with self.assertRaises(OaEngineError) as ctx:
  251. add_sign(db, "t1", tenant_no="zkxw", user_id="u1", user_ids=[])
  252. self.assertIn("加签人", str(ctx.exception))
  253. class TestCompleteTaskOrSign(unittest.TestCase):
  254. """回归:Session autoflush=False 时,或签取消同节点待办不可覆盖本人已同意状态。"""
  255. def setUp(self):
  256. from sqlalchemy import create_engine
  257. from sqlalchemy.orm import sessionmaker
  258. from app.core.database import Base
  259. from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
  260. self.engine = create_engine("sqlite:///:memory:")
  261. # 仅建引擎相关表
  262. OaApp.__table__.create(self.engine)
  263. OaAppVersion.__table__.create(self.engine)
  264. OaInstance.__table__.create(self.engine)
  265. OaTask.__table__.create(self.engine)
  266. OaInstanceEvent.__table__.create(self.engine)
  267. SysUser.__table__.create(self.engine)
  268. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  269. self.db = Session()
  270. self.Base = Base
  271. self.OaAppVersion = OaAppVersion
  272. self.OaInstance = OaInstance
  273. self.OaTask = OaTask
  274. flow = {
  275. "id": "s",
  276. "type": "start",
  277. "child": {
  278. "id": "a1",
  279. "type": "approval",
  280. "name": "审批人",
  281. "approverConfig": {"signMode": "or"},
  282. "child": {"id": "e1", "type": "end"},
  283. },
  284. }
  285. now = datetime.now()
  286. self.db.add(
  287. OaAppVersion(
  288. id="v1",
  289. app_id="app1",
  290. version_no=1,
  291. status="published",
  292. is_latest_published="1",
  293. form_schema_json="{}",
  294. flow_tree_json=json.dumps(flow),
  295. advanced_settings_json="{}",
  296. tenant_no="zkxw",
  297. create_dept_id="",
  298. create_time=now,
  299. update_time=now,
  300. del_flag=0,
  301. )
  302. )
  303. self.db.add(
  304. OaInstance(
  305. id="i1",
  306. app_id="app1",
  307. app_version_id="v1",
  308. title="单人审批",
  309. initiator_user_id="u0",
  310. initiator_dept_id="d1",
  311. form_data_json="{}",
  312. status="running",
  313. tenant_no="zkxw",
  314. create_dept_id="",
  315. create_time=now,
  316. update_time=now,
  317. del_flag=0,
  318. )
  319. )
  320. self.db.add(
  321. OaTask(
  322. id="t1",
  323. instance_id="i1",
  324. node_key="a1",
  325. node_name="审批人",
  326. task_type="approval",
  327. assignee_user_id="u1",
  328. status="pending",
  329. sign_mode="or",
  330. tenant_no="zkxw",
  331. create_dept_id="",
  332. create_time=now,
  333. update_time=now,
  334. del_flag=0,
  335. )
  336. )
  337. self.db.commit()
  338. def tearDown(self):
  339. self.db.close()
  340. self.engine.dispose()
  341. def test_approve_keeps_approved_not_done(self):
  342. from app.services.oa_engine import complete_task
  343. with patch(
  344. "app.services.oa_engine.get_instance_detail",
  345. return_value={"id": "i1", "status": "completed"},
  346. ):
  347. complete_task(
  348. self.db,
  349. "t1",
  350. tenant_no="zkxw",
  351. user_id="u1",
  352. action="approve",
  353. comment="同意",
  354. )
  355. task = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
  356. self.assertEqual(task.status, "approved")
  357. self.assertEqual(task.comment_text, "同意")
  358. # 已处理列表应能查到
  359. from app.services.oa_engine import list_tasks
  360. done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
  361. self.assertEqual(len(done), 1)
  362. self.assertEqual(done[0]["status"], "approved")
  363. def test_or_sign_cancels_siblings_only(self):
  364. from app.services.oa_engine import complete_task
  365. now = datetime.now()
  366. self.db.add(
  367. self.OaTask(
  368. id="t2",
  369. instance_id="i1",
  370. node_key="a1",
  371. node_name="审批人",
  372. task_type="approval",
  373. assignee_user_id="u2",
  374. status="pending",
  375. sign_mode="or",
  376. tenant_no="zkxw",
  377. create_dept_id="",
  378. create_time=now,
  379. update_time=now,
  380. del_flag=0,
  381. )
  382. )
  383. self.db.commit()
  384. with patch(
  385. "app.services.oa_engine.get_instance_detail",
  386. return_value={"id": "i1", "status": "completed"},
  387. ):
  388. complete_task(
  389. self.db,
  390. "t1",
  391. tenant_no="zkxw",
  392. user_id="u1",
  393. action="approve",
  394. comment="同意",
  395. )
  396. t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
  397. t2 = self.db.query(self.OaTask).filter(self.OaTask.id == "t2").first()
  398. self.assertEqual(t1.status, "approved")
  399. self.assertEqual(t2.status, "done")
  400. class TestBuildProgress(unittest.TestCase):
  401. def test_cc_before_approval_and_pending_ahead(self):
  402. from app.services.oa_engine import build_progress_items
  403. root = {
  404. "id": "s",
  405. "type": "start",
  406. "child": {
  407. "id": "c1",
  408. "type": "cc",
  409. "name": "抄送人",
  410. "child": {
  411. "id": "a1",
  412. "type": "approval",
  413. "name": "审批人",
  414. "child": {
  415. "id": "a2",
  416. "type": "approval",
  417. "name": "总经理",
  418. "child": {"id": "e1", "type": "end"},
  419. },
  420. },
  421. },
  422. }
  423. ts = datetime(2026, 7, 15, 14, 5, 7)
  424. # 审批任务 create 同秒但列表故意审批在前
  425. t_approval = MagicMock(
  426. id="tA",
  427. node_key="a1",
  428. node_name="审批人",
  429. task_type="approval",
  430. status="pending",
  431. assignee_user_id="u2",
  432. order_num=0,
  433. round_no=1,
  434. create_time=ts,
  435. update_time=ts,
  436. comment_text=None,
  437. resolve_source="users",
  438. )
  439. t_cc = MagicMock(
  440. id="tC",
  441. node_key="c1",
  442. node_name="抄送人",
  443. task_type="cc",
  444. status="done",
  445. assignee_user_id="u9",
  446. order_num=0,
  447. round_no=1,
  448. create_time=ts,
  449. update_time=ts,
  450. comment_text=None,
  451. resolve_source="users",
  452. )
  453. inst = MagicMock(
  454. status="running",
  455. initiator_user_id="u0",
  456. current_round=1,
  457. create_time=ts,
  458. update_time=ts,
  459. )
  460. items = build_progress_items(
  461. inst=inst,
  462. flow_root=root,
  463. tasks=[t_approval, t_cc],
  464. name_map={"u0": "发起人甲", "u2": "审批乙", "u9": "抄送丙"},
  465. events=[], # P0 可空
  466. )
  467. kinds = [x["kind"] for x in items]
  468. self.assertEqual(kinds[0], "submit")
  469. # 已产生:cc 再 approval
  470. self.assertEqual(
  471. [x["nodeKey"] for x in items if x["kind"] == "task"],
  472. ["c1", "a1"],
  473. )
  474. # running 时未到达 a2
  475. pending = [x for x in items if x["kind"] == "pending_node"]
  476. self.assertTrue(any(p["nodeKey"] == "a2" for p in pending))
  477. def test_running_shows_all_subsequent_pending_nodes(self):
  478. """进行中应挂出路径上全部后续节点,而非只露一个空壳。"""
  479. from app.services.oa_engine import build_progress_items
  480. root = {
  481. "id": "s",
  482. "type": "start",
  483. "child": {
  484. "id": "a1",
  485. "type": "approval",
  486. "name": "一级审批",
  487. "child": {
  488. "id": "a2",
  489. "type": "approval",
  490. "name": "二级审批",
  491. "child": {
  492. "id": "a3",
  493. "type": "approval",
  494. "name": "三级审批",
  495. "child": {"id": "e1", "type": "end"},
  496. },
  497. },
  498. },
  499. }
  500. ts = datetime(2026, 7, 17, 11, 0, 0)
  501. t1 = MagicMock(
  502. id="t1",
  503. node_key="a1",
  504. node_name="一级审批",
  505. task_type="approval",
  506. status="pending",
  507. assignee_user_id="u1",
  508. order_num=0,
  509. round_no=1,
  510. create_time=ts,
  511. update_time=ts,
  512. comment_text=None,
  513. resolve_source="users",
  514. sign_mode="or",
  515. )
  516. inst = MagicMock(
  517. status="running",
  518. initiator_user_id="u0",
  519. current_round=1,
  520. create_time=ts,
  521. update_time=ts,
  522. form_data_json="{}",
  523. tenant_no="t1",
  524. initiator_dept_id="",
  525. )
  526. items = build_progress_items(
  527. inst=inst,
  528. flow_root=root,
  529. tasks=[t1],
  530. name_map={"u0": "发起人", "u1": "甲"},
  531. events=[],
  532. )
  533. pending = [x for x in items if x["kind"] == "pending_node"]
  534. self.assertEqual([p["nodeKey"] for p in pending], ["a2", "a3", "e1"])
  535. self.assertEqual([p["title"] for p in pending], ["二级审批", "三级审批", "结束"])
  536. self.assertEqual(pending[-1]["taskType"], "end")
  537. def test_or_sign_same_node_merged_into_one_card(self):
  538. """或签同节点三人只投影一张进度卡片,people 含各人状态。"""
  539. from app.services.oa_engine import build_progress_items
  540. root = {
  541. "id": "s",
  542. "type": "start",
  543. "child": {
  544. "id": "a1",
  545. "type": "approval",
  546. "name": "部门审批",
  547. "child": {"id": "e1", "type": "end"},
  548. },
  549. }
  550. ts = datetime(2026, 7, 17, 10, 0, 0)
  551. ts2 = datetime(2026, 7, 17, 10, 5, 0)
  552. def mk(tid, uid, status, comment=None, update=None):
  553. return MagicMock(
  554. id=tid,
  555. node_key="a1",
  556. node_name="部门审批",
  557. task_type="approval",
  558. status=status,
  559. assignee_user_id=uid,
  560. order_num=0,
  561. round_no=1,
  562. create_time=ts,
  563. update_time=update or ts,
  564. comment_text=comment,
  565. resolve_source="users",
  566. sign_mode="or",
  567. )
  568. tasks = [
  569. mk("t1", "u1", "approved", "同意", ts2),
  570. mk("t2", "u2", "done"),
  571. mk("t3", "u3", "done"),
  572. ]
  573. inst = MagicMock(
  574. status="completed",
  575. initiator_user_id="u0",
  576. current_round=1,
  577. create_time=ts,
  578. update_time=ts2,
  579. )
  580. items = build_progress_items(
  581. inst=inst,
  582. flow_root=root,
  583. tasks=tasks,
  584. name_map={"u0": "发起人", "u1": "甲", "u2": "乙", "u3": "丙"},
  585. events=[],
  586. )
  587. task_items = [x for x in items if x["kind"] == "task"]
  588. self.assertEqual(len(task_items), 1)
  589. card = task_items[0]
  590. self.assertEqual(card["nodeKey"], "a1")
  591. self.assertEqual(card["status"], "approved")
  592. self.assertEqual(card["signMode"], "or")
  593. self.assertEqual(len(card["people"]), 3)
  594. self.assertEqual(
  595. [(p["nickName"], p["status"]) for p in card["people"]],
  596. [("甲", "approved"), ("乙", "done"), ("丙", "done")],
  597. )
  598. self.assertIn("甲:同意", card["commentText"])
  599. def test_terminal_status_excludes_pending_nodes(self):
  600. from app.services.oa_engine import build_progress_items
  601. root = {
  602. "id": "s",
  603. "type": "start",
  604. "child": {
  605. "id": "a1",
  606. "type": "approval",
  607. "name": "审批人",
  608. "child": {
  609. "id": "a2",
  610. "type": "approval",
  611. "name": "总经理",
  612. "child": {"id": "e1", "type": "end"},
  613. },
  614. },
  615. }
  616. ts = datetime(2026, 7, 15, 14, 5, 7)
  617. t_approval = MagicMock(
  618. id="tA",
  619. node_key="a1",
  620. node_name="审批人",
  621. task_type="approval",
  622. status="rejected",
  623. assignee_user_id="u2",
  624. order_num=0,
  625. round_no=1,
  626. create_time=ts,
  627. update_time=ts,
  628. comment_text="不同意",
  629. resolve_source="users",
  630. )
  631. for status in ("rejected", "completed", "cancelled", "returned"):
  632. inst = MagicMock(
  633. status=status,
  634. initiator_user_id="u0",
  635. current_round=1,
  636. create_time=ts,
  637. update_time=ts,
  638. )
  639. items = build_progress_items(
  640. inst=inst,
  641. flow_root=root,
  642. tasks=[t_approval],
  643. name_map={"u0": "发起人甲", "u2": "审批乙"},
  644. events=[],
  645. )
  646. pending = [x for x in items if x["kind"] == "pending_node"]
  647. self.assertEqual(pending, [], f"status={status} should not have pending_node")
  648. terminal = [x for x in items if x["kind"] == "terminal"]
  649. # 打回仅展示审批人节点「已打回」,不追加独立终态行
  650. expect_n = 0 if status == "returned" else 1
  651. self.assertEqual(
  652. len(terminal), expect_n, f"status={status} terminal count"
  653. )
  654. def test_progress_returned_has_no_kind_return_node(self):
  655. from app.services.oa_engine import build_progress_items
  656. root = {
  657. "id": "s",
  658. "type": "start",
  659. "child": {
  660. "id": "a1",
  661. "type": "approval",
  662. "name": "审批人",
  663. "child": {"id": "e1", "type": "end"},
  664. },
  665. }
  666. ts = datetime(2026, 7, 15, 14, 58, 8)
  667. t_approval = MagicMock(
  668. id="tA",
  669. node_key="a1",
  670. node_name="审批人",
  671. task_type="approval",
  672. status="returned",
  673. assignee_user_id="u2",
  674. order_num=1,
  675. round_no=1,
  676. create_time=ts,
  677. update_time=ts,
  678. comment_text="打回",
  679. resolve_source="users",
  680. )
  681. ret_ev = MagicMock(
  682. event_type="return",
  683. round_no=1,
  684. actor_user_id="u2",
  685. create_time=ts,
  686. comment_text="打回",
  687. node_key="a1",
  688. task_id="tA",
  689. )
  690. inst = MagicMock(
  691. status="returned",
  692. initiator_user_id="u0",
  693. current_round=1,
  694. create_time=ts,
  695. update_time=ts,
  696. )
  697. items = build_progress_items(
  698. inst=inst,
  699. flow_root=root,
  700. tasks=[t_approval],
  701. name_map={"u0": "发起人", "u2": "审批人"},
  702. events=[ret_ev],
  703. )
  704. self.assertFalse(any(x["kind"] == "return" for x in items))
  705. tasks = [x for x in items if x["kind"] == "task"]
  706. self.assertEqual(len(tasks), 1)
  707. self.assertEqual(tasks[0]["status"], "returned")
  708. self.assertEqual(tasks[0]["commentText"], "打回")
  709. terminals = [x for x in items if x["kind"] == "terminal"]
  710. self.assertEqual(terminals, [], "打回不追加独立终态「已打回」行")
  711. class TestReturnAndResubmit(unittest.TestCase):
  712. def setUp(self):
  713. from sqlalchemy import create_engine
  714. from sqlalchemy.orm import sessionmaker
  715. from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
  716. self.engine = create_engine("sqlite:///:memory:")
  717. OaApp.__table__.create(self.engine)
  718. OaAppVersion.__table__.create(self.engine)
  719. OaInstance.__table__.create(self.engine)
  720. OaTask.__table__.create(self.engine)
  721. OaInstanceEvent.__table__.create(self.engine)
  722. SysUser.__table__.create(self.engine)
  723. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  724. self.db = Session()
  725. self.OaInstance = OaInstance
  726. self.OaTask = OaTask
  727. self.OaInstanceEvent = OaInstanceEvent
  728. flow = {
  729. "id": "s",
  730. "type": "start",
  731. "child": {
  732. "id": "a1",
  733. "type": "approval",
  734. "name": "审批人",
  735. "approverConfig": {
  736. "assigneeType": "users",
  737. "userIds": ["u1"],
  738. "signMode": "or",
  739. },
  740. "child": {"id": "e1", "type": "end"},
  741. },
  742. }
  743. now = datetime.now()
  744. self.db.add(
  745. OaAppVersion(
  746. id="v1",
  747. app_id="app1",
  748. version_no=1,
  749. status="published",
  750. is_latest_published="1",
  751. form_schema_json="{}",
  752. flow_tree_json=json.dumps(flow),
  753. advanced_settings_json=json.dumps({"allowReturn": True}),
  754. tenant_no="zkxw",
  755. create_dept_id="",
  756. create_time=now,
  757. update_time=now,
  758. del_flag=0,
  759. )
  760. )
  761. self.db.add(
  762. OaInstance(
  763. id="i1",
  764. app_id="app1",
  765. app_version_id="v1",
  766. title="打回测",
  767. initiator_user_id="u0",
  768. initiator_dept_id="d1",
  769. form_data_json="{}",
  770. status="running",
  771. current_round=1,
  772. tenant_no="zkxw",
  773. create_dept_id="",
  774. create_time=now,
  775. update_time=now,
  776. del_flag=0,
  777. )
  778. )
  779. self.db.add(
  780. OaTask(
  781. id="t1",
  782. instance_id="i1",
  783. node_key="a1",
  784. node_name="审批人",
  785. task_type="approval",
  786. assignee_user_id="u1",
  787. status="pending",
  788. sign_mode="or",
  789. round_no=1,
  790. tenant_no="zkxw",
  791. create_dept_id="",
  792. create_time=now,
  793. update_time=now,
  794. del_flag=0,
  795. )
  796. )
  797. self.db.commit()
  798. def tearDown(self):
  799. self.db.close()
  800. self.engine.dispose()
  801. def test_return_sets_returned_and_event(self):
  802. from app.services.oa_engine import return_task
  803. with patch(
  804. "app.services.oa_engine.get_instance_detail",
  805. return_value={"id": "i1", "status": "returned"},
  806. ):
  807. return_task(
  808. self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
  809. )
  810. inst = self.db.query(self.OaInstance).filter_by(id="i1").first()
  811. task = self.db.query(self.OaTask).filter_by(id="t1").first()
  812. ev = (
  813. self.db.query(self.OaInstanceEvent)
  814. .filter_by(instance_id="i1", event_type="return")
  815. .first()
  816. )
  817. self.assertEqual(inst.status, "returned")
  818. self.assertEqual(task.status, "returned")
  819. self.assertIsNotNone(ev)
  820. self.assertEqual(ev.comment_text, "请补充材料")
  821. def test_resubmit_raises_deprecated(self):
  822. from app.services.oa_engine import OaEngineError, resubmit_instance, return_task
  823. with patch(
  824. "app.services.oa_engine.get_instance_detail",
  825. return_value={"id": "i1", "status": "returned"},
  826. ):
  827. return_task(
  828. self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
  829. )
  830. with self.assertRaises(OaEngineError) as ctx:
  831. resubmit_instance(
  832. self.db,
  833. "i1",
  834. tenant_no="zkxw",
  835. user_id="u0",
  836. form_data={"x": 1},
  837. )
  838. self.assertIn("废弃", str(ctx.exception))
  839. def test_done_list_includes_returned(self):
  840. from app.services.oa_engine import list_tasks, return_task
  841. with patch(
  842. "app.services.oa_engine.get_instance_detail",
  843. return_value={"id": "i1", "status": "returned"},
  844. ):
  845. return_task(
  846. self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
  847. )
  848. done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
  849. self.assertEqual(len(done), 1)
  850. self.assertEqual(done[0]["status"], "returned")
  851. class TestListTasksEnrichment(unittest.TestCase):
  852. """list_tasks 返回整单此刻进度摘要字段。"""
  853. def setUp(self):
  854. from sqlalchemy import create_engine
  855. from sqlalchemy.orm import sessionmaker
  856. from app.models.models import OaApp, OaAppVersion, OaInstance, OaTask, SysUser
  857. self.engine = create_engine("sqlite:///:memory:")
  858. OaApp.__table__.create(self.engine)
  859. OaAppVersion.__table__.create(self.engine)
  860. OaInstance.__table__.create(self.engine)
  861. OaTask.__table__.create(self.engine)
  862. SysUser.__table__.create(self.engine)
  863. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  864. self.db = Session()
  865. self.OaTask = OaTask
  866. now = datetime(2026, 7, 15, 10, 0, 0)
  867. self.db.add(
  868. OaApp(
  869. id="app1",
  870. app_name="请假审批",
  871. initiator_scope="all",
  872. admin_scope="all_oa_admin",
  873. status="1",
  874. tenant_no="zkxw",
  875. create_dept_id="",
  876. create_time=now,
  877. update_time=now,
  878. del_flag=0,
  879. )
  880. )
  881. self.db.add(
  882. OaAppVersion(
  883. id="v1",
  884. app_id="app1",
  885. version_no=1,
  886. status="published",
  887. is_latest_published="1",
  888. form_schema_json="{}",
  889. flow_tree_json="{}",
  890. advanced_settings_json="{}",
  891. tenant_no="zkxw",
  892. create_dept_id="",
  893. create_time=now,
  894. update_time=now,
  895. del_flag=0,
  896. )
  897. )
  898. self.db.add(
  899. SysUser(
  900. id="u0",
  901. user_no="initiator",
  902. nick_name="发起人甲",
  903. password="x",
  904. tenant_no="zkxw",
  905. create_dept_id="",
  906. create_time=now,
  907. update_time=now,
  908. del_flag=0,
  909. )
  910. )
  911. self.db.add(
  912. SysUser(
  913. id="u1",
  914. user_no="approver1",
  915. nick_name="审批人乙",
  916. password="x",
  917. tenant_no="zkxw",
  918. create_dept_id="",
  919. create_time=now,
  920. update_time=now,
  921. del_flag=0,
  922. )
  923. )
  924. self.db.add(
  925. SysUser(
  926. id="u2",
  927. user_no="approver2",
  928. nick_name="审批人丙",
  929. password="x",
  930. tenant_no="zkxw",
  931. create_dept_id="",
  932. create_time=now,
  933. update_time=now,
  934. del_flag=0,
  935. )
  936. )
  937. self.db.add(
  938. OaInstance(
  939. id="i1",
  940. app_id="app1",
  941. app_version_id="v1",
  942. title="请假-忽略",
  943. initiator_user_id="u0",
  944. initiator_dept_id="d1",
  945. form_data_json="{}",
  946. status="running",
  947. tenant_no="zkxw",
  948. create_dept_id="",
  949. create_time=now,
  950. update_time=now,
  951. del_flag=0,
  952. )
  953. )
  954. self.db.add(
  955. OaTask(
  956. id="t-done",
  957. instance_id="i1",
  958. node_key="a1",
  959. node_name="一级审批",
  960. task_type="approval",
  961. assignee_user_id="u1",
  962. status="approved",
  963. tenant_no="zkxw",
  964. create_dept_id="",
  965. create_time=now,
  966. update_time=now,
  967. del_flag=0,
  968. )
  969. )
  970. self.db.add(
  971. OaTask(
  972. id="t-pending",
  973. instance_id="i1",
  974. node_key="a2",
  975. node_name="二级审批",
  976. task_type="approval",
  977. assignee_user_id="u2",
  978. status="pending",
  979. tenant_no="zkxw",
  980. create_dept_id="",
  981. create_time=now,
  982. update_time=now,
  983. del_flag=0,
  984. )
  985. )
  986. self.db.commit()
  987. def tearDown(self):
  988. self.db.close()
  989. self.engine.dispose()
  990. def test_done_list_shows_instance_current_progress(self):
  991. from app.services.oa_engine import list_tasks
  992. done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
  993. self.assertEqual(len(done), 1)
  994. row = done[0]
  995. self.assertEqual(row["appName"], "请假审批")
  996. self.assertEqual(row["initiatorUserName"], "发起人甲")
  997. self.assertEqual(row["applyTime"], "2026-07-15 10:00:00")
  998. self.assertEqual(row["instanceStatus"], "running")
  999. self.assertEqual(row["currentNodeName"], "二级审批")
  1000. self.assertEqual(row["currentApproverNames"], "审批人丙")
  1001. self.assertEqual(row["status"], "approved")
  1002. def test_mine_list_enrichment(self):
  1003. from app.services.oa_engine import list_tasks
  1004. mine = list_tasks(self.db, tenant_no="zkxw", user_id="u0", kind="mine")
  1005. self.assertEqual(len(mine), 1)
  1006. row = mine[0]
  1007. self.assertEqual(row["appName"], "请假审批")
  1008. self.assertEqual(row["instanceStatus"], "running")
  1009. self.assertEqual(row["currentNodeName"], "二级审批")
  1010. self.assertEqual(row["currentApproverNames"], "审批人丙")
  1011. def test_pending_list_enrichment(self):
  1012. from app.services.oa_engine import list_tasks
  1013. pending = list_tasks(self.db, tenant_no="zkxw", user_id="u2", kind="pending")
  1014. self.assertEqual(len(pending), 1)
  1015. row = pending[0]
  1016. self.assertEqual(row["appName"], "请假审批")
  1017. self.assertEqual(row["currentNodeName"], "二级审批")
  1018. def test_terminal_instance_labels(self):
  1019. from app.services.oa_engine import list_tasks
  1020. from app.models.models import OaInstance
  1021. cases = [
  1022. ("completed", "已完成"),
  1023. ("rejected", "已拒绝"),
  1024. ("returned", "已打回"),
  1025. ("cancelled", "已撤销"),
  1026. ]
  1027. inst = self.db.query(OaInstance).filter_by(id="i1").first()
  1028. pending = self.db.query(self.OaTask).filter_by(id="t-pending").first()
  1029. for status, expected_label in cases:
  1030. with self.subTest(status=status):
  1031. inst.status = status
  1032. pending.status = "cancelled"
  1033. self.db.commit()
  1034. mine = list_tasks(self.db, tenant_no="zkxw", user_id="u0", kind="mine")
  1035. row = mine[0]
  1036. self.assertEqual(row["instanceStatus"], status)
  1037. self.assertEqual(row["currentNodeName"], expected_label)
  1038. self.assertEqual(row["currentApproverNames"], "-")
  1039. inst.status = "running"
  1040. pending.status = "pending"
  1041. self.db.commit()
  1042. class TestTransferTask(unittest.TestCase):
  1043. def setUp(self):
  1044. from sqlalchemy import create_engine
  1045. from sqlalchemy.orm import sessionmaker
  1046. from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
  1047. self.engine = create_engine("sqlite:///:memory:")
  1048. OaApp.__table__.create(self.engine)
  1049. OaAppVersion.__table__.create(self.engine)
  1050. OaInstance.__table__.create(self.engine)
  1051. OaTask.__table__.create(self.engine)
  1052. OaInstanceEvent.__table__.create(self.engine)
  1053. SysUser.__table__.create(self.engine)
  1054. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  1055. self.db = Session()
  1056. self.OaTask = OaTask
  1057. self.OaInstance = OaInstance
  1058. self.OaAppVersion = OaAppVersion
  1059. self.SysUser = SysUser
  1060. now = datetime.now()
  1061. self.db.add(
  1062. OaApp(
  1063. id="app1",
  1064. group_id="",
  1065. app_name="测试应用",
  1066. icon_url="",
  1067. description="",
  1068. initiator_scope="all",
  1069. initiator_user_ids="[]",
  1070. admin_scope="all",
  1071. admin_user_ids="[]",
  1072. status="published",
  1073. tenant_no="zkxw",
  1074. create_dept_id="",
  1075. create_time=now,
  1076. update_time=now,
  1077. del_flag=0,
  1078. )
  1079. )
  1080. self.db.add(
  1081. OaAppVersion(
  1082. id="v1",
  1083. app_id="app1",
  1084. version_no=1,
  1085. status="published",
  1086. is_latest_published="1",
  1087. form_schema_json="{}",
  1088. flow_tree_json="{}",
  1089. advanced_settings_json="{}",
  1090. tenant_no="zkxw",
  1091. create_dept_id="",
  1092. create_time=now,
  1093. update_time=now,
  1094. del_flag=0,
  1095. )
  1096. )
  1097. self.db.add(
  1098. OaInstance(
  1099. id="i1",
  1100. app_id="app1",
  1101. app_version_id="v1",
  1102. title="t",
  1103. status="running",
  1104. initiator_user_id="u0",
  1105. initiator_dept_id="d1",
  1106. form_data_json="{}",
  1107. tenant_no="zkxw",
  1108. create_dept_id="",
  1109. create_time=now,
  1110. update_time=now,
  1111. del_flag=0,
  1112. current_round=1,
  1113. )
  1114. )
  1115. for uid, name in (("u1", "李四"), ("u2", "赵六"), ("u3", "钱七")):
  1116. self.db.add(
  1117. SysUser(
  1118. id=uid,
  1119. user_no=uid,
  1120. nick_name=name,
  1121. email="",
  1122. phone_no="",
  1123. password="",
  1124. status="1",
  1125. tenant_no="zkxw",
  1126. create_dept_id="",
  1127. create_time=now,
  1128. update_time=now,
  1129. del_flag=0,
  1130. )
  1131. )
  1132. self.db.add(
  1133. OaTask(
  1134. id="t1",
  1135. instance_id="i1",
  1136. node_key="a1",
  1137. node_name="审批人",
  1138. task_type="approval",
  1139. assignee_user_id="u1",
  1140. status="pending",
  1141. resolve_source="role",
  1142. sign_mode="and",
  1143. round_no=1,
  1144. tenant_no="zkxw",
  1145. order_num=1,
  1146. create_dept_id="",
  1147. create_time=now,
  1148. update_time=now,
  1149. del_flag=0,
  1150. )
  1151. )
  1152. self.db.add(
  1153. OaTask(
  1154. id="t2",
  1155. instance_id="i1",
  1156. node_key="a1",
  1157. node_name="审批人",
  1158. task_type="approval",
  1159. assignee_user_id="u3",
  1160. status="pending",
  1161. resolve_source="role",
  1162. sign_mode="and",
  1163. round_no=1,
  1164. tenant_no="zkxw",
  1165. order_num=2,
  1166. create_dept_id="",
  1167. create_time=now,
  1168. update_time=now,
  1169. del_flag=0,
  1170. )
  1171. )
  1172. self.db.commit()
  1173. def tearDown(self):
  1174. self.db.close()
  1175. self.engine.dispose()
  1176. def test_transfer_rejects_self(self):
  1177. from app.services.oa_engine import OaEngineError, transfer_task
  1178. with self.assertRaises(OaEngineError) as ctx:
  1179. transfer_task(
  1180. self.db, "t1", tenant_no="zkxw", user_id="u1", target_user_id="u1"
  1181. )
  1182. self.assertIn("自己", str(ctx.exception))
  1183. def test_transfer_rejects_existing_pending(self):
  1184. from app.services.oa_engine import OaEngineError, transfer_task
  1185. with self.assertRaises(OaEngineError) as ctx:
  1186. transfer_task(
  1187. self.db, "t1", tenant_no="zkxw", user_id="u1", target_user_id="u3"
  1188. )
  1189. self.assertIn("待办", str(ctx.exception))
  1190. def test_transfer_rejects_non_assignee(self):
  1191. from app.services.oa_engine import OaEngineError, transfer_task
  1192. with self.assertRaises(OaEngineError) as ctx:
  1193. transfer_task(
  1194. self.db, "t1", tenant_no="zkxw", user_id="u2", target_user_id="u2"
  1195. )
  1196. self.assertIn("无权", str(ctx.exception))
  1197. def test_transfer_success_and_keeps_co_approver(self):
  1198. from app.services.oa_engine import list_tasks, transfer_task
  1199. with patch(
  1200. "app.services.oa_engine.get_instance_detail",
  1201. return_value={"id": "i1", "status": "running"},
  1202. ):
  1203. transfer_task(
  1204. self.db,
  1205. "t1",
  1206. tenant_no="zkxw",
  1207. user_id="u1",
  1208. target_user_id="u2",
  1209. comment="请协助处理",
  1210. )
  1211. t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
  1212. t2 = self.db.query(self.OaTask).filter(self.OaTask.id == "t2").first()
  1213. new_task = (
  1214. self.db.query(self.OaTask)
  1215. .filter(
  1216. self.OaTask.instance_id == "i1",
  1217. self.OaTask.assignee_user_id == "u2",
  1218. self.OaTask.status == "pending",
  1219. )
  1220. .first()
  1221. )
  1222. self.assertEqual(t1.status, "transferred")
  1223. self.assertEqual(t1.comment_text, "请协助处理")
  1224. self.assertEqual(t2.status, "pending")
  1225. self.assertIsNotNone(new_task)
  1226. self.assertEqual(new_task.resolve_source, "transfer")
  1227. self.assertEqual(new_task.sign_mode, "and")
  1228. self.assertEqual(new_task.node_key, "a1")
  1229. done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")
  1230. self.assertEqual(len(done), 1)
  1231. self.assertEqual(done[0]["status"], "transferred")
  1232. from app.services.oa_engine import build_progress_items
  1233. tasks = (
  1234. self.db.query(self.OaTask)
  1235. .filter(self.OaTask.instance_id == "i1", self.OaTask.del_flag == 0)
  1236. .all()
  1237. )
  1238. inst = self.db.query(self.OaInstance).filter(self.OaInstance.id == "i1").first()
  1239. name_map = {"u1": "李四", "u2": "赵六", "u3": "钱七"}
  1240. items = build_progress_items(
  1241. inst=inst, flow_root=None, tasks=tasks, name_map=name_map
  1242. )
  1243. card = next(i for i in items if i.get("kind") == "task")
  1244. transferred = next(p for p in card["people"] if p["status"] == "transferred")
  1245. self.assertEqual(transferred["nickName"], "李四")
  1246. self.assertEqual(transferred["transferToUserId"], "u2")
  1247. self.assertEqual(transferred["transferToUserName"], "赵六")
  1248. def test_transfer_comment_optional(self):
  1249. from app.services.oa_engine import transfer_task
  1250. with patch(
  1251. "app.services.oa_engine.get_instance_detail",
  1252. return_value={"id": "i1", "status": "running"},
  1253. ):
  1254. transfer_task(
  1255. self.db,
  1256. "t1",
  1257. tenant_no="zkxw",
  1258. user_id="u1",
  1259. target_user_id="u2",
  1260. )
  1261. t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
  1262. self.assertEqual(t1.status, "transferred")
  1263. self.assertIsNone(t1.comment_text)
  1264. def test_transfer_chain(self):
  1265. from app.services.oa_engine import transfer_task
  1266. now = datetime.now()
  1267. self.db.add(
  1268. self.SysUser(
  1269. id="u4",
  1270. user_no="u4",
  1271. nick_name="孙八",
  1272. email="",
  1273. phone_no="",
  1274. password="",
  1275. status="1",
  1276. tenant_no="zkxw",
  1277. create_dept_id="",
  1278. create_time=now,
  1279. update_time=now,
  1280. del_flag=0,
  1281. )
  1282. )
  1283. self.db.commit()
  1284. with patch(
  1285. "app.services.oa_engine.get_instance_detail",
  1286. return_value={"id": "i1", "status": "running"},
  1287. ):
  1288. transfer_task(
  1289. self.db,
  1290. "t1",
  1291. tenant_no="zkxw",
  1292. user_id="u1",
  1293. target_user_id="u2",
  1294. )
  1295. u2_task = (
  1296. self.db.query(self.OaTask)
  1297. .filter(
  1298. self.OaTask.instance_id == "i1",
  1299. self.OaTask.assignee_user_id == "u2",
  1300. self.OaTask.status == "pending",
  1301. )
  1302. .first()
  1303. )
  1304. self.assertIsNotNone(u2_task)
  1305. with patch(
  1306. "app.services.oa_engine.get_instance_detail",
  1307. return_value={"id": "i1", "status": "running"},
  1308. ):
  1309. transfer_task(
  1310. self.db,
  1311. u2_task.id,
  1312. tenant_no="zkxw",
  1313. user_id="u2",
  1314. target_user_id="u4",
  1315. )
  1316. u2_task = self.db.query(self.OaTask).filter(self.OaTask.id == u2_task.id).first()
  1317. u4_task = (
  1318. self.db.query(self.OaTask)
  1319. .filter(
  1320. self.OaTask.instance_id == "i1",
  1321. self.OaTask.assignee_user_id == "u4",
  1322. self.OaTask.status == "pending",
  1323. )
  1324. .first()
  1325. )
  1326. self.assertEqual(u2_task.status, "transferred")
  1327. self.assertIsNotNone(u4_task)
  1328. self.assertEqual(u4_task.resolve_source, "transfer")
  1329. if __name__ == "__main__":
  1330. unittest.main()