test_oa_instance_history.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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 (
  9. OaApp,
  10. OaAppVersion,
  11. OaInstance,
  12. OaInstanceEvent,
  13. OaTask,
  14. SysDept,
  15. SysUser,
  16. )
  17. from app.services import oa_engine
  18. class TestOaInstanceHistory(unittest.TestCase):
  19. def setUp(self):
  20. self.engine = create_engine("sqlite:///:memory:")
  21. Base.metadata.create_all(
  22. self.engine,
  23. tables=[
  24. OaApp.__table__,
  25. OaAppVersion.__table__,
  26. SysUser.__table__,
  27. SysDept.__table__,
  28. OaInstance.__table__,
  29. OaTask.__table__,
  30. OaInstanceEvent.__table__,
  31. ],
  32. )
  33. Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
  34. self.db = Session()
  35. t1 = datetime(2026, 8, 10, 9, 0, 0)
  36. t2 = datetime(2026, 8, 20, 15, 30, 0)
  37. t3 = datetime(2026, 8, 25, 11, 0, 0)
  38. self.db.add_all(
  39. [
  40. OaApp(
  41. id="app-leave",
  42. app_name="请假申请",
  43. initiator_scope="all",
  44. admin_scope="all_oa_admin",
  45. status="1",
  46. tenant_no="zkxw",
  47. create_dept_id="",
  48. create_time=t1,
  49. update_time=t1,
  50. del_flag=0,
  51. ),
  52. OaAppVersion(
  53. id="v-leave",
  54. app_id="app-leave",
  55. version_no=1,
  56. status="published",
  57. is_latest_published="1",
  58. form_schema_json='{"rule":[{"type":"input","field":"leaveType","title":"请假类型"},{"type":"input","field":"days","title":"天数"},{"type":"upload","field":"voucher","title":"付款凭证"}]}',
  59. flow_tree_json="{}",
  60. advanced_settings_json="{}",
  61. tenant_no="zkxw",
  62. create_dept_id="",
  63. create_time=t1,
  64. update_time=t1,
  65. del_flag=0,
  66. ),
  67. SysUser(
  68. id="u-a",
  69. user_no="a",
  70. nick_name="张三",
  71. password="x",
  72. tenant_no="zkxw",
  73. create_dept_id="",
  74. create_time=t1,
  75. update_time=t1,
  76. del_flag=0,
  77. ),
  78. SysUser(
  79. id="u-b",
  80. user_no="b",
  81. nick_name="李四",
  82. password="x",
  83. tenant_no="zkxw",
  84. create_dept_id="",
  85. create_time=t1,
  86. update_time=t1,
  87. del_flag=0,
  88. ),
  89. SysDept(
  90. id="dept-root",
  91. parent_id="0",
  92. dept_name="总部",
  93. status="1",
  94. tenant_no="zkxw",
  95. create_dept_id="",
  96. create_time=t1,
  97. update_time=t1,
  98. del_flag=0,
  99. ),
  100. SysDept(
  101. id="dept-sales",
  102. parent_id="dept-root",
  103. dept_name="销售部",
  104. status="1",
  105. tenant_no="zkxw",
  106. create_dept_id="",
  107. create_time=t1,
  108. update_time=t1,
  109. del_flag=0,
  110. ),
  111. SysDept(
  112. id="dept-sales-east",
  113. parent_id="dept-sales",
  114. dept_name="华东销售",
  115. status="1",
  116. tenant_no="zkxw",
  117. create_dept_id="",
  118. create_time=t1,
  119. update_time=t1,
  120. del_flag=0,
  121. ),
  122. OaInstance(
  123. id="inst-alice-done",
  124. app_id="app-leave",
  125. app_version_id="v-leave",
  126. title="请假-张三",
  127. initiator_user_id="u-a",
  128. initiator_dept_id="dept-sales",
  129. form_data_json='{"leaveType":"年假","days":"2","voucher":"f33a569a9dd64f2c853899498cd47498.png"}',
  130. status="completed",
  131. tenant_no="zkxw",
  132. create_dept_id="",
  133. create_time=t1,
  134. update_time=t2,
  135. del_flag=0,
  136. ),
  137. OaInstance(
  138. id="inst-bob-run",
  139. app_id="app-leave",
  140. app_version_id="v-leave",
  141. title="请假-李四",
  142. initiator_user_id="u-b",
  143. initiator_dept_id="dept-sales-east",
  144. form_data_json='{"leaveType":"事假","days":"1"}',
  145. status="running",
  146. tenant_no="zkxw",
  147. create_dept_id="",
  148. create_time=t3,
  149. update_time=t3,
  150. del_flag=0,
  151. ),
  152. ]
  153. )
  154. self.db.commit()
  155. def tearDown(self):
  156. self.db.close()
  157. self.engine.dispose()
  158. def test_requires_app_id(self):
  159. with self.assertRaises(oa_engine.OaEngineError):
  160. oa_engine.list_instance_history(
  161. self.db,
  162. tenant_no="zkxw",
  163. user_id="u-a",
  164. can_view_all=False,
  165. app_id="",
  166. )
  167. def test_self_scope_hides_others(self):
  168. data = oa_engine.list_instance_history(
  169. self.db,
  170. tenant_no="zkxw",
  171. user_id="u-a",
  172. can_view_all=False,
  173. app_id="app-leave",
  174. )
  175. self.assertEqual(data["total"], 1)
  176. self.assertFalse(data["canViewAll"])
  177. self.assertEqual(data["list"][0]["id"], "inst-alice-done")
  178. def test_all_scope_returns_both(self):
  179. data = oa_engine.list_instance_history(
  180. self.db,
  181. tenant_no="zkxw",
  182. user_id="u-a",
  183. can_view_all=True,
  184. app_id="app-leave",
  185. )
  186. self.assertEqual(data["total"], 2)
  187. self.assertTrue(data["canViewAll"])
  188. ids = {r["id"] for r in data["list"]}
  189. self.assertEqual(ids, {"inst-alice-done", "inst-bob-run"})
  190. def test_initiator_name_ignored_without_all(self):
  191. data = oa_engine.list_instance_history(
  192. self.db,
  193. tenant_no="zkxw",
  194. user_id="u-a",
  195. can_view_all=False,
  196. app_id="app-leave",
  197. initiator_name="李四",
  198. )
  199. self.assertEqual(data["total"], 1)
  200. self.assertEqual(data["list"][0]["id"], "inst-alice-done")
  201. def test_finish_time_only_on_terminal(self):
  202. data = oa_engine.list_instance_history(
  203. self.db,
  204. tenant_no="zkxw",
  205. user_id="u-a",
  206. can_view_all=True,
  207. app_id="app-leave",
  208. )
  209. by_id = {r["id"]: r for r in data["list"]}
  210. self.assertTrue(by_id["inst-alice-done"].get("finishTime"))
  211. self.assertFalse(by_id["inst-bob-run"].get("finishTime"))
  212. def test_finish_time_filter(self):
  213. data = oa_engine.list_instance_history(
  214. self.db,
  215. tenant_no="zkxw",
  216. user_id="u-a",
  217. can_view_all=True,
  218. app_id="app-leave",
  219. finish_time_from="2026-08-20",
  220. finish_time_to="2026-08-20",
  221. )
  222. self.assertEqual(data["total"], 1)
  223. self.assertEqual(data["list"][0]["id"], "inst-alice-done")
  224. def test_preview_fields_limit_six(self):
  225. data = oa_engine.list_instance_history(
  226. self.db,
  227. tenant_no="zkxw",
  228. user_id="u-a",
  229. can_view_all=False,
  230. app_id="app-leave",
  231. )
  232. pf = data["list"][0].get("previewFields") or []
  233. self.assertIsInstance(pf, list)
  234. self.assertLessEqual(len(pf), 6)
  235. if pf:
  236. self.assertGreaterEqual(len(pf), 1)
  237. def test_preview_fields_keep_upload_files(self):
  238. data = oa_engine.list_instance_history(
  239. self.db,
  240. tenant_no="zkxw",
  241. user_id="u-a",
  242. can_view_all=False,
  243. app_id="app-leave",
  244. )
  245. pf = data["list"][0].get("previewFields") or []
  246. voucher = next((f for f in pf if f.get("label") == "付款凭证"), None)
  247. self.assertIsNotNone(voucher)
  248. self.assertEqual(voucher.get("fieldType"), "upload")
  249. self.assertEqual(
  250. voucher.get("files"),
  251. [
  252. {
  253. "url": "f33a569a9dd64f2c853899498cd47498.png",
  254. "name": "f33a569a9dd64f2c853899498cd47498.png",
  255. }
  256. ],
  257. )
  258. def test_export_history_rows(self):
  259. data = oa_engine.export_instance_history(
  260. self.db,
  261. tenant_no="zkxw",
  262. user_id="u-a",
  263. can_view_all=False,
  264. app_id="app-leave",
  265. )
  266. self.assertEqual(data["total"], 1)
  267. self.assertGreaterEqual(len(data["businessColumns"]), 1)
  268. self.assertEqual(len(data["fixedColumns"]), 13)
  269. row = data["rows"][0]
  270. self.assertEqual(row["id"], "inst-alice-done")
  271. self.assertEqual(row["business"].get("leaveType"), "年假")
  272. self.assertEqual(row["fixed"].get("initiatorName"), "张三")
  273. self.assertIn("approvalRecord", row["fixed"])
  274. def test_dept_filter_includes_descendants(self):
  275. data = oa_engine.list_instance_history(
  276. self.db,
  277. tenant_no="zkxw",
  278. user_id="u-a",
  279. can_view_all=True,
  280. app_id="app-leave",
  281. initiator_dept_id="dept-sales",
  282. )
  283. self.assertEqual(data["total"], 2)
  284. ids = {r["id"] for r in data["list"]}
  285. self.assertEqual(ids, {"inst-alice-done", "inst-bob-run"})
  286. def test_dept_filter_child_only(self):
  287. data = oa_engine.list_instance_history(
  288. self.db,
  289. tenant_no="zkxw",
  290. user_id="u-a",
  291. can_view_all=True,
  292. app_id="app-leave",
  293. initiator_dept_id="dept-sales-east",
  294. )
  295. self.assertEqual(data["total"], 1)
  296. self.assertEqual(data["list"][0]["id"], "inst-bob-run")
  297. def test_dept_filter_respects_self_scope(self):
  298. data = oa_engine.list_instance_history(
  299. self.db,
  300. tenant_no="zkxw",
  301. user_id="u-a",
  302. can_view_all=False,
  303. app_id="app-leave",
  304. initiator_dept_id="dept-sales-east",
  305. )
  306. self.assertEqual(data["total"], 0)
  307. def test_export_selected_ids_only(self):
  308. data = oa_engine.export_instance_history(
  309. self.db,
  310. tenant_no="zkxw",
  311. user_id="u-a",
  312. can_view_all=True,
  313. app_id="app-leave",
  314. instance_ids=["inst-bob-run"],
  315. )
  316. self.assertEqual(data["total"], 1)
  317. self.assertEqual(data["rows"][0]["id"], "inst-bob-run")
  318. if __name__ == "__main__":
  319. unittest.main()