leave.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. """请假申请 API — 工作流测试"""
  2. from __future__ import annotations
  3. import logging
  4. from datetime import datetime
  5. from typing import List, Optional
  6. from pydantic import BaseModel
  7. from fastapi import APIRouter, Depends, Query
  8. from sqlalchemy.orm import Session
  9. from app.core.menu_guard import require_admin_or_super_admin
  10. from app.core.database import get_db
  11. from app.core.response import success_response, error_response
  12. from app.crud import leave as leave_crud
  13. from app.crud.leave import total_pages
  14. from app.schemas.leave import LeaveCreate, LeaveUpdate
  15. from app.utils.excel_export import (
  16. EXPORT_MAX_ROWS,
  17. ExportColumn,
  18. ExportValidationError,
  19. export_rows_to_xlsx_response,
  20. filter_export_columns,
  21. )
  22. logger = logging.getLogger(__name__)
  23. router = APIRouter(prefix="/web/admin/leave", tags=["请假管理"])
  24. _LEAVE_TYPE_MAP = {"personal": "事假", "sick": "病假", "annual": "年假", "other": "其他"}
  25. def _leave_local_callback(db: Session, business_id: str, event: str) -> None:
  26. if not business_id:
  27. return
  28. if event == "completed":
  29. leave_crud.update_leave_status(db, business_id, "approved")
  30. elif event == "rejected":
  31. leave_crud.update_leave_status(db, business_id, "rejected")
  32. db.commit()
  33. from app.services.workflow_callback import register_local_callback
  34. register_local_callback("/api/admin/leave/callback", _leave_local_callback)
  35. @router.get("")
  36. def list_leaves(
  37. page: int = Query(1, ge=1),
  38. pageSize: int = Query(10, ge=1, le=500),
  39. keyword: Optional[str] = Query(None),
  40. status: Optional[str] = Query(None),
  41. _user: dict = Depends(require_admin_or_super_admin),
  42. db: Session = Depends(get_db),
  43. ):
  44. tenant_no = _user.get("tenant_no", "")
  45. user_id = _user.get("user_id") or _user.get("id", "")
  46. items, total = leave_crud.list_leaves(
  47. db, page=page, page_size=pageSize, keyword=keyword, status=status,
  48. tenant_no=tenant_no, create_by=user_id, user_info=_user,
  49. )
  50. for item in items:
  51. item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
  52. return success_response({
  53. "list": items, "total": total, "page": page, "pageSize": pageSize,
  54. "totalPages": total_pages(total, pageSize),
  55. })
  56. _LEAVE_STATUS_MAP = {"draft": "草稿", "pending": "审批中", "approved": "已通过", "rejected": "已驳回"}
  57. LEAVE_EXPORT_COLUMNS = [
  58. ExportColumn(title="请假类型", field="leaveTypeLabel"),
  59. ExportColumn(title="开始日期", field="startDate"),
  60. ExportColumn(title="结束日期", field="endDate"),
  61. ExportColumn(title="天数", field="days"),
  62. ExportColumn(title="请假原因", field="reason"),
  63. ExportColumn(title="状态", field="statusLabel"),
  64. ExportColumn(title="创建时间", field="createTime"),
  65. ]
  66. class LeaveExportBody(BaseModel):
  67. keyword: Optional[str] = None
  68. status: Optional[str] = None
  69. ids: Optional[List[str]] = None
  70. columns: Optional[List[str]] = None
  71. @router.post("/export")
  72. def export_leaves(
  73. body: LeaveExportBody,
  74. _user: dict = Depends(require_admin_or_super_admin),
  75. db: Session = Depends(get_db),
  76. ):
  77. """导出请假列表(当前筛选全量,或 ids 已选)。"""
  78. try:
  79. tenant_no = _user.get("tenant_no", "")
  80. user_id = _user.get("user_id") or _user.get("id", "")
  81. items, _ = leave_crud.list_leaves(
  82. db,
  83. page=1,
  84. page_size=EXPORT_MAX_ROWS,
  85. keyword=body.keyword,
  86. status=body.status,
  87. tenant_no=tenant_no,
  88. create_by=user_id,
  89. ids=body.ids,
  90. max_page_size=EXPORT_MAX_ROWS,
  91. user_info=_user,
  92. )
  93. for item in items:
  94. item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
  95. item["statusLabel"] = _LEAVE_STATUS_MAP.get(item.get("status", ""), item.get("status", ""))
  96. # map frontend props status/leaveType to label fields when columns requested
  97. requested = None
  98. if body.columns:
  99. requested = []
  100. for key in body.columns:
  101. if key == "status":
  102. requested.append("statusLabel")
  103. elif key == "leaveType":
  104. requested.append("leaveTypeLabel")
  105. else:
  106. requested.append(key)
  107. cols = filter_export_columns(LEAVE_EXPORT_COLUMNS, requested)
  108. date_str = datetime.now().strftime("%Y%m%d")
  109. return export_rows_to_xlsx_response(
  110. db,
  111. tenant_no=tenant_no or None,
  112. sheet_title="请假列表",
  113. columns=cols,
  114. rows=items,
  115. filename_stem=f"请假列表_{date_str}",
  116. )
  117. except ExportValidationError as e:
  118. return error_response(400, str(e))
  119. except Exception as e:
  120. return error_response(500, str(e))
  121. @router.post("")
  122. def create_leave(
  123. body: LeaveCreate,
  124. _user: dict = Depends(require_admin_or_super_admin),
  125. db: Session = Depends(get_db),
  126. ):
  127. tenant_no = _user.get("tenant_no", "")
  128. user_id = _user.get("user_id") or _user.get("id", "")
  129. dept_id = _user.get("region_id", "")
  130. row = leave_crud.create_leave(
  131. db,
  132. leave_type=body.leaveType,
  133. start_date=body.startDate,
  134. end_date=body.endDate,
  135. days=body.days,
  136. reason=body.reason or "",
  137. tenant_no=tenant_no,
  138. user_id=user_id,
  139. dept_id=dept_id,
  140. remark=body.remark or "",
  141. )
  142. db.commit()
  143. return success_response(leave_crud._row_to_dict(row))
  144. @router.post("/{leave_id}/submit")
  145. def submit_leave(
  146. leave_id: str,
  147. _user: dict = Depends(require_admin_or_super_admin),
  148. db: Session = Depends(get_db),
  149. ):
  150. from app.models.models import WfProcessDef
  151. from app.services.workflow_engine import WorkflowEngine
  152. tenant_no = _user.get("tenant_no", "")
  153. user_id = _user.get("user_id") or _user.get("id", "")
  154. leave = leave_crud.get_leave(db, leave_id)
  155. if not leave:
  156. return error_response(404, "请假记录不存在")
  157. if leave["status"] not in ("draft", "rejected"):
  158. return error_response(400, "只有草稿或已驳回的请假单可以提交")
  159. defn = db.query(WfProcessDef).filter(
  160. WfProcessDef.process_code == "leave_request",
  161. WfProcessDef.del_flag == 0,
  162. WfProcessDef.status == "1",
  163. ).first()
  164. if not defn:
  165. return error_response(400, "未找到请假审批流程(leave_request),请先在流程配置中创建并发布")
  166. variables = {
  167. "days": leave["days"],
  168. "leave_days": leave["days"],
  169. "leaveType": leave["leaveType"],
  170. "applicant": user_id,
  171. }
  172. try:
  173. engine = WorkflowEngine(db)
  174. instance = engine.start_process(
  175. process_def_id=defn.id,
  176. business_type="leave",
  177. business_id=leave_id,
  178. starter_user_id=user_id,
  179. variables=variables,
  180. callback_url="/api/admin/leave/callback",
  181. tenant_no=tenant_no,
  182. user_id=user_id,
  183. )
  184. leave_crud.update_leave_status(db, leave_id, "pending", wf_instance_id=instance.id, user_id=user_id)
  185. db.commit()
  186. return success_response({"instanceId": instance.id, "status": "pending"})
  187. except Exception as e:
  188. db.rollback()
  189. logger.exception("发起请假审批失败: %s", e)
  190. return error_response(500, f"发起审批失败: {e}")
  191. @router.post("/callback")
  192. def leave_workflow_callback(
  193. body: dict,
  194. db: Session = Depends(get_db),
  195. ):
  196. logger.info("请假工作流回调: %s", body)
  197. business_id = body.get("businessId", "")
  198. event = body.get("event", "")
  199. if not business_id:
  200. return success_response({"received": True})
  201. if event == "completed":
  202. leave_crud.update_leave_status(db, business_id, "approved")
  203. elif event == "rejected":
  204. leave_crud.update_leave_status(db, business_id, "rejected")
  205. db.commit()
  206. return success_response({"received": True})
  207. @router.get("/{leave_id}")
  208. def get_leave(
  209. leave_id: str,
  210. _user: dict = Depends(require_admin_or_super_admin),
  211. db: Session = Depends(get_db),
  212. ):
  213. item = leave_crud.get_leave(db, leave_id)
  214. if not item:
  215. return error_response(404, "请假记录不存在")
  216. item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
  217. return success_response(item)
  218. @router.delete("/{leave_id}")
  219. def delete_leave(
  220. leave_id: str,
  221. _user: dict = Depends(require_admin_or_super_admin),
  222. db: Session = Depends(get_db),
  223. ):
  224. user_id = _user.get("user_id") or _user.get("id", "")
  225. leave = leave_crud.get_leave(db, leave_id)
  226. if not leave:
  227. return error_response(404, "请假记录不存在")
  228. if leave["status"] == "pending":
  229. return error_response(400, "审批中的请假单不能删除")
  230. leave_crud.soft_delete_leave(db, leave_id, user_id=user_id, user_info=_user)
  231. db.commit()
  232. return success_response(None)
  233. @router.put("/{leave_id}")
  234. def update_leave(
  235. leave_id: str,
  236. body: LeaveUpdate,
  237. _user: dict = Depends(require_admin_or_super_admin),
  238. db: Session = Depends(get_db),
  239. ):
  240. user_id = _user.get("user_id") or _user.get("id", "")
  241. leave = leave_crud.get_leave(db, leave_id)
  242. if not leave:
  243. return error_response(404, "请假记录不存在")
  244. if leave["status"] not in ("draft", "rejected"):
  245. return error_response(400, "只有草稿或已驳回状态可以编辑")
  246. leave_crud.update_leave(db, leave_id, user_id=user_id, **body.model_dump(exclude_none=True))
  247. db.commit()
  248. return success_response(leave_crud.get_leave(db, leave_id))