"""项目统计聚合接口 路由路径: - GET /api/pms/projects/summary - 获取所有项目汇总 - GET /api/pms/projects/{id}/stats - 获取单个项目详情 注意: /summary 路由必须写在 /{project_id}/stats 之前, 否则 FastAPI 会将 "summary" 误匹配为 {project_id} 参数。 """ from fastapi import APIRouter, Depends, Query, HTTPException from sqlalchemy.orm import Session from datetime import date from typing import Optional from app.database import get_db_pms from app.models.production import ( PmsProject, PmsWorkOrder, PmsMaterialApproval, ProjectStatus, WorkOrderStatus, ApprovalStatus, ) from app.schemas.project import ProjectStats, ProjectSummary, ProjectCreate, ProjectResponse router = APIRouter(prefix="/api/pms/projects", tags=["项目管理"]) # ============================================================ # 路由定义顺序很重要!更具体的路径必须写在前面! # ============================================================ @router.post("", response_model=ProjectResponse, status_code=201) async def create_project( project_data: ProjectCreate, db: Session = Depends(get_db_pms) ): """ 创建新项目 请求体: - project_no: 项目编号(必填,唯一) - project_name: 项目名称(必填) - start_date: 计划开始日期(可选) - end_date: 计划结束日期(可选) 返回创建后的项目信息,包含自动生成的 id 和 created_at """ # 检查项目编号是否重复 existing = db.query(PmsProject).filter( PmsProject.project_no == project_data.project_no ).first() if existing: raise HTTPException( status_code=409, detail=f"项目编号 '{project_data.project_no}' 已存在" ) # 创建项目实例 project = PmsProject( project_no=project_data.project_no, name=project_data.name, start_date=project_data.start_date, end_date=project_data.end_date, status=project_data.status, ) db.add(project) db.commit() db.refresh(project) return project def calculate_project_stats(project: PmsProject, work_orders: list, approvals: list) -> ProjectStats: """计算单个项目的统计信息""" today = date.today() # 统计工单状态 total_work_orders = len(work_orders) completed_work_orders = sum(1 for wo in work_orders if wo.status == WorkOrderStatus.COMPLETED) in_progress_work_orders = sum(1 for wo in work_orders if wo.status == WorkOrderStatus.IN_PROGRESS) pending_work_orders = sum(1 for wo in work_orders if wo.status == WorkOrderStatus.PENDING) # 计算进度百分比 progress_percentage = ( round(completed_work_orders / total_work_orders * 100, 1) if total_work_orders > 0 else 0.0 ) # 统计审批状态 total_approvals = len(approvals) pending_approvals = sum(1 for a in approvals if a.status == ApprovalStatus.PENDING) approved_approvals = sum(1 for a in approvals if a.status == ApprovalStatus.APPROVED) rejected_approvals = sum(1 for a in approvals if a.status == ApprovalStatus.REJECTED) # 物料到位率 = 已批准 / 总申请 material_ready_rate = ( round(approved_approvals / total_approvals * 100, 1) if total_approvals > 0 else 100.0 ) # 延期预警计算 is_overdue = False overdue_days: int | None = None if project.end_date and project.status != ProjectStatus.COMPLETED: if today > project.end_date: is_overdue = True overdue_days = (today - project.end_date).days return ProjectStats( project_id=project.id, project_no=project.project_no, project_name=project.name, status=project.status, start_date=project.start_date, end_date=project.end_date, total_work_orders=total_work_orders, completed_work_orders=completed_work_orders, in_progress_work_orders=in_progress_work_orders, pending_work_orders=pending_work_orders, progress_percentage=progress_percentage, total_approvals=total_approvals, pending_approvals=pending_approvals, approved_approvals=approved_approvals, rejected_approvals=rejected_approvals, material_ready_rate=material_ready_rate, is_overdue=is_overdue, overdue_days=overdue_days, created_at=project.created_at, ) # ============================================================ # 路由定义顺序很重要!更具体的路径必须写在前面! # ============================================================ @router.get("/summary", response_model=ProjectSummary) async def get_project_summary( status: Optional[ProjectStatus] = Query(None, description="项目状态筛选"), db: Session = Depends(get_db_pms) ): """ 获取项目总览汇总数据 包含所有项目的统计信息: - 项目总数、活跃项目数、已完成项目数、延期项目数 - 全局工单统计和进度 - 物料到位率汇总 - 每个项目的详细统计 """ print("Endpoint /summary reached!") # 调试日志 # 查询所有项目 query = db.query(PmsProject) if status: query = query.filter(PmsProject.status == status) projects = query.order_by(PmsProject.created_at.desc()).all() # 无项目时返回空数据 if not projects: return ProjectSummary( total_projects=0, active_projects=0, completed_projects=0, overdue_projects=0, total_work_orders=0, completed_work_orders=0, overall_progress=0.0, total_pending_approvals=0, overall_material_ready_rate=100.0, projects=[], ) # 获取所有项目ID project_ids = [p.id for p in projects] # 查询所有相关工单 all_work_orders = ( db.query(PmsWorkOrder) .filter(PmsWorkOrder.project_id.in_(project_ids)) .all() ) all_work_order_ids = [wo.id for wo in all_work_orders] # 查询所有相关审批 all_approvals = ( db.query(PmsMaterialApproval) .filter(PmsMaterialApproval.work_order_id.in_(all_work_order_ids)) .all() ) # 按项目分组工单 work_orders_by_project: dict[int, list] = {} for wo in all_work_orders: work_orders_by_project.setdefault(wo.project_id, []).append(wo) # 按工单分组审批(需要通过工单找到项目) approvals_by_project: dict[int, list] = {} work_order_to_project: dict[int, int] = {wo.id: wo.project_id for wo in all_work_orders} for ap in all_approvals: project_id = work_order_to_project.get(ap.work_order_id) if project_id: approvals_by_project.setdefault(project_id, []).append(ap) # 计算每个项目的统计 project_stats_list: list[ProjectStats] = [] total_work_orders = 0 completed_work_orders = 0 total_pending_approvals = 0 overdue_projects = 0 for project in projects: wo_list = work_orders_by_project.get(project.id, []) ap_list = approvals_by_project.get(project.id, []) stats = calculate_project_stats(project, wo_list, ap_list) project_stats_list.append(stats) total_work_orders += stats.total_work_orders completed_work_orders += stats.completed_work_orders total_pending_approvals += stats.pending_approvals if stats.is_overdue: overdue_projects += 1 # 计算全局进度 overall_progress = ( round(completed_work_orders / total_work_orders * 100, 1) if total_work_orders > 0 else 0.0 ) # 计算全局物料到位率 total_approvals_count = sum(s.total_approvals for s in project_stats_list) total_approved = sum(s.approved_approvals for s in project_stats_list) overall_material_ready_rate = ( round(total_approved / total_approvals_count * 100, 1) if total_approvals_count > 0 else 100.0 ) return ProjectSummary( total_projects=len(projects), active_projects=sum(1 for p in projects if p.status == ProjectStatus.ACTIVE), completed_projects=sum(1 for p in projects if p.status == ProjectStatus.COMPLETED), overdue_projects=overdue_projects, total_work_orders=total_work_orders, completed_work_orders=completed_work_orders, overall_progress=overall_progress, total_pending_approvals=total_pending_approvals, overall_material_ready_rate=overall_material_ready_rate, projects=project_stats_list, ) @router.get("/{project_id}/stats", response_model=ProjectStats) async def get_project_stats( project_id: int, db: Session = Depends(get_db_pms) ): """获取单个项目的详细统计信息""" print(f"Endpoint /{project_id}/stats reached!") # 调试日志 project = db.query(PmsProject).filter(PmsProject.id == project_id).first() if not project: raise HTTPException(status_code=404, detail=f"Project ID={project_id} not found") # 查询该项目的工单 work_orders = ( db.query(PmsWorkOrder) .filter(PmsWorkOrder.project_id == project_id) .all() ) work_order_ids = [wo.id for wo in work_orders] # 查询该项目的审批 approvals = ( db.query(PmsMaterialApproval) .filter(PmsMaterialApproval.work_order_id.in_(work_order_ids)) .all() ) return calculate_project_stats(project, work_orders, approvals)