Files
tech-service-portal/app/routers/reports.py
T

136 lines
9.4 KiB
Python

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}