test_oa_instance_history.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. """OA list_instance_history:按 appId + 全员权限收窄。"""
  2. from __future__ import annotations
  3. import unittest
  4. from datetime import datetime
  5. from sqlalchemy import create_engine
  6. from sqlalchemy.orm import sessionmaker
  7. from app.models.base import Base
  8. from app.models.models import OaApp, OaAppVersion, OaInstance, OaInstanceEvent, OaTask, SysUser
  9. from app.services import oa_engine
  10. class TestOaInstanceHistory(unittest.TestCase):
  11. def setUp(self):
  12. self.engine = create_engine("sqlite:///:memory:")
  13. Base.metadata.create_all(
  14. self.engine,
  15. tables=[
  16. OaApp.__table__,
  17. OaAppVersion.__table__,
  18. SysUser.__table__,
  19. OaInstance.__table__,
  20. OaTask.__table__,
  21. OaInstanceEvent.__table__,
  22. ],
  23. )
  24. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  25. self.db = Session()
  26. t1 = datetime(2026, 8, 10, 9, 0, 0)
  27. t2 = datetime(2026, 8, 20, 15, 30, 0)
  28. t3 = datetime(2026, 8, 25, 11, 0, 0)
  29. self.db.add_all(
  30. [
  31. OaApp(
  32. id="app-leave",
  33. app_name="请假申请",
  34. initiator_scope="all",
  35. admin_scope="all_oa_admin",
  36. status="1",
  37. tenant_no="zkxw",
  38. create_dept_id="",
  39. create_time=t1,
  40. update_time=t1,
  41. del_flag=0,
  42. ),
  43. OaAppVersion(
  44. id="v-leave",
  45. app_id="app-leave",
  46. version_no=1,
  47. status="published",
  48. is_latest_published="1",
  49. form_schema_json='{"rule":[{"type":"input","field":"leaveType","title":"请假类型"},{"type":"input","field":"days","title":"天数"},{"type":"upload","field":"voucher","title":"付款凭证"}]}',
  50. flow_tree_json="{}",
  51. advanced_settings_json="{}",
  52. tenant_no="zkxw",
  53. create_dept_id="",
  54. create_time=t1,
  55. update_time=t1,
  56. del_flag=0,
  57. ),
  58. SysUser(
  59. id="u-a",
  60. user_no="a",
  61. nick_name="张三",
  62. password="x",
  63. tenant_no="zkxw",
  64. create_dept_id="",
  65. create_time=t1,
  66. update_time=t1,
  67. del_flag=0,
  68. ),
  69. SysUser(
  70. id="u-b",
  71. user_no="b",
  72. nick_name="李四",
  73. password="x",
  74. tenant_no="zkxw",
  75. create_dept_id="",
  76. create_time=t1,
  77. update_time=t1,
  78. del_flag=0,
  79. ),
  80. OaInstance(
  81. id="inst-alice-done",
  82. app_id="app-leave",
  83. app_version_id="v-leave",
  84. title="请假-张三",
  85. initiator_user_id="u-a",
  86. form_data_json='{"leaveType":"年假","days":"2","voucher":"f33a569a9dd64f2c853899498cd47498.png"}',
  87. status="completed",
  88. tenant_no="zkxw",
  89. create_dept_id="",
  90. create_time=t1,
  91. update_time=t2,
  92. del_flag=0,
  93. ),
  94. OaInstance(
  95. id="inst-bob-run",
  96. app_id="app-leave",
  97. app_version_id="v-leave",
  98. title="请假-李四",
  99. initiator_user_id="u-b",
  100. form_data_json='{"leaveType":"事假","days":"1"}',
  101. status="running",
  102. tenant_no="zkxw",
  103. create_dept_id="",
  104. create_time=t3,
  105. update_time=t3,
  106. del_flag=0,
  107. ),
  108. ]
  109. )
  110. self.db.commit()
  111. def tearDown(self):
  112. self.db.close()
  113. self.engine.dispose()
  114. def test_requires_app_id(self):
  115. with self.assertRaises(oa_engine.OaEngineError):
  116. oa_engine.list_instance_history(
  117. self.db,
  118. tenant_no="zkxw",
  119. user_id="u-a",
  120. can_view_all=False,
  121. app_id="",
  122. )
  123. def test_self_scope_hides_others(self):
  124. data = oa_engine.list_instance_history(
  125. self.db,
  126. tenant_no="zkxw",
  127. user_id="u-a",
  128. can_view_all=False,
  129. app_id="app-leave",
  130. )
  131. self.assertEqual(data["total"], 1)
  132. self.assertFalse(data["canViewAll"])
  133. self.assertEqual(data["list"][0]["id"], "inst-alice-done")
  134. def test_all_scope_returns_both(self):
  135. data = oa_engine.list_instance_history(
  136. self.db,
  137. tenant_no="zkxw",
  138. user_id="u-a",
  139. can_view_all=True,
  140. app_id="app-leave",
  141. )
  142. self.assertEqual(data["total"], 2)
  143. self.assertTrue(data["canViewAll"])
  144. ids = {r["id"] for r in data["list"]}
  145. self.assertEqual(ids, {"inst-alice-done", "inst-bob-run"})
  146. def test_initiator_name_ignored_without_all(self):
  147. data = oa_engine.list_instance_history(
  148. self.db,
  149. tenant_no="zkxw",
  150. user_id="u-a",
  151. can_view_all=False,
  152. app_id="app-leave",
  153. initiator_name="李四",
  154. )
  155. self.assertEqual(data["total"], 1)
  156. self.assertEqual(data["list"][0]["id"], "inst-alice-done")
  157. def test_finish_time_only_on_terminal(self):
  158. data = oa_engine.list_instance_history(
  159. self.db,
  160. tenant_no="zkxw",
  161. user_id="u-a",
  162. can_view_all=True,
  163. app_id="app-leave",
  164. )
  165. by_id = {r["id"]: r for r in data["list"]}
  166. self.assertTrue(by_id["inst-alice-done"].get("finishTime"))
  167. self.assertFalse(by_id["inst-bob-run"].get("finishTime"))
  168. def test_finish_time_filter(self):
  169. data = oa_engine.list_instance_history(
  170. self.db,
  171. tenant_no="zkxw",
  172. user_id="u-a",
  173. can_view_all=True,
  174. app_id="app-leave",
  175. finish_time_from="2026-08-20",
  176. finish_time_to="2026-08-20",
  177. )
  178. self.assertEqual(data["total"], 1)
  179. self.assertEqual(data["list"][0]["id"], "inst-alice-done")
  180. def test_preview_fields_limit_six(self):
  181. data = oa_engine.list_instance_history(
  182. self.db,
  183. tenant_no="zkxw",
  184. user_id="u-a",
  185. can_view_all=False,
  186. app_id="app-leave",
  187. )
  188. pf = data["list"][0].get("previewFields") or []
  189. self.assertIsInstance(pf, list)
  190. self.assertLessEqual(len(pf), 6)
  191. if pf:
  192. self.assertGreaterEqual(len(pf), 1)
  193. def test_preview_fields_keep_upload_files(self):
  194. data = oa_engine.list_instance_history(
  195. self.db,
  196. tenant_no="zkxw",
  197. user_id="u-a",
  198. can_view_all=False,
  199. app_id="app-leave",
  200. )
  201. pf = data["list"][0].get("previewFields") or []
  202. voucher = next((f for f in pf if f.get("label") == "付款凭证"), None)
  203. self.assertIsNotNone(voucher)
  204. self.assertEqual(voucher.get("fieldType"), "upload")
  205. self.assertEqual(
  206. voucher.get("files"),
  207. [
  208. {
  209. "url": "f33a569a9dd64f2c853899498cd47498.png",
  210. "name": "f33a569a9dd64f2c853899498cd47498.png",
  211. }
  212. ],
  213. )
  214. def test_export_history_rows(self):
  215. data = oa_engine.export_instance_history(
  216. self.db,
  217. tenant_no="zkxw",
  218. user_id="u-a",
  219. can_view_all=False,
  220. app_id="app-leave",
  221. )
  222. self.assertEqual(data["total"], 1)
  223. self.assertGreaterEqual(len(data["businessColumns"]), 1)
  224. self.assertEqual(len(data["fixedColumns"]), 13)
  225. row = data["rows"][0]
  226. self.assertEqual(row["id"], "inst-alice-done")
  227. self.assertEqual(row["business"].get("leaveType"), "年假")
  228. self.assertEqual(row["fixed"].get("initiatorName"), "张三")
  229. self.assertIn("approvalRecord", row["fixed"])
  230. def test_export_selected_ids_only(self):
  231. data = oa_engine.export_instance_history(
  232. self.db,
  233. tenant_no="zkxw",
  234. user_id="u-a",
  235. can_view_all=True,
  236. app_id="app-leave",
  237. instance_ids=["inst-bob-run"],
  238. )
  239. self.assertEqual(data["total"], 1)
  240. self.assertEqual(data["rows"][0]["id"], "inst-bob-run")
  241. if __name__ == "__main__":
  242. unittest.main()