oa_approval.py 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507
  1. """OA 审批管理接口(钉钉交互)"""
  2. from datetime import datetime
  3. from typing import Any, Dict, List, Optional
  4. from fastapi import APIRouter, Depends, Query
  5. from pydantic import BaseModel, ConfigDict, Field
  6. from sqlalchemy.orm import Session
  7. from app.core.database import get_db
  8. from app.core.menu_guard import require_admin_or_super_admin
  9. from app.core.response import error_response, success_response
  10. from app.core.utils import parse_datetime
  11. from app.crud import oa_approval as oa_crud
  12. from app.crud import oa_instance_draft as oid_crud
  13. from app.crud import oa_receive_account as ora_crud
  14. from app.models.models import SysUser
  15. from app.services.oa_receive_account import normalize_account_fields
  16. router = APIRouter(prefix="/web/admin/oa-approval", tags=["OA审批"])
  17. OA_START_HISTORY_ALL_PERM = "oa:start:history:all"
  18. OA_DONE_BANK_RECEIPT_PERM = "oa:done:bank-receipt"
  19. def _get_tenant_no(db: Session, user: dict) -> str:
  20. tenant = (user.get("tenant_no") or "").strip()
  21. if tenant:
  22. return tenant
  23. uid = user.get("user_id")
  24. if uid:
  25. row = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
  26. if row and row.tenant_no:
  27. return row.tenant_no
  28. return "zkxw"
  29. # ---------------------------------------------------------------------------
  30. # 收款账户
  31. # ---------------------------------------------------------------------------
  32. class OaReceiveAccountBody(BaseModel):
  33. model_config = ConfigDict(populate_by_name=True)
  34. account_type: str = Field(..., alias="accountType")
  35. account_name: str = Field(..., alias="accountName")
  36. account_no: str = Field(..., alias="accountNo")
  37. bank_name: Optional[str] = Field(None, alias="bankName")
  38. bank_province: Optional[str] = Field(None, alias="bankProvince")
  39. bank_city: Optional[str] = Field(None, alias="bankCity")
  40. bank_branch: Optional[str] = Field(None, alias="bankBranch")
  41. order_num: Optional[int] = Field(None, alias="orderNum")
  42. remark: Optional[str] = None
  43. class OaReceiveAccountBatchDeleteBody(BaseModel):
  44. model_config = ConfigDict(populate_by_name=True)
  45. ids: List[str] = Field(default_factory=list)
  46. @router.get("/receive-accounts")
  47. async def list_receive_accounts(
  48. page: int = Query(1, ge=1),
  49. pageSize: int = Query(10, ge=1, le=100),
  50. keyword: Optional[str] = None,
  51. accountType: Optional[str] = None,
  52. user: dict = Depends(require_admin_or_super_admin),
  53. db: Session = Depends(get_db),
  54. ):
  55. tenant_no = _get_tenant_no(db, user)
  56. items, total = ora_crud.list_accounts(
  57. db,
  58. tenant_no=tenant_no,
  59. keyword=keyword,
  60. account_type=accountType,
  61. page=page,
  62. page_size=pageSize,
  63. )
  64. return success_response({"list": items, "total": total})
  65. @router.get("/receive-accounts/options")
  66. async def list_receive_account_options(
  67. user: dict = Depends(require_admin_or_super_admin),
  68. db: Session = Depends(get_db),
  69. ):
  70. tenant_no = _get_tenant_no(db, user)
  71. return success_response(
  72. {
  73. "list": ora_crud.list_options(
  74. db,
  75. tenant_no=tenant_no,
  76. user_id=user.get("user_id") or "",
  77. )
  78. }
  79. )
  80. @router.post("/receive-accounts/batch-delete")
  81. async def batch_delete_receive_accounts(
  82. body: OaReceiveAccountBatchDeleteBody,
  83. user: dict = Depends(require_admin_or_super_admin),
  84. db: Session = Depends(get_db),
  85. ):
  86. tenant_no = _get_tenant_no(db, user)
  87. count = ora_crud.soft_delete_accounts(
  88. db, body.ids, tenant_no=tenant_no, user_id=user.get("user_id")
  89. )
  90. return success_response({"deletedCount": count}, message="删除成功")
  91. @router.post("/receive-accounts")
  92. async def create_receive_account(
  93. body: OaReceiveAccountBody,
  94. user: dict = Depends(require_admin_or_super_admin),
  95. db: Session = Depends(get_db),
  96. ):
  97. ok, err, fields = normalize_account_fields(body.model_dump(by_alias=True))
  98. if not ok:
  99. return error_response(400, err or "参数无效")
  100. tenant_no = _get_tenant_no(db, user)
  101. obj = ora_crud.create_account(
  102. db,
  103. tenant_no=tenant_no,
  104. user_id=user.get("user_id"),
  105. dept_id=user.get("dept_id") or "",
  106. fields=fields,
  107. )
  108. return success_response(obj, message="创建成功")
  109. @router.get("/receive-accounts/{account_id}")
  110. async def get_receive_account(
  111. account_id: str,
  112. user: dict = Depends(require_admin_or_super_admin),
  113. db: Session = Depends(get_db),
  114. ):
  115. tenant_no = _get_tenant_no(db, user)
  116. obj = ora_crud.get_account(db, account_id, tenant_no)
  117. if not obj:
  118. return error_response(404, "收款账户不存在")
  119. return success_response(obj)
  120. @router.put("/receive-accounts/{account_id}")
  121. async def update_receive_account(
  122. account_id: str,
  123. body: OaReceiveAccountBody,
  124. user: dict = Depends(require_admin_or_super_admin),
  125. db: Session = Depends(get_db),
  126. ):
  127. ok, err, fields = normalize_account_fields(body.model_dump(by_alias=True))
  128. if not ok:
  129. return error_response(400, err or "参数无效")
  130. tenant_no = _get_tenant_no(db, user)
  131. obj = ora_crud.update_account(
  132. db,
  133. account_id,
  134. tenant_no=tenant_no,
  135. user_id=user.get("user_id"),
  136. fields=fields,
  137. )
  138. if not obj:
  139. return error_response(404, "收款账户不存在")
  140. return success_response(obj, message="更新成功")
  141. @router.delete("/receive-accounts/{account_id}")
  142. async def delete_receive_account(
  143. account_id: str,
  144. user: dict = Depends(require_admin_or_super_admin),
  145. db: Session = Depends(get_db),
  146. ):
  147. tenant_no = _get_tenant_no(db, user)
  148. if not ora_crud.soft_delete_account(
  149. db, account_id, tenant_no=tenant_no, user_id=user.get("user_id")
  150. ):
  151. return error_response(404, "收款账户不存在")
  152. return success_response(None, message="删除成功")
  153. # ---------------------------------------------------------------------------
  154. # 发起审批草稿箱(oa_instance_draft)
  155. # ---------------------------------------------------------------------------
  156. class OaInstanceDraftBody(BaseModel):
  157. model_config = ConfigDict(populate_by_name=True)
  158. app_id: str = Field(..., alias="appId")
  159. form_data: Optional[Dict[str, Any]] = Field(None, alias="formData")
  160. title: Optional[str] = None
  161. initiator_dept_id: Optional[str] = Field(None, alias="initiatorDeptId")
  162. initiator_select_by_node: Optional[Dict[str, List[str]]] = Field(
  163. None, alias="initiatorSelectByNode"
  164. )
  165. class OaInstanceDraftUpdateBody(BaseModel):
  166. model_config = ConfigDict(populate_by_name=True)
  167. form_data: Optional[Dict[str, Any]] = Field(None, alias="formData")
  168. title: Optional[str] = None
  169. initiator_dept_id: Optional[str] = Field(None, alias="initiatorDeptId")
  170. initiator_select_by_node: Optional[Dict[str, List[str]]] = Field(
  171. None, alias="initiatorSelectByNode"
  172. )
  173. class OaInstanceDraftBatchDeleteBody(BaseModel):
  174. model_config = ConfigDict(populate_by_name=True)
  175. ids: List[str] = Field(default_factory=list)
  176. class OaInstanceDraftSubmitBody(BaseModel):
  177. model_config = ConfigDict(populate_by_name=True)
  178. form_data: Optional[Dict[str, Any]] = Field(None, alias="formData")
  179. title: Optional[str] = None
  180. initiator_dept_id: Optional[str] = Field(None, alias="initiatorDeptId")
  181. initiator_select_by_node: Optional[Dict[str, List[str]]] = Field(
  182. None, alias="initiatorSelectByNode"
  183. )
  184. def _parse_draft_range_end(raw: Optional[str]) -> Optional[datetime]:
  185. """截止日期:仅日期时扩到当天 23:59:59。"""
  186. if not raw or not str(raw).strip():
  187. return None
  188. s = str(raw).strip()
  189. dt = parse_datetime(s)
  190. if dt is None and len(s) == 10:
  191. try:
  192. dt = datetime.strptime(s, "%Y-%m-%d")
  193. except ValueError:
  194. return None
  195. if dt is None:
  196. return None
  197. if len(s) <= 10:
  198. return dt.replace(hour=23, minute=59, second=59)
  199. return dt
  200. def _parse_draft_range_start(raw: Optional[str]) -> Optional[datetime]:
  201. if not raw or not str(raw).strip():
  202. return None
  203. s = str(raw).strip()
  204. dt = parse_datetime(s)
  205. if dt is None and len(s) == 10:
  206. try:
  207. return datetime.strptime(s, "%Y-%m-%d")
  208. except ValueError:
  209. return None
  210. return dt
  211. @router.get("/drafts/count")
  212. async def oa_instance_draft_count(
  213. user: dict = Depends(require_admin_or_super_admin),
  214. db: Session = Depends(get_db),
  215. ):
  216. tenant_no = _get_tenant_no(db, user)
  217. n = oid_crud.count_drafts(
  218. db,
  219. tenant_no=tenant_no,
  220. owner_user_id=user.get("user_id") or "",
  221. )
  222. return success_response({"count": int(n)})
  223. @router.get("/drafts")
  224. async def list_oa_instance_drafts(
  225. page: int = Query(1, ge=1),
  226. pageSize: int = Query(20, ge=1, le=100),
  227. keyword: Optional[str] = None,
  228. groupId: Optional[str] = None,
  229. updateTimeFrom: Optional[str] = None,
  230. updateTimeTo: Optional[str] = None,
  231. user: dict = Depends(require_admin_or_super_admin),
  232. db: Session = Depends(get_db),
  233. ):
  234. tenant_no = _get_tenant_no(db, user)
  235. items, total = oid_crud.list_drafts(
  236. db,
  237. tenant_no=tenant_no,
  238. owner_user_id=user.get("user_id") or "",
  239. keyword=keyword,
  240. group_id=groupId,
  241. update_time_from=_parse_draft_range_start(updateTimeFrom),
  242. update_time_to=_parse_draft_range_end(updateTimeTo),
  243. page=page,
  244. page_size=pageSize,
  245. )
  246. return success_response({"list": items, "total": total})
  247. @router.post("/drafts")
  248. async def create_oa_instance_draft(
  249. body: OaInstanceDraftBody,
  250. user: dict = Depends(require_admin_or_super_admin),
  251. db: Session = Depends(get_db),
  252. ):
  253. try:
  254. tenant_no = _get_tenant_no(db, user)
  255. data = oid_crud.create_draft(
  256. db,
  257. tenant_no=tenant_no,
  258. owner_user_id=user.get("user_id") or "",
  259. app_id=body.app_id,
  260. form_data=body.form_data,
  261. initiator_select_by_node=body.initiator_select_by_node,
  262. initiator_dept_id=body.initiator_dept_id,
  263. title=body.title,
  264. create_dept_id=user.get("dept_id") or "",
  265. )
  266. return success_response(data, message="已存入草稿箱")
  267. except ValueError as e:
  268. return error_response(400, str(e))
  269. @router.post("/drafts/batch-delete")
  270. async def batch_delete_oa_instance_drafts(
  271. body: OaInstanceDraftBatchDeleteBody,
  272. user: dict = Depends(require_admin_or_super_admin),
  273. db: Session = Depends(get_db),
  274. ):
  275. try:
  276. tenant_no = _get_tenant_no(db, user)
  277. n = oid_crud.batch_soft_delete_drafts(
  278. db,
  279. body.ids,
  280. tenant_no=tenant_no,
  281. owner_user_id=user.get("user_id") or "",
  282. )
  283. return success_response({"deleted": n}, message="删除成功")
  284. except oid_crud.DraftAccessError as e:
  285. return error_response(403, str(e))
  286. except ValueError as e:
  287. return error_response(400, str(e))
  288. @router.get("/drafts/{draft_id}")
  289. async def get_oa_instance_draft(
  290. draft_id: str,
  291. user: dict = Depends(require_admin_or_super_admin),
  292. db: Session = Depends(get_db),
  293. ):
  294. try:
  295. tenant_no = _get_tenant_no(db, user)
  296. data = oid_crud.get_draft_detail(
  297. db,
  298. draft_id,
  299. tenant_no=tenant_no,
  300. owner_user_id=user.get("user_id") or "",
  301. )
  302. return success_response(data)
  303. except oid_crud.DraftAccessError as e:
  304. return error_response(404, str(e))
  305. @router.put("/drafts/{draft_id}")
  306. async def update_oa_instance_draft(
  307. draft_id: str,
  308. body: OaInstanceDraftUpdateBody,
  309. user: dict = Depends(require_admin_or_super_admin),
  310. db: Session = Depends(get_db),
  311. ):
  312. try:
  313. tenant_no = _get_tenant_no(db, user)
  314. data = oid_crud.update_draft(
  315. db,
  316. draft_id,
  317. tenant_no=tenant_no,
  318. owner_user_id=user.get("user_id") or "",
  319. form_data=body.form_data,
  320. initiator_select_by_node=body.initiator_select_by_node,
  321. initiator_dept_id=body.initiator_dept_id,
  322. title=body.title,
  323. )
  324. return success_response(data, message="草稿已保存")
  325. except oid_crud.DraftAccessError as e:
  326. return error_response(404, str(e))
  327. except ValueError as e:
  328. return error_response(400, str(e))
  329. @router.delete("/drafts/{draft_id}")
  330. async def delete_oa_instance_draft(
  331. draft_id: str,
  332. user: dict = Depends(require_admin_or_super_admin),
  333. db: Session = Depends(get_db),
  334. ):
  335. try:
  336. tenant_no = _get_tenant_no(db, user)
  337. oid_crud.soft_delete_draft(
  338. db,
  339. draft_id,
  340. tenant_no=tenant_no,
  341. owner_user_id=user.get("user_id") or "",
  342. )
  343. return success_response(None, message="删除成功")
  344. except oid_crud.DraftAccessError as e:
  345. return error_response(404, str(e))
  346. @router.post("/drafts/{draft_id}/submit")
  347. async def submit_oa_instance_draft(
  348. draft_id: str,
  349. body: Optional[OaInstanceDraftSubmitBody] = None,
  350. user: dict = Depends(require_admin_or_super_admin),
  351. db: Session = Depends(get_db),
  352. ):
  353. from app.services.oa_engine import OaEngineError
  354. payload = body or OaInstanceDraftSubmitBody()
  355. try:
  356. tenant_no = _get_tenant_no(db, user)
  357. data = oid_crud.submit_draft(
  358. db,
  359. draft_id,
  360. tenant_no=tenant_no,
  361. owner_user_id=user.get("user_id") or "",
  362. form_data=payload.form_data,
  363. initiator_select_by_node=payload.initiator_select_by_node,
  364. initiator_dept_id=payload.initiator_dept_id,
  365. title=payload.title,
  366. )
  367. return success_response(data, message="发起成功")
  368. except oid_crud.DraftAccessError as e:
  369. return error_response(404, str(e))
  370. except OaEngineError as e:
  371. return error_response(400, str(e))
  372. except ValueError as e:
  373. return error_response(400, str(e))
  374. # ---------------------------------------------------------------------------
  375. # 分组
  376. # ---------------------------------------------------------------------------
  377. class OaGroupCreateBody(BaseModel):
  378. model_config = ConfigDict(populate_by_name=True)
  379. group_name: str = Field(..., alias="groupName")
  380. order_num: Optional[int] = Field(None, alias="orderNum")
  381. remark: Optional[str] = None
  382. class OaGroupUpdateBody(BaseModel):
  383. model_config = ConfigDict(populate_by_name=True)
  384. group_name: Optional[str] = Field(None, alias="groupName")
  385. order_num: Optional[int] = Field(None, alias="orderNum")
  386. remark: Optional[str] = None
  387. @router.get("/groups")
  388. async def list_oa_groups(
  389. user: dict = Depends(require_admin_or_super_admin),
  390. db: Session = Depends(get_db),
  391. ):
  392. tenant_no = _get_tenant_no(db, user)
  393. return success_response({"list": oa_crud.list_groups(db, tenant_no=tenant_no)})
  394. @router.post("/groups")
  395. async def create_oa_group(
  396. body: OaGroupCreateBody,
  397. user: dict = Depends(require_admin_or_super_admin),
  398. db: Session = Depends(get_db),
  399. ):
  400. try:
  401. tenant_no = _get_tenant_no(db, user)
  402. obj = oa_crud.create_group(
  403. db,
  404. tenant_no=tenant_no,
  405. group_name=body.group_name,
  406. order_num=body.order_num,
  407. remark=body.remark,
  408. user_id=user.get("user_id"),
  409. )
  410. return success_response(oa_crud._group_to_dict(obj), message="创建成功")
  411. except ValueError as e:
  412. return error_response(400, str(e))
  413. @router.put("/groups/{group_id}")
  414. async def update_oa_group(
  415. group_id: str,
  416. body: OaGroupUpdateBody,
  417. user: dict = Depends(require_admin_or_super_admin),
  418. db: Session = Depends(get_db),
  419. ):
  420. try:
  421. tenant_no = _get_tenant_no(db, user)
  422. payload = body.model_dump(exclude_unset=True, by_alias=False)
  423. obj = oa_crud.update_group(
  424. db,
  425. group_id,
  426. tenant_no=tenant_no,
  427. group_name=payload.get("group_name"),
  428. order_num=payload.get("order_num"),
  429. remark=payload.get("remark"),
  430. user_id=user.get("user_id"),
  431. )
  432. return success_response(oa_crud._group_to_dict(obj), message="更新成功")
  433. except ValueError as e:
  434. return error_response(400, str(e))
  435. @router.delete("/groups/{group_id}")
  436. async def delete_oa_group(
  437. group_id: str,
  438. user: dict = Depends(require_admin_or_super_admin),
  439. db: Session = Depends(get_db),
  440. ):
  441. try:
  442. tenant_no = _get_tenant_no(db, user)
  443. oa_crud.delete_group(db, group_id, tenant_no=tenant_no, user_id=user.get("user_id"))
  444. return success_response(None, message="删除成功")
  445. except ValueError as e:
  446. return error_response(400, str(e))
  447. # ---------------------------------------------------------------------------
  448. # 应用 / 草稿
  449. # ---------------------------------------------------------------------------
  450. class OaAppCreateBody(BaseModel):
  451. model_config = ConfigDict(populate_by_name=True)
  452. app_name: str = Field(..., alias="appName")
  453. group_id: Optional[str] = Field(None, alias="groupId")
  454. icon_url: Optional[str] = Field(None, alias="iconUrl")
  455. description: Optional[str] = None
  456. initiator_scope: Optional[str] = Field("all", alias="initiatorScope")
  457. initiator_user_ids: Optional[List[str]] = Field(None, alias="initiatorUserIds")
  458. admin_scope: Optional[str] = Field("all_oa_admin", alias="adminScope")
  459. admin_user_ids: Optional[List[str]] = Field(None, alias="adminUserIds")
  460. status: Optional[str] = "1"
  461. order_num: Optional[int] = Field(None, alias="orderNum")
  462. remark: Optional[str] = None
  463. class OaAppUpdateBody(BaseModel):
  464. model_config = ConfigDict(populate_by_name=True)
  465. app_name: Optional[str] = Field(None, alias="appName")
  466. group_id: Optional[str] = Field(None, alias="groupId")
  467. icon_url: Optional[str] = Field(None, alias="iconUrl")
  468. description: Optional[str] = None
  469. initiator_scope: Optional[str] = Field(None, alias="initiatorScope")
  470. initiator_user_ids: Optional[List[str]] = Field(None, alias="initiatorUserIds")
  471. admin_scope: Optional[str] = Field(None, alias="adminScope")
  472. admin_user_ids: Optional[List[str]] = Field(None, alias="adminUserIds")
  473. status: Optional[str] = None
  474. order_num: Optional[int] = Field(None, alias="orderNum")
  475. remark: Optional[str] = None
  476. class OaDraftSaveBody(BaseModel):
  477. model_config = ConfigDict(populate_by_name=True)
  478. basic: Optional[Dict[str, Any]] = None
  479. form_schema_json: Optional[Any] = Field(None, alias="formSchemaJson")
  480. flow_tree_json: Optional[Any] = Field(None, alias="flowTreeJson")
  481. advanced_settings_json: Optional[Any] = Field(None, alias="advancedSettingsJson")
  482. @router.get("/apps")
  483. async def list_oa_apps(
  484. groupId: Optional[str] = None,
  485. keyword: Optional[str] = None,
  486. status: Optional[str] = None,
  487. user: dict = Depends(require_admin_or_super_admin),
  488. db: Session = Depends(get_db),
  489. ):
  490. tenant_no = _get_tenant_no(db, user)
  491. items = oa_crud.list_apps(
  492. db, tenant_no=tenant_no, group_id=groupId, keyword=keyword, status=status
  493. )
  494. return success_response({"list": items})
  495. @router.post("/apps")
  496. async def create_oa_app(
  497. body: OaAppCreateBody,
  498. user: dict = Depends(require_admin_or_super_admin),
  499. db: Session = Depends(get_db),
  500. ):
  501. try:
  502. tenant_no = _get_tenant_no(db, user)
  503. obj = oa_crud.create_app(
  504. db,
  505. tenant_no=tenant_no,
  506. app_name=body.app_name,
  507. group_id=body.group_id,
  508. icon_url=body.icon_url,
  509. description=body.description,
  510. initiator_scope=body.initiator_scope or "all",
  511. initiator_user_ids=body.initiator_user_ids,
  512. admin_scope=body.admin_scope or "all_oa_admin",
  513. admin_user_ids=body.admin_user_ids,
  514. status=body.status or "1",
  515. order_num=body.order_num,
  516. remark=body.remark,
  517. user_id=user.get("user_id"),
  518. )
  519. group_name = ""
  520. if obj.group_id:
  521. g = oa_crud.get_group(db, obj.group_id, tenant_no)
  522. group_name = g.group_name if g else ""
  523. return success_response(
  524. oa_crud._app_to_dict(obj, group_name=group_name, has_draft=True),
  525. message="创建成功",
  526. )
  527. except ValueError as e:
  528. return error_response(400, str(e))
  529. @router.post("/apps/{app_id}/copy")
  530. async def copy_oa_app(
  531. app_id: str,
  532. user: dict = Depends(require_admin_or_super_admin),
  533. db: Session = Depends(get_db),
  534. ):
  535. try:
  536. tenant_no = _get_tenant_no(db, user)
  537. obj = oa_crud.copy_app(
  538. db, app_id, tenant_no=tenant_no, user_id=user.get("user_id")
  539. )
  540. group_name = ""
  541. if obj.group_id:
  542. g = oa_crud.get_group(db, obj.group_id, tenant_no)
  543. group_name = g.group_name if g else ""
  544. return success_response(
  545. oa_crud._app_to_dict(obj, group_name=group_name, has_draft=True),
  546. message="复制成功",
  547. )
  548. except ValueError as e:
  549. return error_response(400, str(e))
  550. @router.put("/apps/{app_id}")
  551. async def update_oa_app(
  552. app_id: str,
  553. body: OaAppUpdateBody,
  554. user: dict = Depends(require_admin_or_super_admin),
  555. db: Session = Depends(get_db),
  556. ):
  557. try:
  558. tenant_no = _get_tenant_no(db, user)
  559. payload = body.model_dump(exclude_unset=True, by_alias=False)
  560. obj = oa_crud.update_app(db, app_id, tenant_no=tenant_no, user_id=user.get("user_id"), **payload)
  561. group_name = ""
  562. if obj.group_id:
  563. g = oa_crud.get_group(db, obj.group_id, tenant_no)
  564. group_name = g.group_name if g else ""
  565. return success_response(oa_crud._app_to_dict(obj, group_name=group_name), message="更新成功")
  566. except ValueError as e:
  567. return error_response(400, str(e))
  568. @router.delete("/apps/{app_id}")
  569. async def delete_oa_app(
  570. app_id: str,
  571. user: dict = Depends(require_admin_or_super_admin),
  572. db: Session = Depends(get_db),
  573. ):
  574. try:
  575. tenant_no = _get_tenant_no(db, user)
  576. oa_crud.delete_app(db, app_id, tenant_no=tenant_no, user_id=user.get("user_id"))
  577. return success_response(None, message="删除成功")
  578. except ValueError as e:
  579. return error_response(400, str(e))
  580. @router.get("/apps/{app_id}/draft")
  581. async def get_oa_app_draft(
  582. app_id: str,
  583. user: dict = Depends(require_admin_or_super_admin),
  584. db: Session = Depends(get_db),
  585. ):
  586. try:
  587. tenant_no = _get_tenant_no(db, user)
  588. data = oa_crud.get_or_create_draft(
  589. db, app_id, tenant_no=tenant_no, user_id=user.get("user_id")
  590. )
  591. return success_response(data)
  592. except ValueError as e:
  593. return error_response(400, str(e))
  594. @router.put("/apps/{app_id}/draft")
  595. async def save_oa_app_draft(
  596. app_id: str,
  597. body: OaDraftSaveBody,
  598. user: dict = Depends(require_admin_or_super_admin),
  599. db: Session = Depends(get_db),
  600. ):
  601. try:
  602. tenant_no = _get_tenant_no(db, user)
  603. data = oa_crud.save_draft(
  604. db,
  605. app_id,
  606. tenant_no=tenant_no,
  607. basic=body.basic,
  608. form_schema_json=body.form_schema_json,
  609. flow_tree_json=body.flow_tree_json,
  610. advanced_settings_json=body.advanced_settings_json,
  611. user_id=user.get("user_id"),
  612. )
  613. return success_response(data, message="草稿已保存")
  614. except ValueError as e:
  615. return error_response(400, str(e))
  616. @router.post("/apps/{app_id}/publish")
  617. async def publish_oa_app(
  618. app_id: str,
  619. user: dict = Depends(require_admin_or_super_admin),
  620. db: Session = Depends(get_db),
  621. ):
  622. from app.services.oa_publish import publish_app
  623. tenant_no = _get_tenant_no(db, user)
  624. data, errors = publish_app(
  625. db, app_id, tenant_no=tenant_no, user_id=user.get("user_id")
  626. )
  627. if errors:
  628. return error_response(400, "发布校验未通过", data={"errors": errors})
  629. return success_response(data, message="发布成功")
  630. # ---------------------------------------------------------------------------
  631. # 运行时:模板 / 实例 / 任务
  632. # ---------------------------------------------------------------------------
  633. class OaStartInstanceBody(BaseModel):
  634. model_config = ConfigDict(populate_by_name=True)
  635. app_id: str = Field(..., alias="appId")
  636. form_data: Optional[Dict[str, Any]] = Field(None, alias="formData")
  637. title: Optional[str] = None
  638. initiator_dept_id: Optional[str] = Field(None, alias="initiatorDeptId")
  639. initiator_select_by_node: Optional[Dict[str, List[str]]] = Field(
  640. None, alias="initiatorSelectByNode"
  641. )
  642. related_instance_ids: Optional[List[str]] = Field(None, alias="relatedInstanceIds")
  643. source_instance_id: Optional[str] = Field(None, alias="sourceInstanceId")
  644. class OaFlowPreviewBody(BaseModel):
  645. model_config = ConfigDict(populate_by_name=True)
  646. form_data: Optional[Dict[str, Any]] = Field(None, alias="formData")
  647. initiator_select_by_node: Optional[Dict[str, List[str]]] = Field(
  648. None, alias="initiatorSelectByNode"
  649. )
  650. class OaTaskActionBody(BaseModel):
  651. model_config = ConfigDict(populate_by_name=True)
  652. comment: Optional[str] = None
  653. class OaCcReadBatchBody(BaseModel):
  654. model_config = ConfigDict(populate_by_name=True)
  655. ids: List[str] = Field(default_factory=list)
  656. class OaAddSignBody(BaseModel):
  657. model_config = ConfigDict(populate_by_name=True)
  658. user_ids: List[str] = Field(default_factory=list, alias="userIds")
  659. comment: Optional[str] = None
  660. class OaTransferBody(BaseModel):
  661. model_config = ConfigDict(populate_by_name=True)
  662. target_user_id: str = Field(..., alias="targetUserId")
  663. comment: Optional[str] = None
  664. class OaCommentCreateBody(BaseModel):
  665. model_config = ConfigDict(populate_by_name=True)
  666. content: Optional[str] = ""
  667. attachment_refs: Optional[List[Dict[str, Any]]] = Field(
  668. None, alias="attachmentRefs"
  669. )
  670. mention_user_ids: Optional[List[str]] = Field(None, alias="mentionUserIds")
  671. class OaBankReceiptItemBody(BaseModel):
  672. model_config = ConfigDict(populate_by_name=True)
  673. file_url: str = Field(..., alias="fileUrl")
  674. file_name: Optional[str] = Field(None, alias="fileName")
  675. file_type: Optional[str] = Field(None, alias="fileType")
  676. file_size: Optional[int] = Field(None, alias="fileSize")
  677. class OaBankReceiptReplaceBody(BaseModel):
  678. model_config = ConfigDict(populate_by_name=True)
  679. items: List[OaBankReceiptItemBody] = Field(default_factory=list)
  680. class OaResubmitBody(BaseModel):
  681. model_config = ConfigDict(populate_by_name=True)
  682. form_data: Optional[Dict[str, Any]] = Field(None, alias="formData")
  683. initiator_select_by_node: Optional[Dict[str, List[str]]] = Field(
  684. None, alias="initiatorSelectByNode"
  685. )
  686. @router.get("/templates")
  687. async def list_oa_templates(
  688. user: dict = Depends(require_admin_or_super_admin),
  689. db: Session = Depends(get_db),
  690. ):
  691. from app.services import oa_engine
  692. tenant_no = _get_tenant_no(db, user)
  693. items = oa_engine.list_templates(db, tenant_no=tenant_no, user_id=user.get("user_id") or "")
  694. return success_response({"list": items})
  695. @router.get("/templates/{app_id}")
  696. async def get_oa_template(
  697. app_id: str,
  698. user: dict = Depends(require_admin_or_super_admin),
  699. db: Session = Depends(get_db),
  700. ):
  701. from app.services.oa_engine import OaEngineError, get_template_detail
  702. try:
  703. tenant_no = _get_tenant_no(db, user)
  704. data = get_template_detail(
  705. db,
  706. tenant_no=tenant_no,
  707. app_id=app_id,
  708. user_id=user.get("user_id") or "",
  709. )
  710. return success_response(data)
  711. except OaEngineError as e:
  712. return error_response(400, str(e))
  713. @router.post("/templates/{app_id}/flow-preview")
  714. async def preview_oa_template_flow(
  715. app_id: str,
  716. body: OaFlowPreviewBody,
  717. user: dict = Depends(require_admin_or_super_admin),
  718. db: Session = Depends(get_db),
  719. ):
  720. """发起前预览审批链(条件齐后解析最终路径与审批人)。"""
  721. from app.services.oa_engine import OaEngineError, preview_start_flow
  722. try:
  723. tenant_no = _get_tenant_no(db, user)
  724. uid = user.get("user_id") or ""
  725. dept_id = None
  726. if uid:
  727. row = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
  728. if row:
  729. dept_id = row.dept_id
  730. data = preview_start_flow(
  731. db,
  732. tenant_no=tenant_no,
  733. app_id=app_id,
  734. initiator_user_id=uid,
  735. initiator_dept_id=dept_id,
  736. initiator_select_by_node=body.initiator_select_by_node,
  737. form_data=body.form_data,
  738. )
  739. return success_response(data)
  740. except OaEngineError as e:
  741. return error_response(400, str(e))
  742. @router.get("/templates/{app_id}/flow-preview")
  743. async def preview_oa_template_flow_get(
  744. app_id: str,
  745. user: dict = Depends(require_admin_or_super_admin),
  746. db: Session = Depends(get_db),
  747. ):
  748. """兼容旧 GET:无 formData,含条件分支时多为待确定占位。"""
  749. from app.services.oa_engine import OaEngineError, preview_start_flow
  750. try:
  751. tenant_no = _get_tenant_no(db, user)
  752. uid = user.get("user_id") or ""
  753. dept_id = None
  754. if uid:
  755. row = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
  756. if row:
  757. dept_id = row.dept_id
  758. data = preview_start_flow(
  759. db,
  760. tenant_no=tenant_no,
  761. app_id=app_id,
  762. initiator_user_id=uid,
  763. initiator_dept_id=dept_id,
  764. initiator_select_by_node=None,
  765. form_data=None,
  766. )
  767. return success_response(data)
  768. except OaEngineError as e:
  769. return error_response(400, str(e))
  770. @router.post("/instances")
  771. async def start_oa_instance(
  772. body: OaStartInstanceBody,
  773. user: dict = Depends(require_admin_or_super_admin),
  774. db: Session = Depends(get_db),
  775. ):
  776. from app.services.oa_engine import OaEngineError, start_instance
  777. try:
  778. tenant_no = _get_tenant_no(db, user)
  779. data = start_instance(
  780. db,
  781. tenant_no=tenant_no,
  782. app_id=body.app_id,
  783. initiator_user_id=user.get("user_id") or "",
  784. initiator_dept_id=body.initiator_dept_id,
  785. form_data=body.form_data,
  786. title=body.title,
  787. initiator_select_by_node=body.initiator_select_by_node,
  788. related_instance_ids=body.related_instance_ids,
  789. source_instance_id=body.source_instance_id,
  790. )
  791. return success_response(data, message="发起成功")
  792. except OaEngineError as e:
  793. return error_response(400, str(e))
  794. @router.get("/instances/history")
  795. async def list_oa_instance_history(
  796. appId: str = Query(..., description="流程应用 ID"),
  797. instanceId: str = Query(""),
  798. instanceStatus: str = Query(""),
  799. initiatorName: str = Query(""),
  800. applyTimeFrom: str = Query(""),
  801. applyTimeTo: str = Query(""),
  802. finishTimeFrom: str = Query(""),
  803. finishTimeTo: str = Query(""),
  804. initiatorDeptId: str = Query(""),
  805. page: int = Query(1),
  806. pageSize: int = Query(20, alias="pageSize"),
  807. user: dict = Depends(require_admin_or_super_admin),
  808. db: Session = Depends(get_db),
  809. ):
  810. from app.core.menu_guard import user_has_menu_perm
  811. from app.services.oa_engine import OaEngineError, list_instance_history
  812. try:
  813. tenant_no = _get_tenant_no(db, user)
  814. can_view_all = user_has_menu_perm(db, user, OA_START_HISTORY_ALL_PERM)
  815. data = list_instance_history(
  816. db,
  817. tenant_no=tenant_no,
  818. user_id=user.get("user_id") or "",
  819. can_view_all=can_view_all,
  820. app_id=appId,
  821. instance_id=instanceId or None,
  822. initiator_name=initiatorName or None,
  823. apply_time_from=applyTimeFrom or None,
  824. apply_time_to=applyTimeTo or None,
  825. finish_time_from=finishTimeFrom or None,
  826. finish_time_to=finishTimeTo or None,
  827. instance_status=instanceStatus or None,
  828. initiator_dept_id=initiatorDeptId or None,
  829. page=page,
  830. page_size=pageSize,
  831. )
  832. return success_response(data)
  833. except OaEngineError as e:
  834. return error_response(400, str(e))
  835. @router.get("/instances/history/export")
  836. async def export_oa_instance_history(
  837. appId: str = Query(..., description="流程应用 ID"),
  838. instanceId: str = Query(""),
  839. instanceIds: str = Query("", description="逗号分隔的实例 ID,用于导出勾选"),
  840. instanceStatus: str = Query(""),
  841. initiatorName: str = Query(""),
  842. applyTimeFrom: str = Query(""),
  843. applyTimeTo: str = Query(""),
  844. finishTimeFrom: str = Query(""),
  845. finishTimeTo: str = Query(""),
  846. initiatorDeptId: str = Query(""),
  847. user: dict = Depends(require_admin_or_super_admin),
  848. db: Session = Depends(get_db),
  849. ):
  850. from app.core.menu_guard import user_has_menu_perm
  851. from app.services.oa_engine import OaEngineError, export_instance_history
  852. try:
  853. tenant_no = _get_tenant_no(db, user)
  854. can_view_all = user_has_menu_perm(db, user, OA_START_HISTORY_ALL_PERM)
  855. id_list = [x.strip() for x in (instanceIds or "").split(",") if x.strip()]
  856. data = export_instance_history(
  857. db,
  858. tenant_no=tenant_no,
  859. user_id=user.get("user_id") or "",
  860. can_view_all=can_view_all,
  861. app_id=appId,
  862. instance_id=instanceId or None,
  863. instance_ids=id_list or None,
  864. initiator_name=initiatorName or None,
  865. apply_time_from=applyTimeFrom or None,
  866. apply_time_to=applyTimeTo or None,
  867. finish_time_from=finishTimeFrom or None,
  868. finish_time_to=finishTimeTo or None,
  869. instance_status=instanceStatus or None,
  870. initiator_dept_id=initiatorDeptId or None,
  871. )
  872. return success_response(data)
  873. except OaEngineError as e:
  874. return error_response(400, str(e))
  875. @router.get("/instances/{instance_id}")
  876. async def get_oa_instance(
  877. instance_id: str,
  878. user: dict = Depends(require_admin_or_super_admin),
  879. db: Session = Depends(get_db),
  880. ):
  881. from app.services.oa_engine import OaEngineError, get_instance_detail
  882. try:
  883. tenant_no = _get_tenant_no(db, user)
  884. return success_response(
  885. get_instance_detail(
  886. db,
  887. instance_id,
  888. tenant_no=tenant_no,
  889. viewer_user_id=user.get("user_id") or "",
  890. )
  891. )
  892. except OaEngineError as e:
  893. return error_response(400, str(e))
  894. @router.get("/instances/{instance_id}/form-attachments")
  895. async def list_oa_instance_form_attachments(
  896. instance_id: str,
  897. user: dict = Depends(require_admin_or_super_admin),
  898. db: Session = Depends(get_db),
  899. ):
  900. from app.services import oa_instance_comment as oa_comment
  901. from app.services.oa_engine import OaEngineError
  902. try:
  903. tenant_no = _get_tenant_no(db, user)
  904. items = oa_comment.list_form_attachments(
  905. db, instance_id=instance_id, tenant_no=tenant_no
  906. )
  907. return success_response({"items": items})
  908. except OaEngineError as e:
  909. return error_response(400, str(e))
  910. @router.get("/instances/{instance_id}/bank-receipts")
  911. async def list_oa_bank_receipts(
  912. instance_id: str,
  913. user: dict = Depends(require_admin_or_super_admin),
  914. db: Session = Depends(get_db),
  915. ):
  916. from app.core.menu_guard import user_has_menu_perm
  917. from app.services import oa_bank_receipt as br
  918. if not user_has_menu_perm(db, user, OA_DONE_BANK_RECEIPT_PERM):
  919. return error_response(403, "无权上传银行回执单")
  920. try:
  921. tenant_no = _get_tenant_no(db, user)
  922. items = br.list_bank_receipts(db, instance_id=instance_id, tenant_no=tenant_no)
  923. return success_response({"items": items})
  924. except br.OaBankReceiptError as e:
  925. return error_response(400, str(e))
  926. @router.put("/instances/{instance_id}/bank-receipts")
  927. async def replace_oa_bank_receipts(
  928. instance_id: str,
  929. body: OaBankReceiptReplaceBody,
  930. user: dict = Depends(require_admin_or_super_admin),
  931. db: Session = Depends(get_db),
  932. ):
  933. from app.core.menu_guard import user_has_menu_perm
  934. from app.services import oa_bank_receipt as br
  935. if not user_has_menu_perm(db, user, OA_DONE_BANK_RECEIPT_PERM):
  936. return error_response(403, "无权上传银行回执单")
  937. try:
  938. tenant_no = _get_tenant_no(db, user)
  939. items = br.replace_bank_receipts(
  940. db,
  941. instance_id=instance_id,
  942. tenant_no=tenant_no,
  943. items=[it.model_dump(by_alias=True) for it in (body.items or [])],
  944. user_id=user.get("user_id") or "",
  945. dept_id=user.get("dept_id") or "",
  946. )
  947. db.commit()
  948. return success_response({"items": items})
  949. except br.OaBankReceiptError as e:
  950. db.rollback()
  951. return error_response(400, str(e))
  952. except Exception:
  953. db.rollback()
  954. raise
  955. @router.get("/instances/{instance_id}/comments")
  956. async def list_oa_instance_comments(
  957. instance_id: str,
  958. user: dict = Depends(require_admin_or_super_admin),
  959. db: Session = Depends(get_db),
  960. ):
  961. from app.services import oa_instance_comment as oa_comment
  962. from app.services.oa_engine import OaEngineError
  963. try:
  964. tenant_no = _get_tenant_no(db, user)
  965. items = oa_comment.list_comments(
  966. db,
  967. instance_id=instance_id,
  968. tenant_no=tenant_no,
  969. viewer_user_id=user.get("user_id") or "",
  970. )
  971. return success_response({"list": items})
  972. except OaEngineError as e:
  973. return error_response(400, str(e))
  974. @router.post("/instances/{instance_id}/comments")
  975. async def create_oa_instance_comment(
  976. instance_id: str,
  977. body: OaCommentCreateBody,
  978. user: dict = Depends(require_admin_or_super_admin),
  979. db: Session = Depends(get_db),
  980. ):
  981. from app.services import oa_instance_comment as oa_comment
  982. from app.services.oa_engine import OaEngineError
  983. try:
  984. tenant_no = _get_tenant_no(db, user)
  985. data = oa_comment.create_comment(
  986. db,
  987. instance_id=instance_id,
  988. tenant_no=tenant_no,
  989. user_id=user.get("user_id") or "",
  990. dept_id=user.get("dept_id") or "",
  991. content=body.content,
  992. attachment_refs=body.attachment_refs,
  993. mention_user_ids=body.mention_user_ids,
  994. )
  995. return success_response(data)
  996. except OaEngineError as e:
  997. return error_response(400, str(e))
  998. @router.post("/instances/{instance_id}/comments/{comment_id}/revoke")
  999. async def revoke_oa_instance_comment(
  1000. instance_id: str,
  1001. comment_id: str,
  1002. user: dict = Depends(require_admin_or_super_admin),
  1003. db: Session = Depends(get_db),
  1004. ):
  1005. from app.services import oa_instance_comment as oa_comment
  1006. from app.services.oa_engine import OaEngineError
  1007. try:
  1008. tenant_no = _get_tenant_no(db, user)
  1009. data = oa_comment.revoke_comment(
  1010. db,
  1011. instance_id=instance_id,
  1012. comment_id=comment_id,
  1013. tenant_no=tenant_no,
  1014. user_id=user.get("user_id") or "",
  1015. )
  1016. return success_response(data)
  1017. except OaEngineError as e:
  1018. return error_response(400, str(e))
  1019. @router.get("/related-candidates")
  1020. async def list_related_candidates(
  1021. keyword: str = Query(""),
  1022. tab: str = Query("mine"),
  1023. page: int = Query(1),
  1024. pageSize: int = Query(20, alias="pageSize"),
  1025. user: dict = Depends(require_admin_or_super_admin),
  1026. db: Session = Depends(get_db),
  1027. ):
  1028. from app.services import oa_related_approval as rel
  1029. from app.services.oa_engine import OaEngineError
  1030. tenant_no = _get_tenant_no(db, user)
  1031. try:
  1032. data = rel.list_related_candidates(
  1033. db,
  1034. tenant_no=tenant_no,
  1035. user_id=user.get("user_id") or "",
  1036. tab=tab,
  1037. keyword=keyword,
  1038. page=page,
  1039. page_size=pageSize,
  1040. )
  1041. except OaEngineError as e:
  1042. return error_response(400, str(e))
  1043. return success_response(data)
  1044. @router.get("/related-summaries")
  1045. async def get_related_summaries(
  1046. ids: str = Query("", description="comma-separated instance ids"),
  1047. mode: str = Query("picker"),
  1048. user: dict = Depends(require_admin_or_super_admin),
  1049. db: Session = Depends(get_db),
  1050. ):
  1051. from app.services import oa_related_approval as rel
  1052. from app.services.oa_engine import OaEngineError
  1053. tenant_no = _get_tenant_no(db, user)
  1054. id_list = [x.strip() for x in (ids or "").split(",") if x.strip()]
  1055. try:
  1056. items = rel.build_related_summaries(
  1057. db,
  1058. tenant_no=tenant_no,
  1059. user_id=user.get("user_id") or "",
  1060. ids=id_list,
  1061. mode=mode,
  1062. )
  1063. except OaEngineError as e:
  1064. return error_response(400, str(e))
  1065. return success_response({"list": items})
  1066. @router.get("/tasks/pending/count")
  1067. async def oa_pending_task_count(
  1068. user: dict = Depends(require_admin_or_super_admin),
  1069. db: Session = Depends(get_db),
  1070. ):
  1071. from app.services.oa_engine import OaEngineError, count_pending_tasks
  1072. try:
  1073. tenant_no = _get_tenant_no(db, user)
  1074. n = count_pending_tasks(
  1075. db, tenant_no=tenant_no, user_id=user.get("user_id") or ""
  1076. )
  1077. return success_response({"count": int(n)})
  1078. except OaEngineError as e:
  1079. return error_response(400, str(e))
  1080. @router.get("/tasks/cc/unread-count")
  1081. async def oa_cc_unread_count(
  1082. user: dict = Depends(require_admin_or_super_admin),
  1083. db: Session = Depends(get_db),
  1084. ):
  1085. from app.services.oa_engine import OaEngineError, count_cc_unread
  1086. try:
  1087. tenant_no = _get_tenant_no(db, user)
  1088. n = count_cc_unread(
  1089. db, tenant_no=tenant_no, user_id=user.get("user_id") or ""
  1090. )
  1091. return success_response({"count": int(n)})
  1092. except OaEngineError as e:
  1093. return error_response(400, str(e))
  1094. @router.post("/tasks/cc/read-batch")
  1095. async def oa_cc_read_batch(
  1096. body: OaCcReadBatchBody,
  1097. user: dict = Depends(require_admin_or_super_admin),
  1098. db: Session = Depends(get_db),
  1099. ):
  1100. from app.services.oa_engine import OaEngineError, mark_cc_tasks_read_batch
  1101. try:
  1102. tenant_no = _get_tenant_no(db, user)
  1103. data = mark_cc_tasks_read_batch(
  1104. db,
  1105. tenant_no=tenant_no,
  1106. user_id=user.get("user_id") or "",
  1107. ids=body.ids or [],
  1108. )
  1109. return success_response(data, message="已标记已读")
  1110. except OaEngineError as e:
  1111. return error_response(400, str(e))
  1112. @router.post("/tasks/{task_id}/read")
  1113. async def oa_task_mark_read(
  1114. task_id: str,
  1115. user: dict = Depends(require_admin_or_super_admin),
  1116. db: Session = Depends(get_db),
  1117. ):
  1118. from app.services.oa_engine import OaEngineError, mark_cc_task_read
  1119. try:
  1120. tenant_no = _get_tenant_no(db, user)
  1121. data = mark_cc_task_read(
  1122. db,
  1123. tenant_no=tenant_no,
  1124. user_id=user.get("user_id") or "",
  1125. task_id=task_id,
  1126. )
  1127. return success_response(data, message="已标记已读")
  1128. except OaEngineError as e:
  1129. return error_response(400, str(e))
  1130. @router.get("/tasks/{kind}")
  1131. async def list_oa_tasks(
  1132. kind: str,
  1133. readStatus: Optional[str] = Query(None, alias="readStatus"),
  1134. appName: Optional[str] = Query(None, alias="appName"),
  1135. initiatorName: Optional[str] = Query(None, alias="initiatorName"),
  1136. keyword: Optional[str] = Query(None, alias="keyword"),
  1137. applyTimeFrom: Optional[str] = Query(None, alias="applyTimeFrom"),
  1138. applyTimeTo: Optional[str] = Query(None, alias="applyTimeTo"),
  1139. instanceStatus: Optional[str] = Query(None, alias="instanceStatus"),
  1140. page: int = Query(1, ge=1),
  1141. pageSize: int = Query(20, ge=1, le=100),
  1142. user: dict = Depends(require_admin_or_super_admin),
  1143. db: Session = Depends(get_db),
  1144. ):
  1145. from app.services.oa_engine import OaEngineError, list_tasks
  1146. try:
  1147. tenant_no = _get_tenant_no(db, user)
  1148. data = list_tasks(
  1149. db,
  1150. tenant_no=tenant_no,
  1151. user_id=user.get("user_id") or "",
  1152. kind=kind,
  1153. read_status=readStatus,
  1154. app_name=appName,
  1155. initiator_name=initiatorName,
  1156. keyword=keyword,
  1157. apply_time_from=applyTimeFrom,
  1158. apply_time_to=applyTimeTo,
  1159. instance_status=instanceStatus,
  1160. page=page,
  1161. page_size=pageSize,
  1162. )
  1163. return success_response({"list": data.get("list") or [], "total": int(data.get("total") or 0)})
  1164. except OaEngineError as e:
  1165. return error_response(400, str(e))
  1166. @router.post("/tasks/{task_id}/approve")
  1167. async def approve_oa_task(
  1168. task_id: str,
  1169. body: Optional[OaTaskActionBody] = None,
  1170. user: dict = Depends(require_admin_or_super_admin),
  1171. db: Session = Depends(get_db),
  1172. ):
  1173. from app.services.oa_engine import OaEngineError, complete_task
  1174. try:
  1175. tenant_no = _get_tenant_no(db, user)
  1176. data = complete_task(
  1177. db,
  1178. task_id,
  1179. tenant_no=tenant_no,
  1180. user_id=user.get("user_id") or "",
  1181. action="approve",
  1182. comment=(body.comment if body else None),
  1183. )
  1184. return success_response(data, message="已同意")
  1185. except OaEngineError as e:
  1186. return error_response(400, str(e))
  1187. @router.post("/tasks/{task_id}/reject")
  1188. async def reject_oa_task(
  1189. task_id: str,
  1190. body: Optional[OaTaskActionBody] = None,
  1191. user: dict = Depends(require_admin_or_super_admin),
  1192. db: Session = Depends(get_db),
  1193. ):
  1194. from app.services.oa_engine import OaEngineError, complete_task
  1195. try:
  1196. tenant_no = _get_tenant_no(db, user)
  1197. data = complete_task(
  1198. db,
  1199. task_id,
  1200. tenant_no=tenant_no,
  1201. user_id=user.get("user_id") or "",
  1202. action="reject",
  1203. comment=(body.comment if body else None),
  1204. )
  1205. return success_response(data, message="已拒绝")
  1206. except OaEngineError as e:
  1207. return error_response(400, str(e))
  1208. @router.post("/tasks/{task_id}/return")
  1209. async def return_oa_task(
  1210. task_id: str,
  1211. body: Optional[OaTaskActionBody] = None,
  1212. user: dict = Depends(require_admin_or_super_admin),
  1213. db: Session = Depends(get_db),
  1214. ):
  1215. from app.services.oa_engine import OaEngineError, return_task
  1216. try:
  1217. tenant_no = _get_tenant_no(db, user)
  1218. data = return_task(
  1219. db,
  1220. task_id,
  1221. tenant_no=tenant_no,
  1222. user_id=user.get("user_id") or "",
  1223. comment=(body.comment if body else None),
  1224. )
  1225. return success_response(data, message="已打回")
  1226. except OaEngineError as e:
  1227. return error_response(400, str(e))
  1228. @router.post("/instances/{instance_id}/resubmit")
  1229. async def resubmit_oa_instance(
  1230. instance_id: str,
  1231. body: Optional[OaResubmitBody] = None,
  1232. user: dict = Depends(require_admin_or_super_admin),
  1233. db: Session = Depends(get_db),
  1234. ):
  1235. from app.services.oa_engine import OaEngineError, resubmit_instance
  1236. try:
  1237. tenant_no = _get_tenant_no(db, user)
  1238. data = resubmit_instance(
  1239. db,
  1240. instance_id,
  1241. tenant_no=tenant_no,
  1242. user_id=user.get("user_id") or "",
  1243. form_data=(body.form_data if body else None),
  1244. initiator_select_by_node=(body.initiator_select_by_node if body else None),
  1245. )
  1246. return success_response(data, message="已再次提交")
  1247. except OaEngineError as e:
  1248. return error_response(400, str(e))
  1249. @router.post("/tasks/{task_id}/add-sign")
  1250. async def add_sign_oa_task(
  1251. task_id: str,
  1252. body: OaAddSignBody,
  1253. user: dict = Depends(require_admin_or_super_admin),
  1254. db: Session = Depends(get_db),
  1255. ):
  1256. from app.services.oa_engine import OaEngineError, add_sign
  1257. try:
  1258. tenant_no = _get_tenant_no(db, user)
  1259. data = add_sign(
  1260. db,
  1261. task_id,
  1262. tenant_no=tenant_no,
  1263. user_id=user.get("user_id") or "",
  1264. user_ids=body.user_ids,
  1265. comment=body.comment,
  1266. )
  1267. return success_response(data, message="加签成功")
  1268. except OaEngineError as e:
  1269. return error_response(400, str(e))
  1270. @router.post("/tasks/{task_id}/transfer")
  1271. async def transfer_oa_task(
  1272. task_id: str,
  1273. body: OaTransferBody,
  1274. user: dict = Depends(require_admin_or_super_admin),
  1275. db: Session = Depends(get_db),
  1276. ):
  1277. from app.services.oa_engine import OaEngineError, transfer_task
  1278. try:
  1279. tenant_no = _get_tenant_no(db, user)
  1280. data = transfer_task(
  1281. db,
  1282. task_id,
  1283. tenant_no=tenant_no,
  1284. user_id=user.get("user_id") or "",
  1285. target_user_id=body.target_user_id,
  1286. comment=body.comment,
  1287. )
  1288. return success_response(data, message="已转交")
  1289. except OaEngineError as e:
  1290. return error_response(400, str(e))
  1291. @router.post("/instances/{instance_id}/withdraw")
  1292. async def withdraw_oa_instance(
  1293. instance_id: str,
  1294. user: dict = Depends(require_admin_or_super_admin),
  1295. db: Session = Depends(get_db),
  1296. ):
  1297. from app.services.oa_engine import OaEngineError, withdraw_instance
  1298. try:
  1299. tenant_no = _get_tenant_no(db, user)
  1300. data = withdraw_instance(
  1301. db, instance_id, tenant_no=tenant_no, user_id=user.get("user_id") or ""
  1302. )
  1303. return success_response(data, message="已撤销")
  1304. except OaEngineError as e:
  1305. return error_response(400, str(e))