| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- """请假申请 API — 工作流测试"""
- from __future__ import annotations
- import logging
- from datetime import datetime
- from typing import List, Optional
- from pydantic import BaseModel, ConfigDict, Field
- from fastapi import APIRouter, Depends, Query
- from sqlalchemy.orm import Session
- from app.core.menu_guard import require_admin_or_super_admin
- from app.core.database import get_db
- from app.core.response import success_response, error_response
- from app.crud import leave as leave_crud
- from app.crud.leave import total_pages
- from app.schemas.leave import LeaveCreate, LeaveUpdate
- from app.utils.excel_export import (
- EXPORT_MAX_ROWS,
- ExportColumn,
- ExportValidationError,
- export_rows_to_xlsx_response,
- filter_export_columns,
- )
- logger = logging.getLogger(__name__)
- router = APIRouter(prefix="/web/admin/leave", tags=["请假管理"])
- _LEAVE_TYPE_MAP = {"personal": "事假", "sick": "病假", "annual": "年假", "other": "其他"}
- def _leave_local_callback(db: Session, business_id: str, event: str) -> None:
- if not business_id:
- return
- if event == "completed":
- leave_crud.update_leave_status(db, business_id, "approved")
- elif event == "rejected":
- leave_crud.update_leave_status(db, business_id, "rejected")
- db.commit()
- from app.services.workflow_callback import register_local_callback
- register_local_callback("/api/admin/leave/callback", _leave_local_callback)
- @router.get("")
- def list_leaves(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=500),
- keyword: Optional[str] = Query(None),
- status: Optional[str] = Query(None),
- reasonIn: Optional[str] = Query(None),
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _user.get("tenant_no", "")
- user_id = _user.get("user_id") or _user.get("id", "")
- items, total = leave_crud.list_leaves(
- db, page=page, page_size=pageSize, keyword=keyword, status=status,
- reason_in=reasonIn,
- tenant_no=tenant_no, create_by=user_id, user_info=_user,
- )
- for item in items:
- item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
- return success_response({
- "list": items, "total": total, "page": page, "pageSize": pageSize,
- "totalPages": total_pages(total, pageSize),
- })
- class LeaveColumnFacetBody(BaseModel):
- """请假列表列头可选值(去重;可附带列表上下文)"""
- model_config = ConfigDict(populate_by_name=True, extra="allow")
- field: str = Field(..., description="reason")
- keyword: Optional[str] = Field(default=None, description="列表关键字(原因/类型)")
- limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
- @router.post("/column-facet-values")
- def leave_column_facet_values(
- body: LeaveColumnFacetBody,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """请假列表列头可选值:在列表上下文(排除本列)下去重。"""
- from app.models.models import BizLeaveRequest
- from app.services.column_facet import list_column_facet_values
- from app.services.pl.facet_context import strip_simple_facet_self_params
- if body.field != "reason":
- return error_response(400, "不支持的列")
- try:
- tenant_no = _user.get("tenant_no", "")
- user_id = _user.get("user_id") or _user.get("id", "")
- data = body.model_dump(by_alias=True, exclude_none=True)
- extra = getattr(body, "model_extra", None) or {}
- for key, value in extra.items():
- if key not in data and value not in (None, ""):
- data[key] = value
- list_keyword = (body.keyword or "").strip() or None
- params = strip_simple_facet_self_params(data, body.field)
- filters = []
- kw = list_keyword or str(params.get("keyword") or "").strip()
- status_in = params.get("statusIn") or params.get("status_in")
- if status_in:
- values = [s.strip() for s in str(status_in).split(",") if s.strip()]
- if values:
- filters.append(BizLeaveRequest.status.in_(values))
- leave_type_in = params.get("leaveTypeIn") or params.get("leave_type_in")
- if leave_type_in:
- values = [s.strip() for s in str(leave_type_in).split(",") if s.strip()]
- if values:
- filters.append(BizLeaveRequest.leave_type.in_(values))
- rows = list_column_facet_values(
- db,
- BizLeaveRequest,
- BizLeaveRequest.reason,
- tenant_no=tenant_no,
- keyword=None,
- limit=body.limit if body.limit is not None else 500,
- extra_filters=filters or None,
- apply_scope=lambda query: leave_crud.apply_leave_list_filters(
- query, db, keyword=kw,
- status=None if status_in else params.get("status"),
- tenant_no=tenant_no, create_by=user_id, user_info=_user,
- ),
- )
- return success_response({"list": rows})
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- _LEAVE_STATUS_MAP = {"draft": "草稿", "pending": "审批中", "approved": "已通过", "rejected": "已驳回"}
- LEAVE_EXPORT_COLUMNS = [
- ExportColumn(title="请假类型", field="leaveTypeLabel"),
- ExportColumn(title="开始日期", field="startDate"),
- ExportColumn(title="结束日期", field="endDate"),
- ExportColumn(title="天数", field="days"),
- ExportColumn(title="请假原因", field="reason"),
- ExportColumn(title="状态", field="statusLabel"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class LeaveExportBody(BaseModel):
- keyword: Optional[str] = None
- status: Optional[str] = None
- reason_in: Optional[str] = Field(default=None, alias="reasonIn")
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.post("/export")
- def export_leaves(
- body: LeaveExportBody,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """导出请假列表(当前筛选全量,或 ids 已选)。"""
- try:
- tenant_no = _user.get("tenant_no", "")
- user_id = _user.get("user_id") or _user.get("id", "")
- items, _ = leave_crud.list_leaves(
- db,
- page=1,
- page_size=EXPORT_MAX_ROWS,
- keyword=body.keyword,
- status=body.status,
- reason_in=body.reason_in,
- tenant_no=tenant_no,
- create_by=user_id,
- ids=body.ids,
- max_page_size=EXPORT_MAX_ROWS,
- user_info=_user,
- )
- for item in items:
- item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
- item["statusLabel"] = _LEAVE_STATUS_MAP.get(item.get("status", ""), item.get("status", ""))
- # map frontend props status/leaveType to label fields when columns requested
- requested = None
- if body.columns:
- requested = []
- for key in body.columns:
- if key == "status":
- requested.append("statusLabel")
- elif key == "leaveType":
- requested.append("leaveTypeLabel")
- else:
- requested.append(key)
- cols = filter_export_columns(LEAVE_EXPORT_COLUMNS, requested)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no or None,
- sheet_title="请假列表",
- columns=cols,
- rows=items,
- filename_stem=f"请假列表_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("")
- def create_leave(
- body: LeaveCreate,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _user.get("tenant_no", "")
- user_id = _user.get("user_id") or _user.get("id", "")
- dept_id = _user.get("region_id", "")
- row = leave_crud.create_leave(
- db,
- leave_type=body.leaveType,
- start_date=body.startDate,
- end_date=body.endDate,
- days=body.days,
- reason=body.reason or "",
- tenant_no=tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- remark=body.remark or "",
- )
- db.commit()
- return success_response(leave_crud._row_to_dict(row))
- @router.post("/{leave_id}/submit")
- def submit_leave(
- leave_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- from app.models.models import WfProcessDef
- from app.services.workflow_engine import WorkflowEngine
- tenant_no = _user.get("tenant_no", "")
- user_id = _user.get("user_id") or _user.get("id", "")
- leave = leave_crud.get_leave(db, leave_id)
- if not leave:
- return error_response(404, "请假记录不存在")
- if leave["status"] not in ("draft", "rejected"):
- return error_response(400, "只有草稿或已驳回的请假单可以提交")
- defn = db.query(WfProcessDef).filter(
- WfProcessDef.process_code == "leave_request",
- WfProcessDef.del_flag == 0,
- WfProcessDef.status == "1",
- ).first()
- if not defn:
- return error_response(400, "未找到请假审批流程(leave_request),请先在流程配置中创建并发布")
- variables = {
- "days": leave["days"],
- "leave_days": leave["days"],
- "leaveType": leave["leaveType"],
- "applicant": user_id,
- }
- try:
- engine = WorkflowEngine(db)
- instance = engine.start_process(
- process_def_id=defn.id,
- business_type="leave",
- business_id=leave_id,
- starter_user_id=user_id,
- variables=variables,
- callback_url="/api/admin/leave/callback",
- tenant_no=tenant_no,
- user_id=user_id,
- )
- leave_crud.update_leave_status(db, leave_id, "pending", wf_instance_id=instance.id, user_id=user_id)
- db.commit()
- return success_response({"instanceId": instance.id, "status": "pending"})
- except Exception as e:
- db.rollback()
- logger.exception("发起请假审批失败: %s", e)
- return error_response(500, f"发起审批失败: {e}")
- @router.post("/callback")
- def leave_workflow_callback(
- body: dict,
- db: Session = Depends(get_db),
- ):
- logger.info("请假工作流回调: %s", body)
- business_id = body.get("businessId", "")
- event = body.get("event", "")
- if not business_id:
- return success_response({"received": True})
- if event == "completed":
- leave_crud.update_leave_status(db, business_id, "approved")
- elif event == "rejected":
- leave_crud.update_leave_status(db, business_id, "rejected")
- db.commit()
- return success_response({"received": True})
- @router.get("/{leave_id}")
- def get_leave(
- leave_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- item = leave_crud.get_leave(db, leave_id)
- if not item:
- return error_response(404, "请假记录不存在")
- item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
- return success_response(item)
- @router.delete("/{leave_id}")
- def delete_leave(
- leave_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- user_id = _user.get("user_id") or _user.get("id", "")
- leave = leave_crud.get_leave(db, leave_id)
- if not leave:
- return error_response(404, "请假记录不存在")
- if leave["status"] == "pending":
- return error_response(400, "审批中的请假单不能删除")
- leave_crud.soft_delete_leave(db, leave_id, user_id=user_id, user_info=_user)
- db.commit()
- return success_response(None)
- @router.put("/{leave_id}")
- def update_leave(
- leave_id: str,
- body: LeaveUpdate,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- user_id = _user.get("user_id") or _user.get("id", "")
- leave = leave_crud.get_leave(db, leave_id)
- if not leave:
- return error_response(404, "请假记录不存在")
- if leave["status"] not in ("draft", "rejected"):
- return error_response(400, "只有草稿或已驳回状态可以编辑")
- leave_crud.update_leave(db, leave_id, user_id=user_id, **body.model_dump(exclude_none=True))
- db.commit()
- return success_response(leave_crud.get_leave(db, leave_id))
|