from datetime import timedelta from typing import Literal, Optional from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import Response from sqlalchemy.orm import Session from .. import config, report_exec as RE, report_people as RP, reports as R, schemas as S from ..db import get_db from ..models import Department, Equipment, Location, Position, User from ..security import require_engineer router = APIRouter(prefix="/reports", tags=["reports"]) CATALOG = [{"id": "completed-orders", "title": "Выполненные наряды", "description": "Список: машина, чек-лист, кто выполнил, должность, когда. Период обязателен."}, {"id": "equipment-state", "title": "Текущее состояние", "description": "Обслуживаемые узлы машин: что требует обслуживания сейчас, что скоро, остаток и прогноз."}, {"id": "execution", "title": "Выполнение работ", "description": "По оборудованию: процент выполнения чек-листов, суточных осмотров, ТО и заданий, отклонения и комментарии."}, {"id": "people", "title": "По сотрудникам", "description": "Количество и качество выполненных нарядов и задач, трудоёмкость; списком и календарём."}] @router.get("") def list_reports(_: User = Depends(require_engineer)): return CATALOG @router.get("/completed-orders") def completed_orders( date_from: S.datetime = Query(..., alias="from", description="Начало периода (дата и время)"), date_to: S.datetime = Query(..., alias="to", description="Конец периода (дата и время)"), equipment_id: Optional[int] = None, position_id: Optional[int] = None, user_id: Optional[int] = None, include_review: bool = False, format: Literal["json", "pdf", "csv"] = "json", db: Session = Depends(get_db), u: User = Depends(require_engineer)): if date_to <= date_from: raise HTTPException(422, "Конец периода должен быть позже начала") if date_to - date_from > timedelta(days=366): raise HTTPException(422, "Период не должен превышать 366 дней") f = R.Filters(date_from, date_to, equipment_id, position_id, user_id, include_review) rows = R.completed_orders(db, f) stamp = config.now().strftime("%Y%m%d_%H%M") if format == "pdf": data = R.to_pdf("Отчёт: выполненные наряды", f, R.describe_filters(db, f), rows, u.full_name or u.username) return Response(data, media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="completed_orders_{stamp}.pdf"'}) if format == "csv": return Response(R.to_csv(rows), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="completed_orders_{stamp}.csv"'}) return {"from": config.iso(date_from), "to": config.iso(date_to), "total": len(rows), "truncated": len(rows) >= R.MAX_ROWS, "rows": [{**r, "finished_at": config.iso(r["finished_at"])} for r in rows]} @router.get("/equipment-state") def equipment_state(equipment_id: Optional[int] = None, location_id: Optional[int] = None, state: Literal["", "due", "soon", "attention", "ok"] = "", include_inactive: bool = False, format: Literal["json", "pdf", "csv"] = "json", db: Session = Depends(get_db), u: User = Depends(require_engineer)): """Текущее состояние обслуживаемых узлов: что требует обслуживания, что скоро, остаток и прогноз.""" f = R.StateFilters(equipment_id, location_id, state, include_inactive) all_rows = R.equipment_state(db, f) # сводка считается по всем узлам выбранных машин/мест, независимо от фильтра состояния from .. import scope visible = scope.equipment_ids(db, u) # оборудование чужих отделов не показываем if visible is not None: all_rows = [r for r in all_rows if r["equipment_id"] in visible] rows = R.filter_state(all_rows, state) stamp = config.now().strftime("%Y%m%d_%H%M") if format == "csv": return Response(R.state_csv(rows), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="equipment_state_{stamp}.csv"'}) if format == "pdf": txt = [] if equipment_id and (e := db.get(Equipment, equipment_id)): txt.append(f"машина: {e.name}") if location_id and (loc := db.get(Location, location_id)): txt.append(f"место: {loc.name}") txt.append({"": "все узлы", "due": "только «Требуется»", "soon": "только «Скоро»", "attention": "«Требуется» и «Скоро»", "ok": "только «В норме»"}[state]) data = R.state_pdf(f, txt, rows, u.full_name or u.username) return Response(data, media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="equipment_state_{stamp}.pdf"'}) summary = {k: sum(1 for r in all_rows if r["state"] == k) for k in ("due", "soon", "ok", "nodata")} return {"generated_at": config.iso(config.now()), "total": len(rows), "summary": summary, "rows": rows} @router.get("/execution") def execution(date_from: S.datetime = Query(..., alias="from"), date_to: S.datetime = Query(..., alias="to"), equipment_id: Optional[int] = None, location_id: Optional[int] = None, format: Literal["json", "pdf", "csv"] = "json", db: Session = Depends(get_db), u: User = Depends(require_engineer)): """Выполнение чек-листов, суточных осмотров, ТО и суточных заданий по оборудованию: % и отклонения.""" if date_to <= date_from: raise HTTPException(422, "Конец периода должен быть позже начала") if date_to - date_from > timedelta(days=366): raise HTTPException(422, "Период не должен превышать 366 дней") data = RE.execution(db, date_from, date_to, equipment_id, location_id) stamp = config.now().strftime("%Y%m%d_%H%M") if format == "csv": return Response(RE.to_csv(data), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="execution_{stamp}.csv"'}) if format == "pdf": txt = [] if equipment_id and (e := db.get(Equipment, equipment_id)): txt.append(f"машина: {e.name}") if location_id and (loc := db.get(Location, location_id)): txt.append(f"место: {loc.name}") return Response(RE.to_pdf(data, date_from, date_to, txt, u.full_name or u.username), media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="execution_{stamp}.pdf"'}) return {"from": config.iso(date_from), "to": config.iso(date_to), **data} @router.get("/people") def people(date_from: S.datetime = Query(..., alias="from"), date_to: S.datetime = Query(..., alias="to"), department_id: Optional[int] = None, position_id: Optional[int] = None, user_id: Optional[int] = None, format: Literal["json", "pdf", "csv", "csv_calendar"] = "json", db: Session = Depends(get_db), u: User = Depends(require_engineer)): """По сотрудникам: количество и качество выполненных нарядов и задач за период, трудоёмкость, календарь по дням.""" if date_to <= date_from: raise HTTPException(422, "Конец периода должен быть позже начала") if date_to - date_from > timedelta(days=366): raise HTTPException(422, "Период не должен превышать 366 дней") data = RP.people(db, date_from, date_to, department_id, position_id, user_id) stamp = config.now().strftime("%Y%m%d_%H%M") if format in ("csv", "csv_calendar"): return Response(RP.to_csv(data, calendar=format == "csv_calendar"), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="people_{stamp}.csv"'}) if format == "pdf": txt = [] for model, val, label in ((Department, department_id, "отдел"), (Position, position_id, "должность"), (User, user_id, "сотрудник")): if val and (x := db.get(model, val)): txt.append(f"{label}: {getattr(x, 'full_name', None) or getattr(x, 'name', None) or getattr(x, 'username', '')}") return Response(RP.to_pdf(data, date_from, date_to, txt, u.full_name or u.username), media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="people_{stamp}.pdf"'}) return {"from": config.iso(date_from), "to": config.iso(date_to), **data}