leave.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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, ConfigDict, Field
  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. reasonIn: Optional[str] = Query(None),
  42. _user: dict = Depends(require_admin_or_super_admin),
  43. db: Session = Depends(get_db),
  44. ):
  45. tenant_no = _user.get("tenant_no", "")
  46. user_id = _user.get("user_id") or _user.get("id", "")
  47. items, total = leave_crud.list_leaves(
  48. db, page=page, page_size=pageSize, keyword=keyword, status=status,
  49. reason_in=reasonIn,
  50. tenant_no=tenant_no, create_by=user_id, user_info=_user,
  51. )
  52. for item in items:
  53. item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
  54. return success_response({
  55. "list": items, "total": total, "page": page, "pageSize": pageSize,
  56. "totalPages": total_pages(total, pageSize),
  57. })
  58. class LeaveColumnFacetBody(BaseModel):
  59. """请假列表列头可选值(去重;可附带列表上下文)"""
  60. model_config = ConfigDict(populate_by_name=True, extra="allow")
  61. field: str = Field(..., description="reason")
  62. keyword: Optional[str] = Field(default=None, description="列表关键字(原因/类型)")
  63. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  64. @router.post("/column-facet-values")
  65. def leave_column_facet_values(
  66. body: LeaveColumnFacetBody,
  67. _user: dict = Depends(require_admin_or_super_admin),
  68. db: Session = Depends(get_db),
  69. ):
  70. """请假列表列头可选值:在列表上下文(排除本列)下去重。"""
  71. from app.models.models import BizLeaveRequest
  72. from app.services.column_facet import list_column_facet_values
  73. from app.services.pl.facet_context import strip_simple_facet_self_params
  74. if body.field != "reason":
  75. return error_response(400, "不支持的列")
  76. try:
  77. tenant_no = _user.get("tenant_no", "")
  78. user_id = _user.get("user_id") or _user.get("id", "")
  79. data = body.model_dump(by_alias=True, exclude_none=True)
  80. extra = getattr(body, "model_extra", None) or {}
  81. for key, value in extra.items():
  82. if key not in data and value not in (None, ""):
  83. data[key] = value
  84. list_keyword = (body.keyword or "").strip() or None
  85. params = strip_simple_facet_self_params(data, body.field)
  86. filters = []
  87. kw = list_keyword or str(params.get("keyword") or "").strip()
  88. status_in = params.get("statusIn") or params.get("status_in")
  89. if status_in:
  90. values = [s.strip() for s in str(status_in).split(",") if s.strip()]
  91. if values:
  92. filters.append(BizLeaveRequest.status.in_(values))
  93. leave_type_in = params.get("leaveTypeIn") or params.get("leave_type_in")
  94. if leave_type_in:
  95. values = [s.strip() for s in str(leave_type_in).split(",") if s.strip()]
  96. if values:
  97. filters.append(BizLeaveRequest.leave_type.in_(values))
  98. rows = list_column_facet_values(
  99. db,
  100. BizLeaveRequest,
  101. BizLeaveRequest.reason,
  102. tenant_no=tenant_no,
  103. keyword=None,
  104. limit=body.limit if body.limit is not None else 500,
  105. extra_filters=filters or None,
  106. apply_scope=lambda query: leave_crud.apply_leave_list_filters(
  107. query, db, keyword=kw,
  108. status=None if status_in else params.get("status"),
  109. tenant_no=tenant_no, create_by=user_id, user_info=_user,
  110. ),
  111. )
  112. return success_response({"list": rows})
  113. except ValueError as e:
  114. return error_response(400, str(e))
  115. except Exception as e:
  116. return error_response(500, str(e))
  117. _LEAVE_STATUS_MAP = {"draft": "草稿", "pending": "审批中", "approved": "已通过", "rejected": "已驳回"}
  118. LEAVE_EXPORT_COLUMNS = [
  119. ExportColumn(title="请假类型", field="leaveTypeLabel"),
  120. ExportColumn(title="开始日期", field="startDate"),
  121. ExportColumn(title="结束日期", field="endDate"),
  122. ExportColumn(title="天数", field="days"),
  123. ExportColumn(title="请假原因", field="reason"),
  124. ExportColumn(title="状态", field="statusLabel"),
  125. ExportColumn(title="创建时间", field="createTime"),
  126. ]
  127. class LeaveExportBody(BaseModel):
  128. keyword: Optional[str] = None
  129. status: Optional[str] = None
  130. reason_in: Optional[str] = Field(default=None, alias="reasonIn")
  131. ids: Optional[List[str]] = None
  132. columns: Optional[List[str]] = None
  133. @router.post("/export")
  134. def export_leaves(
  135. body: LeaveExportBody,
  136. _user: dict = Depends(require_admin_or_super_admin),
  137. db: Session = Depends(get_db),
  138. ):
  139. """导出请假列表(当前筛选全量,或 ids 已选)。"""
  140. try:
  141. tenant_no = _user.get("tenant_no", "")
  142. user_id = _user.get("user_id") or _user.get("id", "")
  143. items, _ = leave_crud.list_leaves(
  144. db,
  145. page=1,
  146. page_size=EXPORT_MAX_ROWS,
  147. keyword=body.keyword,
  148. status=body.status,
  149. reason_in=body.reason_in,
  150. tenant_no=tenant_no,
  151. create_by=user_id,
  152. ids=body.ids,
  153. max_page_size=EXPORT_MAX_ROWS,
  154. user_info=_user,
  155. )
  156. for item in items:
  157. item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
  158. item["statusLabel"] = _LEAVE_STATUS_MAP.get(item.get("status", ""), item.get("status", ""))
  159. # map frontend props status/leaveType to label fields when columns requested
  160. requested = None
  161. if body.columns:
  162. requested = []
  163. for key in body.columns:
  164. if key == "status":
  165. requested.append("statusLabel")
  166. elif key == "leaveType":
  167. requested.append("leaveTypeLabel")
  168. else:
  169. requested.append(key)
  170. cols = filter_export_columns(LEAVE_EXPORT_COLUMNS, requested)
  171. date_str = datetime.now().strftime("%Y%m%d")
  172. return export_rows_to_xlsx_response(
  173. db,
  174. tenant_no=tenant_no or None,
  175. sheet_title="请假列表",
  176. columns=cols,
  177. rows=items,
  178. filename_stem=f"请假列表_{date_str}",
  179. )
  180. except ExportValidationError as e:
  181. return error_response(400, str(e))
  182. except Exception as e:
  183. return error_response(500, str(e))
  184. @router.post("")
  185. def create_leave(
  186. body: LeaveCreate,
  187. _user: dict = Depends(require_admin_or_super_admin),
  188. db: Session = Depends(get_db),
  189. ):
  190. tenant_no = _user.get("tenant_no", "")
  191. user_id = _user.get("user_id") or _user.get("id", "")
  192. dept_id = _user.get("region_id", "")
  193. row = leave_crud.create_leave(
  194. db,
  195. leave_type=body.leaveType,
  196. start_date=body.startDate,
  197. end_date=body.endDate,
  198. days=body.days,
  199. reason=body.reason or "",
  200. tenant_no=tenant_no,
  201. user_id=user_id,
  202. dept_id=dept_id,
  203. remark=body.remark or "",
  204. )
  205. db.commit()
  206. return success_response(leave_crud._row_to_dict(row))
  207. @router.post("/{leave_id}/submit")
  208. def submit_leave(
  209. leave_id: str,
  210. _user: dict = Depends(require_admin_or_super_admin),
  211. db: Session = Depends(get_db),
  212. ):
  213. from app.models.models import WfProcessDef
  214. from app.services.workflow_engine import WorkflowEngine
  215. tenant_no = _user.get("tenant_no", "")
  216. user_id = _user.get("user_id") or _user.get("id", "")
  217. leave = leave_crud.get_leave(db, leave_id)
  218. if not leave:
  219. return error_response(404, "请假记录不存在")
  220. if leave["status"] not in ("draft", "rejected"):
  221. return error_response(400, "只有草稿或已驳回的请假单可以提交")
  222. defn = db.query(WfProcessDef).filter(
  223. WfProcessDef.process_code == "leave_request",
  224. WfProcessDef.del_flag == 0,
  225. WfProcessDef.status == "1",
  226. ).first()
  227. if not defn:
  228. return error_response(400, "未找到请假审批流程(leave_request),请先在流程配置中创建并发布")
  229. variables = {
  230. "days": leave["days"],
  231. "leave_days": leave["days"],
  232. "leaveType": leave["leaveType"],
  233. "applicant": user_id,
  234. }
  235. try:
  236. engine = WorkflowEngine(db)
  237. instance = engine.start_process(
  238. process_def_id=defn.id,
  239. business_type="leave",
  240. business_id=leave_id,
  241. starter_user_id=user_id,
  242. variables=variables,
  243. callback_url="/api/admin/leave/callback",
  244. tenant_no=tenant_no,
  245. user_id=user_id,
  246. )
  247. leave_crud.update_leave_status(db, leave_id, "pending", wf_instance_id=instance.id, user_id=user_id)
  248. db.commit()
  249. return success_response({"instanceId": instance.id, "status": "pending"})
  250. except Exception as e:
  251. db.rollback()
  252. logger.exception("发起请假审批失败: %s", e)
  253. return error_response(500, f"发起审批失败: {e}")
  254. @router.post("/callback")
  255. def leave_workflow_callback(
  256. body: dict,
  257. db: Session = Depends(get_db),
  258. ):
  259. logger.info("请假工作流回调: %s", body)
  260. business_id = body.get("businessId", "")
  261. event = body.get("event", "")
  262. if not business_id:
  263. return success_response({"received": True})
  264. if event == "completed":
  265. leave_crud.update_leave_status(db, business_id, "approved")
  266. elif event == "rejected":
  267. leave_crud.update_leave_status(db, business_id, "rejected")
  268. db.commit()
  269. return success_response({"received": True})
  270. @router.get("/{leave_id}")
  271. def get_leave(
  272. leave_id: str,
  273. _user: dict = Depends(require_admin_or_super_admin),
  274. db: Session = Depends(get_db),
  275. ):
  276. item = leave_crud.get_leave(db, leave_id)
  277. if not item:
  278. return error_response(404, "请假记录不存在")
  279. item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
  280. return success_response(item)
  281. @router.delete("/{leave_id}")
  282. def delete_leave(
  283. leave_id: str,
  284. _user: dict = Depends(require_admin_or_super_admin),
  285. db: Session = Depends(get_db),
  286. ):
  287. user_id = _user.get("user_id") or _user.get("id", "")
  288. leave = leave_crud.get_leave(db, leave_id)
  289. if not leave:
  290. return error_response(404, "请假记录不存在")
  291. if leave["status"] == "pending":
  292. return error_response(400, "审批中的请假单不能删除")
  293. leave_crud.soft_delete_leave(db, leave_id, user_id=user_id, user_info=_user)
  294. db.commit()
  295. return success_response(None)
  296. @router.put("/{leave_id}")
  297. def update_leave(
  298. leave_id: str,
  299. body: LeaveUpdate,
  300. _user: dict = Depends(require_admin_or_super_admin),
  301. db: Session = Depends(get_db),
  302. ):
  303. user_id = _user.get("user_id") or _user.get("id", "")
  304. leave = leave_crud.get_leave(db, leave_id)
  305. if not leave:
  306. return error_response(404, "请假记录不存在")
  307. if leave["status"] not in ("draft", "rejected"):
  308. return error_response(400, "只有草稿或已驳回状态可以编辑")
  309. leave_crud.update_leave(db, leave_id, user_id=user_id, **body.model_dump(exclude_none=True))
  310. db.commit()
  311. return success_response(leave_crud.get_leave(db, leave_id))