Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit 67e411eea8
246 changed files with 23152 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
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}