| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605 |
- """OA 引擎单元测试(纯逻辑 + 轻量 mock)"""
- import json
- import unittest
- from datetime import datetime
- from unittest.mock import MagicMock, patch
- from app.services.oa_engine import (
- OaEngineError,
- _find_node,
- _flow_node_key_order,
- _match_condition_group,
- _sort_tasks_for_detail,
- advance_from,
- )
- class TestOaEngineHelpers(unittest.TestCase):
- def test_find_node(self):
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "child": {"id": "e1", "type": "end"},
- },
- }
- self.assertEqual(_find_node(root, "a1")["id"], "a1")
- self.assertEqual(_find_node(root, "e1")["id"], "e1")
- self.assertIsNone(_find_node(root, "x"))
- def test_condition_match(self):
- groups = [
- {
- "name": "gt3",
- "when": "{days} > 3",
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
- ]
- g = _match_condition_group(groups, {"days": 5})
- self.assertEqual(g["name"], "gt3")
- g2 = _match_condition_group(groups, {"days": 1})
- self.assertEqual(g2["name"], "default")
- def test_condition_match_rules_list(self):
- groups = [
- {
- "name": "gt3",
- "rules": [{"field": "days", "op": ">", "value": 3}],
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "isDefault": True, "rules": [], "child": {"id": "n2", "type": "end"}},
- ]
- self.assertEqual(_match_condition_group(groups, {"days": 5})["name"], "gt3")
- self.assertEqual(_match_condition_group(groups, {"days": 1})["name"], "default")
- def test_condition_match_rules_and(self):
- groups = [
- {
- "name": "both",
- "rules": [
- {"field": "days", "op": ">", "value": 3},
- {"field": "leaveType", "op": "==", "value": "事假"},
- ],
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "rules": [], "isDefault": True, "child": {"id": "n2", "type": "end"}},
- ]
- self.assertEqual(
- _match_condition_group(groups, {"days": 5, "leaveType": "事假"})["name"],
- "both",
- )
- self.assertEqual(
- _match_condition_group(groups, {"days": 5, "leaveType": "病假"})["name"],
- "default",
- )
- def test_condition_contains_person(self):
- groups = [
- {
- "name": "hit",
- "rules": [
- {"field": "Fperson", "op": "contains", "value": ["u1", "u2"]}
- ],
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
- ]
- g = _match_condition_group(
- groups,
- {"Fperson": [{"id": "u2", "name": "李四"}]},
- )
- self.assertEqual(g["name"], "hit")
- g2 = _match_condition_group(
- groups,
- {"Fperson": [{"id": "u9", "name": "其他人"}]},
- )
- self.assertEqual(g2["name"], "default")
- def test_condition_contains_person_single_object(self):
- groups = [
- {
- "name": "hit",
- "rules": [{"field": "Fperson", "op": "contains", "value": ["u1"]}],
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "isDefault": True, "child": {"id": "n2", "type": "end"}},
- ]
- g = _match_condition_group(
- groups, {"Fperson": {"id": "u1", "name": "张三"}}
- )
- self.assertEqual(g["name"], "hit")
- def test_condition_contains_dept(self):
- groups = [
- {
- "name": "hit",
- "rules": [
- {"field": "Fdept", "op": "contains", "value": ["d1", "d2"]}
- ],
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
- ]
- g = _match_condition_group(
- groups,
- {"Fdept": [{"id": "d2", "name": "研发"}]},
- )
- self.assertEqual(g["name"], "hit")
- g2 = _match_condition_group(
- groups,
- {"Fdept": [{"id": "d9", "name": "其他"}]},
- )
- self.assertEqual(g2["name"], "default")
- def test_condition_contains_owner_dept_single_object(self):
- groups = [
- {
- "name": "hit",
- "rules": [{"field": "Fod", "op": "contains", "value": ["d1"]}],
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "isDefault": True, "child": {"id": "n2", "type": "end"}},
- ]
- g = _match_condition_group(
- groups, {"Fod": {"id": "d1", "name": "研发"}}
- )
- self.assertEqual(g["name"], "hit")
- def test_condition_contains_initiator(self):
- groups = [
- {
- "name": "hit",
- "rules": [
- {
- "field": "__initiator__",
- "op": "contains",
- "value": ["u1", "u2"],
- }
- ],
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
- ]
- g = _match_condition_group(groups, {}, initiator_user_id="u2")
- self.assertEqual(g["name"], "hit")
- g2 = _match_condition_group(groups, {}, initiator_user_id="u9")
- self.assertEqual(g2["name"], "default")
- def test_condition_initiator_and_form_field(self):
- groups = [
- {
- "name": "both",
- "rules": [
- {"field": "__initiator__", "op": "contains", "value": ["u1"]},
- {"field": "days", "op": ">", "value": 3},
- ],
- "child": {"id": "n1", "type": "end"},
- },
- {"name": "default", "isDefault": True, "child": {"id": "n2", "type": "end"}},
- ]
- self.assertEqual(
- _match_condition_group(
- groups, {"days": 5}, initiator_user_id="u1"
- )["name"],
- "both",
- )
- self.assertEqual(
- _match_condition_group(
- groups, {"days": 5}, initiator_user_id="u9"
- )["name"],
- "default",
- )
- def test_flow_node_key_order_cc_before_approval(self):
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "c1",
- "type": "cc",
- "child": {
- "id": "a1",
- "type": "approval",
- "child": {"id": "e1", "type": "end"},
- },
- },
- }
- order = _flow_node_key_order(root)
- self.assertLess(order["c1"], order["a1"])
- def test_sort_tasks_by_flow_when_same_create_time(self):
- """抄送先于审批设计时,即使 create_time / order_num 相同也应按树序展示。"""
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "c1",
- "type": "cc",
- "child": {
- "id": "a1",
- "type": "approval",
- "child": {"id": "e1", "type": "end"},
- },
- },
- }
- ts = datetime(2026, 7, 15, 14, 5, 7)
- approval = MagicMock(node_key="a1", order_num=0, create_time=ts, id="AAAA")
- cc = MagicMock(node_key="c1", order_num=0, create_time=ts, id="BBBB")
- # 故意把审批放在列表前面,模拟 create_time 同秒时 PK 乱序
- sorted_tasks = _sort_tasks_for_detail([approval, cc], flow_root=root)
- self.assertEqual([t.node_key for t in sorted_tasks], ["c1", "a1"])
- class TestAdvanceFrom(unittest.TestCase):
- def test_linear_creates_approval_then_stops(self):
- flow = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "负责人",
- "approverConfig": {"assigneeType": "dept_leader"},
- "child": {"id": "e1", "type": "end"},
- },
- }
- db = MagicMock()
- inst = MagicMock(
- id="inst1",
- tenant_no="zkxw",
- initiator_user_id="u0",
- initiator_dept_id="d1",
- form_data_json="{}",
- status="running",
- )
- ver = MagicMock(
- flow_tree_json=json.dumps(flow),
- advanced_settings_json=json.dumps({"supervisorFallbackToDeptLeader": True}),
- )
- fake_task = MagicMock()
- with patch(
- "app.services.oa_engine._next_task_order_num", return_value=1
- ), patch(
- "app.services.oa_engine._create_tasks_for_node",
- return_value=[fake_task],
- ) as mock_create:
- advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
- mock_create.assert_called_once()
- self.assertEqual(inst.status, "running")
- self.assertEqual(mock_create.call_args.kwargs.get("order_num_start"), 1)
- def test_complete_path_to_end_via_cc(self):
- flow = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "c1",
- "type": "cc",
- "name": "抄送",
- "approverConfig": {"assigneeType": "users", "userIds": ["u9"]},
- "child": {"id": "e1", "type": "end"},
- },
- }
- db = MagicMock()
- inst = MagicMock(
- id="inst1",
- tenant_no="zkxw",
- initiator_user_id="u0",
- initiator_dept_id="d1",
- form_data_json="{}",
- status="running",
- update_time=datetime.now(),
- )
- ver = MagicMock(
- flow_tree_json=json.dumps(flow),
- advanced_settings_json="{}",
- )
- with patch(
- "app.services.oa_engine._next_task_order_num", return_value=1
- ), patch(
- "app.services.oa_engine._create_tasks_for_node",
- return_value=[MagicMock()],
- ):
- advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
- self.assertEqual(inst.status, "completed")
- def test_empty_assignee_raises(self):
- flow = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "approverConfig": {"assigneeType": "dept_leader"},
- "child": {"id": "e1", "type": "end"},
- },
- }
- db = MagicMock()
- inst = MagicMock(
- id="inst1",
- tenant_no="zkxw",
- initiator_user_id="u0",
- initiator_dept_id="d1",
- form_data_json="{}",
- status="running",
- )
- ver = MagicMock(flow_tree_json=json.dumps(flow), advanced_settings_json="{}")
- with patch(
- "app.services.oa_engine._next_task_order_num", return_value=1
- ), patch("app.services.oa_engine._create_tasks_for_node", return_value=[]):
- with self.assertRaises(OaEngineError):
- advance_from(db, inst=inst, ver=ver, from_node=None, user_id="u0")
- class TestAddSign(unittest.TestCase):
- def test_add_sign_rejects_when_disabled(self):
- from app.services.oa_engine import add_sign
- db = MagicMock()
- task = MagicMock(
- id="t1",
- assignee_user_id="u1",
- task_type="approval",
- status="pending",
- instance_id="i1",
- node_key="a1",
- node_name="审批",
- sign_mode="or",
- comment_text=None,
- )
- inst = MagicMock(id="i1", status="running", initiator_dept_id="d1")
- ver = MagicMock(
- id="v1",
- advanced_settings_json=json.dumps({"allowAddSign": False}),
- del_flag=0,
- )
- db.query.return_value.filter.return_value.first.side_effect = [task, inst, ver]
- with self.assertRaises(OaEngineError) as ctx:
- add_sign(
- db,
- "t1",
- tenant_no="zkxw",
- user_id="u1",
- user_ids=["u2"],
- )
- self.assertIn("不允许加签", str(ctx.exception))
- def test_add_sign_requires_users(self):
- from app.services.oa_engine import add_sign
- db = MagicMock()
- task = MagicMock(
- id="t1",
- assignee_user_id="u1",
- task_type="approval",
- status="pending",
- instance_id="i1",
- )
- inst = MagicMock(id="i1", status="running")
- ver = MagicMock(advanced_settings_json="{}", del_flag=0)
- db.query.return_value.filter.return_value.first.side_effect = [task, inst, ver]
- with self.assertRaises(OaEngineError) as ctx:
- add_sign(db, "t1", tenant_no="zkxw", user_id="u1", user_ids=[])
- self.assertIn("加签人", str(ctx.exception))
- class TestCompleteTaskOrSign(unittest.TestCase):
- """回归:Session autoflush=False 时,或签取消同节点待办不可覆盖本人已同意状态。"""
- def setUp(self):
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- from app.core.database import Base
- from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
- self.engine = create_engine("sqlite:///:memory:")
- # 仅建引擎相关表
- OaApp.__table__.create(self.engine)
- OaAppVersion.__table__.create(self.engine)
- OaInstance.__table__.create(self.engine)
- OaTask.__table__.create(self.engine)
- OaInstanceEvent.__table__.create(self.engine)
- SysUser.__table__.create(self.engine)
- Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
- self.db = Session()
- self.Base = Base
- self.OaAppVersion = OaAppVersion
- self.OaInstance = OaInstance
- self.OaTask = OaTask
- flow = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "审批人",
- "approverConfig": {"signMode": "or"},
- "child": {"id": "e1", "type": "end"},
- },
- }
- now = datetime.now()
- self.db.add(
- 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="{}",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaInstance(
- id="i1",
- app_id="app1",
- app_version_id="v1",
- title="单人审批",
- initiator_user_id="u0",
- initiator_dept_id="d1",
- form_data_json="{}",
- status="running",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaTask(
- id="t1",
- instance_id="i1",
- node_key="a1",
- node_name="审批人",
- task_type="approval",
- assignee_user_id="u1",
- status="pending",
- sign_mode="or",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.commit()
- def tearDown(self):
- self.db.close()
- self.engine.dispose()
- def test_approve_keeps_approved_not_done(self):
- from app.services.oa_engine import complete_task
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "completed"},
- ):
- complete_task(
- self.db,
- "t1",
- tenant_no="zkxw",
- user_id="u1",
- action="approve",
- comment="同意",
- )
- task = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
- self.assertEqual(task.status, "approved")
- self.assertEqual(task.comment_text, "同意")
- # 已处理列表应能查到
- from app.services.oa_engine import list_tasks
- done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")["list"]
- self.assertEqual(len(done), 1)
- self.assertEqual(done[0]["status"], "approved")
- def test_or_sign_cancels_siblings_only(self):
- from app.services.oa_engine import complete_task
- now = datetime.now()
- self.db.add(
- self.OaTask(
- id="t2",
- instance_id="i1",
- node_key="a1",
- node_name="审批人",
- task_type="approval",
- assignee_user_id="u2",
- status="pending",
- sign_mode="or",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.commit()
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "completed"},
- ):
- complete_task(
- self.db,
- "t1",
- tenant_no="zkxw",
- user_id="u1",
- action="approve",
- comment="同意",
- )
- t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
- t2 = self.db.query(self.OaTask).filter(self.OaTask.id == "t2").first()
- self.assertEqual(t1.status, "approved")
- self.assertEqual(t2.status, "done")
- class TestBuildProgress(unittest.TestCase):
- def test_cc_before_approval_and_pending_ahead(self):
- from app.services.oa_engine import build_progress_items
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "c1",
- "type": "cc",
- "name": "抄送人",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "审批人",
- "child": {
- "id": "a2",
- "type": "approval",
- "name": "总经理",
- "child": {"id": "e1", "type": "end"},
- },
- },
- },
- }
- ts = datetime(2026, 7, 15, 14, 5, 7)
- # 审批任务 create 同秒但列表故意审批在前
- t_approval = MagicMock(
- id="tA",
- node_key="a1",
- node_name="审批人",
- task_type="approval",
- status="pending",
- assignee_user_id="u2",
- order_num=0,
- round_no=1,
- create_time=ts,
- update_time=ts,
- comment_text=None,
- resolve_source="users",
- )
- t_cc = MagicMock(
- id="tC",
- node_key="c1",
- node_name="抄送人",
- task_type="cc",
- status="done",
- assignee_user_id="u9",
- order_num=0,
- round_no=1,
- create_time=ts,
- update_time=ts,
- comment_text=None,
- resolve_source="users",
- )
- inst = MagicMock(
- status="running",
- initiator_user_id="u0",
- current_round=1,
- create_time=ts,
- update_time=ts,
- )
- items = build_progress_items(
- inst=inst,
- flow_root=root,
- tasks=[t_approval, t_cc],
- name_map={"u0": "发起人甲", "u2": "审批乙", "u9": "抄送丙"},
- events=[], # P0 可空
- )
- kinds = [x["kind"] for x in items]
- self.assertEqual(kinds[0], "submit")
- # 已产生:cc 再 approval
- self.assertEqual(
- [x["nodeKey"] for x in items if x["kind"] == "task"],
- ["c1", "a1"],
- )
- # running 时未到达 a2
- pending = [x for x in items if x["kind"] == "pending_node"]
- self.assertTrue(any(p["nodeKey"] == "a2" for p in pending))
- def test_running_shows_all_subsequent_pending_nodes(self):
- """进行中应挂出路径上全部后续节点,而非只露一个空壳。"""
- from app.services.oa_engine import build_progress_items
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "一级审批",
- "child": {
- "id": "a2",
- "type": "approval",
- "name": "二级审批",
- "child": {
- "id": "a3",
- "type": "approval",
- "name": "三级审批",
- "child": {"id": "e1", "type": "end"},
- },
- },
- },
- }
- ts = datetime(2026, 7, 17, 11, 0, 0)
- t1 = MagicMock(
- id="t1",
- node_key="a1",
- node_name="一级审批",
- task_type="approval",
- status="pending",
- assignee_user_id="u1",
- order_num=0,
- round_no=1,
- create_time=ts,
- update_time=ts,
- comment_text=None,
- resolve_source="users",
- sign_mode="or",
- )
- inst = MagicMock(
- status="running",
- initiator_user_id="u0",
- current_round=1,
- create_time=ts,
- update_time=ts,
- form_data_json="{}",
- tenant_no="t1",
- initiator_dept_id="",
- )
- items = build_progress_items(
- inst=inst,
- flow_root=root,
- tasks=[t1],
- name_map={"u0": "发起人", "u1": "甲"},
- events=[],
- )
- pending = [x for x in items if x["kind"] == "pending_node"]
- self.assertEqual([p["nodeKey"] for p in pending], ["a2", "a3", "e1"])
- self.assertEqual([p["title"] for p in pending], ["二级审批", "三级审批", "结束"])
- self.assertEqual(pending[-1]["taskType"], "end")
- def test_or_sign_same_node_merged_into_one_card(self):
- """或签同节点三人只投影一张进度卡片,people 含各人状态。"""
- from app.services.oa_engine import build_progress_items
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "部门审批",
- "child": {"id": "e1", "type": "end"},
- },
- }
- ts = datetime(2026, 7, 17, 10, 0, 0)
- ts2 = datetime(2026, 7, 17, 10, 5, 0)
- def mk(tid, uid, status, comment=None, update=None):
- return MagicMock(
- id=tid,
- node_key="a1",
- node_name="部门审批",
- task_type="approval",
- status=status,
- assignee_user_id=uid,
- order_num=0,
- round_no=1,
- create_time=ts,
- update_time=update or ts,
- comment_text=comment,
- resolve_source="users",
- sign_mode="or",
- )
- tasks = [
- mk("t1", "u1", "approved", "同意", ts2),
- mk("t2", "u2", "done"),
- mk("t3", "u3", "done"),
- ]
- inst = MagicMock(
- status="completed",
- initiator_user_id="u0",
- current_round=1,
- create_time=ts,
- update_time=ts2,
- )
- items = build_progress_items(
- inst=inst,
- flow_root=root,
- tasks=tasks,
- name_map={"u0": "发起人", "u1": "甲", "u2": "乙", "u3": "丙"},
- events=[],
- )
- task_items = [x for x in items if x["kind"] == "task"]
- self.assertEqual(len(task_items), 1)
- card = task_items[0]
- self.assertEqual(card["nodeKey"], "a1")
- self.assertEqual(card["status"], "approved")
- self.assertEqual(card["signMode"], "or")
- self.assertEqual(len(card["people"]), 3)
- self.assertEqual(
- [(p["nickName"], p["status"]) for p in card["people"]],
- [("甲", "approved"), ("乙", "done"), ("丙", "done")],
- )
- self.assertIn("甲:同意", card["commentText"])
- def test_terminal_status_excludes_pending_nodes(self):
- from app.services.oa_engine import build_progress_items
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "审批人",
- "child": {
- "id": "a2",
- "type": "approval",
- "name": "总经理",
- "child": {"id": "e1", "type": "end"},
- },
- },
- }
- ts = datetime(2026, 7, 15, 14, 5, 7)
- t_approval = MagicMock(
- id="tA",
- node_key="a1",
- node_name="审批人",
- task_type="approval",
- status="rejected",
- assignee_user_id="u2",
- order_num=0,
- round_no=1,
- create_time=ts,
- update_time=ts,
- comment_text="不同意",
- resolve_source="users",
- )
- for status in ("rejected", "completed", "cancelled", "returned"):
- inst = MagicMock(
- status=status,
- initiator_user_id="u0",
- current_round=1,
- create_time=ts,
- update_time=ts,
- )
- items = build_progress_items(
- inst=inst,
- flow_root=root,
- tasks=[t_approval],
- name_map={"u0": "发起人甲", "u2": "审批乙"},
- events=[],
- )
- pending = [x for x in items if x["kind"] == "pending_node"]
- self.assertEqual(pending, [], f"status={status} should not have pending_node")
- terminal = [x for x in items if x["kind"] == "terminal"]
- # 打回仅展示审批人节点「已打回」,不追加独立终态行
- expect_n = 0 if status == "returned" else 1
- self.assertEqual(
- len(terminal), expect_n, f"status={status} terminal count"
- )
- def test_cancelled_terminal_attributes_to_initiator(self):
- """撤销终态归因发起人;任务 cancelled 仍投影为节点,由前端映射「无需处理」。"""
- from app.services.oa_engine import build_progress_items
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "直接主管",
- "child": {"id": "e1", "type": "end"},
- },
- }
- ts = datetime(2026, 8, 13, 12, 47, 46)
- t_pending_cancelled = MagicMock(
- id="tA",
- node_key="a1",
- node_name="直接主管",
- task_type="approval",
- status="cancelled",
- assignee_user_id="u2",
- order_num=0,
- round_no=1,
- create_time=datetime(2026, 8, 13, 11, 17, 38),
- update_time=ts,
- comment_text=None,
- resolve_source="direct_supervisor",
- sign_mode="and",
- )
- inst = MagicMock(
- status="cancelled",
- initiator_user_id="u0",
- current_round=1,
- create_time=datetime(2026, 8, 13, 11, 17, 38),
- update_time=ts,
- )
- items = build_progress_items(
- inst=inst,
- flow_root=root,
- tasks=[t_pending_cancelled],
- name_map={"u0": "吴子龙", "u2": "张主管"},
- events=[],
- )
- task_card = next(x for x in items if x["kind"] == "task")
- self.assertEqual(task_card["title"], "直接主管")
- self.assertEqual(task_card["status"], "cancelled")
- self.assertEqual(task_card["people"][0]["status"], "cancelled")
- self.assertEqual(task_card["timestamp"], "", "cancelled 节点勿带撤销时间")
- terminal = next(x for x in items if x["kind"] == "terminal")
- self.assertEqual(terminal["status"], "cancelled")
- self.assertEqual(terminal["title"], "已撤销")
- self.assertEqual(terminal["assigneeUserId"], "u0")
- self.assertEqual(terminal["assigneeUserName"], "吴子龙")
- self.assertEqual(terminal["commentText"], "发起人撤销")
- self.assertTrue(terminal["timestamp"])
- def test_progress_returned_has_no_kind_return_node(self):
- from app.services.oa_engine import build_progress_items
- root = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "审批人",
- "child": {"id": "e1", "type": "end"},
- },
- }
- ts = datetime(2026, 7, 15, 14, 58, 8)
- t_approval = MagicMock(
- id="tA",
- node_key="a1",
- node_name="审批人",
- task_type="approval",
- status="returned",
- assignee_user_id="u2",
- order_num=1,
- round_no=1,
- create_time=ts,
- update_time=ts,
- comment_text="打回",
- resolve_source="users",
- )
- ret_ev = MagicMock(
- event_type="return",
- round_no=1,
- actor_user_id="u2",
- create_time=ts,
- comment_text="打回",
- node_key="a1",
- task_id="tA",
- )
- inst = MagicMock(
- status="returned",
- initiator_user_id="u0",
- current_round=1,
- create_time=ts,
- update_time=ts,
- )
- items = build_progress_items(
- inst=inst,
- flow_root=root,
- tasks=[t_approval],
- name_map={"u0": "发起人", "u2": "审批人"},
- events=[ret_ev],
- )
- self.assertFalse(any(x["kind"] == "return" for x in items))
- tasks = [x for x in items if x["kind"] == "task"]
- self.assertEqual(len(tasks), 1)
- self.assertEqual(tasks[0]["status"], "returned")
- self.assertEqual(tasks[0]["commentText"], "打回")
- terminals = [x for x in items if x["kind"] == "terminal"]
- self.assertEqual(terminals, [], "打回不追加独立终态「已打回」行")
- class TestReturnAndResubmit(unittest.TestCase):
- def setUp(self):
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
- self.engine = create_engine("sqlite:///:memory:")
- OaApp.__table__.create(self.engine)
- OaAppVersion.__table__.create(self.engine)
- OaInstance.__table__.create(self.engine)
- OaTask.__table__.create(self.engine)
- OaInstanceEvent.__table__.create(self.engine)
- SysUser.__table__.create(self.engine)
- Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
- self.db = Session()
- self.OaInstance = OaInstance
- self.OaTask = OaTask
- self.OaInstanceEvent = OaInstanceEvent
- flow = {
- "id": "s",
- "type": "start",
- "child": {
- "id": "a1",
- "type": "approval",
- "name": "审批人",
- "approverConfig": {
- "assigneeType": "users",
- "userIds": ["u1"],
- "signMode": "or",
- },
- "child": {"id": "e1", "type": "end"},
- },
- }
- now = datetime.now()
- self.db.add(
- 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({"allowReturn": True}),
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaInstance(
- id="i1",
- app_id="app1",
- app_version_id="v1",
- title="打回测",
- initiator_user_id="u0",
- initiator_dept_id="d1",
- form_data_json="{}",
- status="running",
- current_round=1,
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaTask(
- id="t1",
- instance_id="i1",
- node_key="a1",
- node_name="审批人",
- task_type="approval",
- assignee_user_id="u1",
- status="pending",
- sign_mode="or",
- round_no=1,
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.commit()
- def tearDown(self):
- self.db.close()
- self.engine.dispose()
- def test_return_sets_returned_and_event(self):
- from app.services.oa_engine import return_task
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "returned"},
- ):
- return_task(
- self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
- )
- inst = self.db.query(self.OaInstance).filter_by(id="i1").first()
- task = self.db.query(self.OaTask).filter_by(id="t1").first()
- ev = (
- self.db.query(self.OaInstanceEvent)
- .filter_by(instance_id="i1", event_type="return")
- .first()
- )
- self.assertEqual(inst.status, "returned")
- self.assertEqual(task.status, "returned")
- self.assertIsNotNone(ev)
- self.assertEqual(ev.comment_text, "请补充材料")
- def test_resubmit_raises_deprecated(self):
- from app.services.oa_engine import OaEngineError, resubmit_instance, return_task
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "returned"},
- ):
- return_task(
- self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
- )
- with self.assertRaises(OaEngineError) as ctx:
- resubmit_instance(
- self.db,
- "i1",
- tenant_no="zkxw",
- user_id="u0",
- form_data={"x": 1},
- )
- self.assertIn("废弃", str(ctx.exception))
- def test_done_list_includes_returned(self):
- from app.services.oa_engine import list_tasks, return_task
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "returned"},
- ):
- return_task(
- self.db, "t1", tenant_no="zkxw", user_id="u1", comment="请补充材料"
- )
- done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")["list"]
- self.assertEqual(len(done), 1)
- self.assertEqual(done[0]["status"], "returned")
- class TestListTasksEnrichment(unittest.TestCase):
- """list_tasks 返回整单此刻进度摘要字段。"""
- def setUp(self):
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- from app.models.models import OaApp, OaAppVersion, OaInstance, OaTask, SysUser
- self.engine = create_engine("sqlite:///:memory:")
- OaApp.__table__.create(self.engine)
- OaAppVersion.__table__.create(self.engine)
- OaInstance.__table__.create(self.engine)
- OaTask.__table__.create(self.engine)
- SysUser.__table__.create(self.engine)
- Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
- self.db = Session()
- self.OaTask = OaTask
- now = datetime(2026, 7, 15, 10, 0, 0)
- self.db.add(
- OaApp(
- id="app1",
- app_name="请假审批",
- initiator_scope="all",
- admin_scope="all_oa_admin",
- status="1",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaAppVersion(
- id="v1",
- app_id="app1",
- version_no=1,
- status="published",
- is_latest_published="1",
- form_schema_json="{}",
- flow_tree_json="{}",
- advanced_settings_json="{}",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- SysUser(
- id="u0",
- user_no="initiator",
- nick_name="发起人甲",
- password="x",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- SysUser(
- id="u1",
- user_no="approver1",
- nick_name="审批人乙",
- password="x",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- SysUser(
- id="u2",
- user_no="approver2",
- nick_name="审批人丙",
- password="x",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaInstance(
- id="i1",
- app_id="app1",
- app_version_id="v1",
- title="请假-忽略",
- initiator_user_id="u0",
- initiator_dept_id="d1",
- form_data_json="{}",
- status="running",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaTask(
- id="t-done",
- instance_id="i1",
- node_key="a1",
- node_name="一级审批",
- task_type="approval",
- assignee_user_id="u1",
- status="approved",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaTask(
- id="t-pending",
- instance_id="i1",
- node_key="a2",
- node_name="二级审批",
- task_type="approval",
- assignee_user_id="u2",
- status="pending",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.commit()
- def tearDown(self):
- self.db.close()
- self.engine.dispose()
- def test_done_list_shows_instance_current_progress(self):
- from app.services.oa_engine import list_tasks
- done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")["list"]
- self.assertEqual(len(done), 1)
- row = done[0]
- self.assertEqual(row["appName"], "请假审批")
- self.assertEqual(row["initiatorUserName"], "发起人甲")
- self.assertEqual(row["applyTime"], "2026-07-15 10:00:00")
- self.assertEqual(row["instanceStatus"], "running")
- self.assertEqual(row["currentNodeName"], "二级审批")
- self.assertEqual(row["currentApproverNames"], "审批人丙")
- self.assertEqual(row["status"], "approved")
- def test_mine_list_enrichment(self):
- from app.services.oa_engine import list_tasks
- mine = list_tasks(self.db, tenant_no="zkxw", user_id="u0", kind="mine")["list"]
- self.assertEqual(len(mine), 1)
- row = mine[0]
- self.assertEqual(row["appName"], "请假审批")
- self.assertEqual(row["instanceStatus"], "running")
- self.assertEqual(row["currentNodeName"], "二级审批")
- self.assertEqual(row["currentApproverNames"], "审批人丙")
- def test_pending_list_enrichment(self):
- from app.services.oa_engine import list_tasks
- pending = list_tasks(self.db, tenant_no="zkxw", user_id="u2", kind="pending")["list"]
- self.assertEqual(len(pending), 1)
- row = pending[0]
- self.assertEqual(row["appName"], "请假审批")
- self.assertEqual(row["currentNodeName"], "二级审批")
- def test_terminal_instance_labels(self):
- from app.services.oa_engine import list_tasks
- from app.models.models import OaInstance
- cases = [
- ("completed", "已完成"),
- ("rejected", "已拒绝"),
- ("returned", "已打回"),
- ("cancelled", "已撤销"),
- ]
- inst = self.db.query(OaInstance).filter_by(id="i1").first()
- pending = self.db.query(self.OaTask).filter_by(id="t-pending").first()
- for status, expected_label in cases:
- with self.subTest(status=status):
- inst.status = status
- pending.status = "cancelled"
- self.db.commit()
- mine = list_tasks(self.db, tenant_no="zkxw", user_id="u0", kind="mine")["list"]
- row = mine[0]
- self.assertEqual(row["instanceStatus"], status)
- self.assertEqual(row["currentNodeName"], expected_label)
- self.assertEqual(row["currentApproverNames"], "-")
- inst.status = "running"
- pending.status = "pending"
- self.db.commit()
- class TestTransferTask(unittest.TestCase):
- def setUp(self):
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
- self.engine = create_engine("sqlite:///:memory:")
- OaApp.__table__.create(self.engine)
- OaAppVersion.__table__.create(self.engine)
- OaInstance.__table__.create(self.engine)
- OaTask.__table__.create(self.engine)
- OaInstanceEvent.__table__.create(self.engine)
- SysUser.__table__.create(self.engine)
- Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
- self.db = Session()
- self.OaTask = OaTask
- self.OaInstance = OaInstance
- self.OaAppVersion = OaAppVersion
- self.SysUser = SysUser
- now = datetime.now()
- self.db.add(
- OaApp(
- id="app1",
- group_id="",
- app_name="测试应用",
- icon_url="",
- description="",
- initiator_scope="all",
- initiator_user_ids="[]",
- admin_scope="all",
- admin_user_ids="[]",
- status="published",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaAppVersion(
- id="v1",
- app_id="app1",
- version_no=1,
- status="published",
- is_latest_published="1",
- form_schema_json="{}",
- flow_tree_json="{}",
- advanced_settings_json="{}",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaInstance(
- id="i1",
- app_id="app1",
- app_version_id="v1",
- title="t",
- status="running",
- initiator_user_id="u0",
- initiator_dept_id="d1",
- form_data_json="{}",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- current_round=1,
- )
- )
- for uid, name in (("u1", "李四"), ("u2", "赵六"), ("u3", "钱七")):
- self.db.add(
- SysUser(
- id=uid,
- user_no=uid,
- nick_name=name,
- email="",
- phone_no="",
- password="",
- status="1",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaTask(
- id="t1",
- instance_id="i1",
- node_key="a1",
- node_name="审批人",
- task_type="approval",
- assignee_user_id="u1",
- status="pending",
- resolve_source="role",
- sign_mode="and",
- round_no=1,
- tenant_no="zkxw",
- order_num=1,
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.add(
- OaTask(
- id="t2",
- instance_id="i1",
- node_key="a1",
- node_name="审批人",
- task_type="approval",
- assignee_user_id="u3",
- status="pending",
- resolve_source="role",
- sign_mode="and",
- round_no=1,
- tenant_no="zkxw",
- order_num=2,
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.commit()
- def tearDown(self):
- self.db.close()
- self.engine.dispose()
- def test_transfer_rejects_self(self):
- from app.services.oa_engine import OaEngineError, transfer_task
- with self.assertRaises(OaEngineError) as ctx:
- transfer_task(
- self.db, "t1", tenant_no="zkxw", user_id="u1", target_user_id="u1"
- )
- self.assertIn("自己", str(ctx.exception))
- def test_transfer_rejects_existing_pending(self):
- from app.services.oa_engine import OaEngineError, transfer_task
- with self.assertRaises(OaEngineError) as ctx:
- transfer_task(
- self.db, "t1", tenant_no="zkxw", user_id="u1", target_user_id="u3"
- )
- self.assertIn("待办", str(ctx.exception))
- def test_transfer_rejects_non_assignee(self):
- from app.services.oa_engine import OaEngineError, transfer_task
- with self.assertRaises(OaEngineError) as ctx:
- transfer_task(
- self.db, "t1", tenant_no="zkxw", user_id="u2", target_user_id="u2"
- )
- self.assertIn("无权", str(ctx.exception))
- def test_transfer_success_and_keeps_co_approver(self):
- from app.services.oa_engine import list_tasks, transfer_task
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "running"},
- ):
- transfer_task(
- self.db,
- "t1",
- tenant_no="zkxw",
- user_id="u1",
- target_user_id="u2",
- comment="请协助处理",
- )
- t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
- t2 = self.db.query(self.OaTask).filter(self.OaTask.id == "t2").first()
- new_task = (
- self.db.query(self.OaTask)
- .filter(
- self.OaTask.instance_id == "i1",
- self.OaTask.assignee_user_id == "u2",
- self.OaTask.status == "pending",
- )
- .first()
- )
- self.assertEqual(t1.status, "transferred")
- self.assertEqual(t1.comment_text, "请协助处理")
- self.assertEqual(t2.status, "pending")
- self.assertIsNotNone(new_task)
- self.assertEqual(new_task.resolve_source, "transfer")
- self.assertEqual(new_task.sign_mode, "and")
- self.assertEqual(new_task.node_key, "a1")
- done = list_tasks(self.db, tenant_no="zkxw", user_id="u1", kind="done")["list"]
- self.assertEqual(len(done), 1)
- self.assertEqual(done[0]["status"], "transferred")
- from app.services.oa_engine import build_progress_items
- tasks = (
- self.db.query(self.OaTask)
- .filter(self.OaTask.instance_id == "i1", self.OaTask.del_flag == 0)
- .all()
- )
- inst = self.db.query(self.OaInstance).filter(self.OaInstance.id == "i1").first()
- name_map = {"u1": "李四", "u2": "赵六", "u3": "钱七"}
- items = build_progress_items(
- inst=inst, flow_root=None, tasks=tasks, name_map=name_map
- )
- card = next(i for i in items if i.get("kind") == "task")
- transferred = next(p for p in card["people"] if p["status"] == "transferred")
- self.assertEqual(transferred["nickName"], "李四")
- self.assertEqual(transferred["transferToUserId"], "u2")
- self.assertEqual(transferred["transferToUserName"], "赵六")
- def test_transfer_comment_optional(self):
- from app.services.oa_engine import transfer_task
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "running"},
- ):
- transfer_task(
- self.db,
- "t1",
- tenant_no="zkxw",
- user_id="u1",
- target_user_id="u2",
- )
- t1 = self.db.query(self.OaTask).filter(self.OaTask.id == "t1").first()
- self.assertEqual(t1.status, "transferred")
- self.assertIsNone(t1.comment_text)
- def test_transfer_chain(self):
- from app.services.oa_engine import transfer_task
- now = datetime.now()
- self.db.add(
- self.SysUser(
- id="u4",
- user_no="u4",
- nick_name="孙八",
- email="",
- phone_no="",
- password="",
- status="1",
- tenant_no="zkxw",
- create_dept_id="",
- create_time=now,
- update_time=now,
- del_flag=0,
- )
- )
- self.db.commit()
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "running"},
- ):
- transfer_task(
- self.db,
- "t1",
- tenant_no="zkxw",
- user_id="u1",
- target_user_id="u2",
- )
- u2_task = (
- self.db.query(self.OaTask)
- .filter(
- self.OaTask.instance_id == "i1",
- self.OaTask.assignee_user_id == "u2",
- self.OaTask.status == "pending",
- )
- .first()
- )
- self.assertIsNotNone(u2_task)
- with patch(
- "app.services.oa_engine.get_instance_detail",
- return_value={"id": "i1", "status": "running"},
- ):
- transfer_task(
- self.db,
- u2_task.id,
- tenant_no="zkxw",
- user_id="u2",
- target_user_id="u4",
- )
- u2_task = self.db.query(self.OaTask).filter(self.OaTask.id == u2_task.id).first()
- u4_task = (
- self.db.query(self.OaTask)
- .filter(
- self.OaTask.instance_id == "i1",
- self.OaTask.assignee_user_id == "u4",
- self.OaTask.status == "pending",
- )
- .first()
- )
- self.assertEqual(u2_task.status, "transferred")
- self.assertIsNotNone(u4_task)
- self.assertEqual(u4_task.resolve_source, "transfer")
- if __name__ == "__main__":
- unittest.main()
|