|
|
@@ -689,7 +689,11 @@ class TestBuildProgress(unittest.TestCase):
|
|
|
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"]
|
|
|
- self.assertEqual(len(terminal), 1, f"status={status} should have one terminal")
|
|
|
+ # 打回仅展示审批人节点「已打回」,不追加独立终态行
|
|
|
+ expect_n = 0 if status == "returned" else 1
|
|
|
+ self.assertEqual(
|
|
|
+ len(terminal), expect_n, f"status={status} terminal count"
|
|
|
+ )
|
|
|
|
|
|
def test_progress_returned_has_no_kind_return_node(self):
|
|
|
from app.services.oa_engine import build_progress_items
|
|
|
@@ -748,8 +752,7 @@ class TestBuildProgress(unittest.TestCase):
|
|
|
self.assertEqual(tasks[0]["status"], "returned")
|
|
|
self.assertEqual(tasks[0]["commentText"], "打回")
|
|
|
terminals = [x for x in items if x["kind"] == "terminal"]
|
|
|
- self.assertEqual(len(terminals), 1)
|
|
|
- self.assertEqual(terminals[0]["status"], "returned")
|
|
|
+ self.assertEqual(terminals, [], "打回不追加独立终态「已打回」行")
|
|
|
|
|
|
|
|
|
class TestReturnAndResubmit(unittest.TestCase):
|
|
|
@@ -1110,6 +1113,300 @@ class TestListTasksEnrichment(unittest.TestCase):
|
|
|
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")
|
|
|
+ self.assertEqual(len(done), 1)
|
|
|
+ self.assertEqual(done[0]["status"], "transferred")
|
|
|
+
|
|
|
+ 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()
|
|
|
|