oa_instance_draft.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. """OA 发起审批草稿 CRUD(oa_instance_draft)"""
  2. from __future__ import annotations
  3. import json
  4. from datetime import datetime
  5. from typing import Any, Dict, List, Optional, Tuple
  6. from sqlalchemy import func, or_
  7. from sqlalchemy.orm import Session
  8. from app.core.utils import generate_uuid
  9. from app.crud import oa_approval as oa_crud
  10. from app.models.models import OaApp, OaAppGroup, OaAppVersion, OaInstanceDraft
  11. from app.services.oa_related_approval import flatten_form_fields_for_summary
  12. def _loads(raw: Any, default: Any) -> Any:
  13. if raw is None or raw == "":
  14. return default
  15. if isinstance(raw, (dict, list)):
  16. return raw
  17. try:
  18. return json.loads(raw)
  19. except (TypeError, ValueError, json.JSONDecodeError):
  20. return default
  21. def _dumps(obj: Any) -> Optional[str]:
  22. if obj is None:
  23. return None
  24. return json.dumps(obj, ensure_ascii=False)
  25. def row_to_dict(
  26. d: OaInstanceDraft,
  27. *,
  28. app_name: str = "",
  29. version_no: Optional[int] = None,
  30. group_id: str = "",
  31. group_name: str = "",
  32. include_payload: bool = False,
  33. ) -> Dict[str, Any]:
  34. out: Dict[str, Any] = {
  35. "id": d.id,
  36. "appId": d.app_id,
  37. "appVersionId": d.app_version_id,
  38. "appName": app_name or "",
  39. "versionNo": version_no,
  40. "groupId": group_id or "",
  41. "groupName": group_name or "",
  42. "title": d.title or "",
  43. "ownerUserId": d.owner_user_id,
  44. "initiatorDeptId": d.initiator_dept_id or "",
  45. "status": "draft",
  46. "createTime": d.create_time.strftime("%Y-%m-%d %H:%M:%S") if d.create_time else "",
  47. "updateTime": d.update_time.strftime("%Y-%m-%d %H:%M:%S") if d.update_time else "",
  48. }
  49. if include_payload:
  50. out["formData"] = _loads(d.form_data_json, {}) or {}
  51. out["initiatorSelectByNode"] = _loads(d.initiator_select_json, {}) or {}
  52. return out
  53. class DraftAccessError(ValueError):
  54. """草稿不存在或无权访问"""
  55. def get_owned_draft(
  56. db: Session,
  57. draft_id: str,
  58. *,
  59. tenant_no: str,
  60. owner_user_id: str,
  61. ) -> OaInstanceDraft:
  62. row = (
  63. db.query(OaInstanceDraft)
  64. .filter(
  65. OaInstanceDraft.id == draft_id,
  66. OaInstanceDraft.tenant_no == tenant_no,
  67. OaInstanceDraft.owner_user_id == owner_user_id,
  68. OaInstanceDraft.del_flag == 0,
  69. )
  70. .first()
  71. )
  72. if not row:
  73. raise DraftAccessError("草稿不存在或无权访问")
  74. return row
  75. def _enrich(db: Session, d: OaInstanceDraft, *, include_payload: bool = False) -> Dict[str, Any]:
  76. app = (
  77. db.query(OaApp)
  78. .filter(OaApp.id == d.app_id, OaApp.tenant_no == d.tenant_no, OaApp.del_flag == 0)
  79. .first()
  80. )
  81. ver = (
  82. db.query(OaAppVersion)
  83. .filter(OaAppVersion.id == d.app_version_id, OaAppVersion.del_flag == 0)
  84. .first()
  85. )
  86. group_name = ""
  87. group_id = ""
  88. if app and app.group_id:
  89. group_id = app.group_id
  90. g = (
  91. db.query(OaAppGroup)
  92. .filter(OaAppGroup.id == app.group_id, OaAppGroup.del_flag == 0)
  93. .first()
  94. )
  95. if g:
  96. group_name = g.group_name or ""
  97. out = row_to_dict(
  98. d,
  99. app_name=(app.app_name if app else "") or "",
  100. version_no=ver.version_no if ver else None,
  101. group_id=group_id,
  102. group_name=group_name,
  103. include_payload=include_payload,
  104. )
  105. if include_payload and "formData" in out:
  106. from app.services.oa_datetime_normalize import normalize_datetime_form_values
  107. schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
  108. out["formData"] = normalize_datetime_form_values(schema, out.get("formData") or {})
  109. return out
  110. def count_drafts(db: Session, *, tenant_no: str, owner_user_id: str) -> int:
  111. return (
  112. db.query(OaInstanceDraft)
  113. .filter(
  114. OaInstanceDraft.tenant_no == tenant_no,
  115. OaInstanceDraft.owner_user_id == owner_user_id,
  116. OaInstanceDraft.del_flag == 0,
  117. )
  118. .count()
  119. )
  120. def list_drafts(
  121. db: Session,
  122. *,
  123. tenant_no: str,
  124. owner_user_id: str,
  125. keyword: Optional[str] = None,
  126. group_id: Optional[str] = None,
  127. update_time_from: Optional[datetime] = None,
  128. update_time_to: Optional[datetime] = None,
  129. page: int = 1,
  130. page_size: int = 20,
  131. ) -> Tuple[List[Dict[str, Any]], int]:
  132. q = (
  133. db.query(OaInstanceDraft, OaApp, OaAppVersion, OaAppGroup)
  134. .outerjoin(
  135. OaApp,
  136. (OaApp.id == OaInstanceDraft.app_id)
  137. & (OaApp.tenant_no == OaInstanceDraft.tenant_no)
  138. & (OaApp.del_flag == 0),
  139. )
  140. .outerjoin(
  141. OaAppVersion,
  142. (OaAppVersion.id == OaInstanceDraft.app_version_id) & (OaAppVersion.del_flag == 0),
  143. )
  144. .outerjoin(
  145. OaAppGroup,
  146. (OaAppGroup.id == OaApp.group_id) & (OaAppGroup.del_flag == 0),
  147. )
  148. .filter(
  149. OaInstanceDraft.tenant_no == tenant_no,
  150. OaInstanceDraft.owner_user_id == owner_user_id,
  151. OaInstanceDraft.del_flag == 0,
  152. )
  153. )
  154. kw = (keyword or "").strip()
  155. if kw:
  156. like = f"%{kw}%"
  157. q = q.filter(or_(OaApp.app_name.like(like), OaInstanceDraft.title.like(like)))
  158. gid = (group_id or "").strip()
  159. if gid:
  160. q = q.filter(OaApp.group_id == gid)
  161. if update_time_from is not None:
  162. q = q.filter(OaInstanceDraft.update_time >= update_time_from)
  163. if update_time_to is not None:
  164. q = q.filter(OaInstanceDraft.update_time <= update_time_to)
  165. total = q.order_by(None).with_entities(func.count(OaInstanceDraft.id)).scalar() or 0
  166. page = max(1, page)
  167. page_size = min(max(1, page_size), 100)
  168. rows = (
  169. q.order_by(OaInstanceDraft.update_time.desc())
  170. .offset((page - 1) * page_size)
  171. .limit(page_size)
  172. .all()
  173. )
  174. items: List[Dict[str, Any]] = []
  175. for d, app, ver, group in rows:
  176. item = row_to_dict(
  177. d,
  178. app_name=(app.app_name if app else "") or "",
  179. version_no=ver.version_no if ver else None,
  180. group_id=(app.group_id if app else "") or "",
  181. group_name=(group.group_name if group else "") or "",
  182. )
  183. schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
  184. form_data = _loads(d.form_data_json, {}) or {}
  185. preview = [
  186. {"label": f.get("label") or "", "value": f.get("value") or ""}
  187. for f in flatten_form_fields_for_summary(schema, form_data)[:3]
  188. ]
  189. item["previewFields"] = preview
  190. items.append(item)
  191. return items, int(total)
  192. def get_draft_detail(
  193. db: Session,
  194. draft_id: str,
  195. *,
  196. tenant_no: str,
  197. owner_user_id: str,
  198. ) -> Dict[str, Any]:
  199. d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
  200. return _enrich(db, d, include_payload=True)
  201. def create_draft(
  202. db: Session,
  203. *,
  204. tenant_no: str,
  205. owner_user_id: str,
  206. app_id: str,
  207. form_data: Optional[Dict[str, Any]] = None,
  208. initiator_select_by_node: Optional[Dict[str, Any]] = None,
  209. initiator_dept_id: Optional[str] = None,
  210. title: Optional[str] = None,
  211. create_dept_id: str = "",
  212. ) -> Dict[str, Any]:
  213. app = oa_crud.get_app(db, app_id, tenant_no)
  214. if not app or app.status != "1":
  215. raise ValueError("应用不存在或已停用")
  216. ver = oa_crud._find_latest_published(db, app_id, tenant_no)
  217. if not ver:
  218. raise ValueError("应用尚未发布,无法暂存草稿")
  219. now = datetime.now()
  220. row = OaInstanceDraft(
  221. id=generate_uuid(),
  222. app_id=app_id,
  223. app_version_id=ver.id,
  224. title=(title or app.app_name or "")[:200] or None,
  225. form_data_json=_dumps(form_data or {}),
  226. initiator_select_json=_dumps(initiator_select_by_node or {}),
  227. initiator_dept_id=(initiator_dept_id or "").strip() or None,
  228. owner_user_id=owner_user_id,
  229. tenant_no=tenant_no,
  230. create_by=owner_user_id,
  231. create_dept_id=create_dept_id or "",
  232. create_time=now,
  233. update_by=owner_user_id,
  234. update_time=now,
  235. del_flag=0,
  236. )
  237. db.add(row)
  238. db.commit()
  239. db.refresh(row)
  240. return _enrich(db, row, include_payload=True)
  241. def update_draft(
  242. db: Session,
  243. draft_id: str,
  244. *,
  245. tenant_no: str,
  246. owner_user_id: str,
  247. form_data: Optional[Dict[str, Any]] = None,
  248. initiator_select_by_node: Optional[Dict[str, Any]] = None,
  249. initiator_dept_id: Optional[str] = None,
  250. title: Optional[str] = None,
  251. ) -> Dict[str, Any]:
  252. d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
  253. ver = oa_crud._find_latest_published(db, d.app_id, tenant_no)
  254. if ver:
  255. d.app_version_id = ver.id
  256. if form_data is not None:
  257. d.form_data_json = _dumps(form_data)
  258. if initiator_select_by_node is not None:
  259. d.initiator_select_json = _dumps(initiator_select_by_node)
  260. if initiator_dept_id is not None:
  261. d.initiator_dept_id = (initiator_dept_id or "").strip() or None
  262. if title is not None:
  263. d.title = (title or "")[:200] or None
  264. d.update_by = owner_user_id
  265. d.update_time = datetime.now()
  266. db.commit()
  267. db.refresh(d)
  268. return _enrich(db, d, include_payload=True)
  269. def soft_delete_draft(
  270. db: Session,
  271. draft_id: str,
  272. *,
  273. tenant_no: str,
  274. owner_user_id: str,
  275. ) -> None:
  276. d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
  277. d.del_flag = 1
  278. d.update_by = owner_user_id
  279. d.update_time = datetime.now()
  280. db.commit()
  281. def batch_soft_delete_drafts(
  282. db: Session,
  283. ids: List[str],
  284. *,
  285. tenant_no: str,
  286. owner_user_id: str,
  287. ) -> int:
  288. cleaned = [str(x).strip() for x in (ids or []) if str(x).strip()]
  289. if not cleaned:
  290. raise ValueError("请选择要删除的草稿")
  291. rows = (
  292. db.query(OaInstanceDraft)
  293. .filter(
  294. OaInstanceDraft.id.in_(cleaned),
  295. OaInstanceDraft.tenant_no == tenant_no,
  296. OaInstanceDraft.owner_user_id == owner_user_id,
  297. OaInstanceDraft.del_flag == 0,
  298. )
  299. .all()
  300. )
  301. if len(rows) != len(set(cleaned)):
  302. raise DraftAccessError("草稿不存在或无权访问")
  303. now = datetime.now()
  304. for d in rows:
  305. d.del_flag = 1
  306. d.update_by = owner_user_id
  307. d.update_time = now
  308. db.commit()
  309. return len(rows)
  310. def submit_draft(
  311. db: Session,
  312. draft_id: str,
  313. *,
  314. tenant_no: str,
  315. owner_user_id: str,
  316. form_data: Optional[Dict[str, Any]] = None,
  317. initiator_select_by_node: Optional[Dict[str, Any]] = None,
  318. initiator_dept_id: Optional[str] = None,
  319. title: Optional[str] = None,
  320. ) -> Dict[str, Any]:
  321. """提交草稿:走 start_instance,成功后软删草稿。"""
  322. from app.services.oa_engine import start_instance
  323. d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
  324. payload_form = form_data if form_data is not None else (_loads(d.form_data_json, {}) or {})
  325. payload_select = (
  326. initiator_select_by_node
  327. if initiator_select_by_node is not None
  328. else (_loads(d.initiator_select_json, {}) or {})
  329. )
  330. dept = initiator_dept_id if initiator_dept_id is not None else d.initiator_dept_id
  331. use_title = title if title is not None else d.title
  332. data = start_instance(
  333. db,
  334. tenant_no=tenant_no,
  335. app_id=d.app_id,
  336. initiator_user_id=owner_user_id,
  337. initiator_dept_id=dept,
  338. form_data=payload_form if isinstance(payload_form, dict) else {},
  339. title=use_title,
  340. initiator_select_by_node=payload_select if isinstance(payload_select, dict) else {},
  341. )
  342. d.del_flag = 1
  343. d.update_by = owner_user_id
  344. d.update_time = datetime.now()
  345. db.commit()
  346. return data