99 lines
6.9 KiB
Python
99 lines
6.9 KiB
Python
"""История машины: всё, что с ней происходило — инциденты, обслуживание узлов, наработка, наряды, суточные задания."""
|
||
from datetime import date, datetime, time, timedelta
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, Query
|
||
from sqlalchemy import select
|
||
from sqlalchemy.orm import Session
|
||
|
||
from .. import config, service
|
||
from ..db import get_db
|
||
from ..models import DailyTask, Equipment, Incident, MeterReading, Order, ServiceEvent, ServiceUnit, TaskEvent, User
|
||
from ..security import current_user
|
||
from .catalog import get_or_404
|
||
from .inventory import user_names
|
||
|
||
router = APIRouter(tags=["history"])
|
||
|
||
TYPES = {"incident": "Отказ / неисправность", "service": "Обслуживание узла", "meter": "Наработка", "order": "Наряд", "task": "Задача"}
|
||
ORDER_RU = {"new": "создан", "completed": "выполнен", "cancelled": "отменён"}
|
||
TASK_RU = {"assigned": "назначено", "in_progress": "в работе", "paused": "приостановлено", "done": "выполнено", "cancelled": "отменено"}
|
||
SVC_RU = {"initial": "начальное состояние", "order": "обслужен по наряду", "reset": "сброс (обслужен вне наряда)", "incident": "заменён при отказе"}
|
||
SOURCE_RU = {"public": "через публичную форму", "api": "из внешней системы"}
|
||
|
||
|
||
def collect(db: Session, eq: Equipment, types: set[str], start: datetime | None, end: datetime | None) -> list[dict]:
|
||
names = user_names(db)
|
||
ev: list[dict] = []
|
||
|
||
def add(at, typ, title, text="", who=None, **ref):
|
||
if at is None or (start and at < start) or (end and at >= end):
|
||
return
|
||
ev.append({"at": at, "type": typ, "type_ru": TYPES[typ], "title": title, "text": text, "user_name": names.get(who), **ref})
|
||
|
||
if "incident" in types:
|
||
for i in db.scalars(select(Incident).where(Incident.equipment_id == eq.id)).all():
|
||
units = ", ".join(u.unit_name + (" (заменён)" if u.replaced else "") for u in i.units)
|
||
src = SOURCE_RU.get(i.source, "")
|
||
text = "; ".join(x for x in (i.description, f"узлы: {units}" if units else "",
|
||
("замена: " + (i.replacement_note or "да")) if i.replaced else "",
|
||
f"{src}: {i.reporter_name} {i.reporter_contact}".strip() if src else "") if x)
|
||
what = "Неисправность" if i.kind == "fault" else "Отказ"
|
||
add(i.occurred_at, "incident", f"{what}: {i.title}", text, i.author_id, incident_id=i.id,
|
||
severity="warn" if i.kind == "fault" else "bad")
|
||
if i.status == "closed":
|
||
add(i.closed_at, "incident", f"{what} закрыт{'а' if i.kind == 'fault' else ''}: {i.title}", i.close_note,
|
||
i.closed_by_id, incident_id=i.id, severity="ok")
|
||
if "service" in types:
|
||
units = {u.id: u for u in db.scalars(select(ServiceUnit).where(ServiceUnit.equipment_id == eq.id)).all()}
|
||
if units:
|
||
for e in db.scalars(select(ServiceEvent).where(ServiceEvent.unit_id.in_(units))).all():
|
||
add(e.at, "service", f"{units[e.unit_id].name}: {SVC_RU.get(e.kind, e.kind)}",
|
||
f"при наработке {service.num(e.hours)} м/ч" + (f"; {e.note}" if e.note and e.kind != "order" else ""),
|
||
e.user_id, unit_id=e.unit_id, order_id=e.order_id, severity="warn" if e.kind == "incident" else "info")
|
||
if "meter" in types:
|
||
for m in db.scalars(select(MeterReading).where(MeterReading.equipment_id == eq.id)).all():
|
||
src = {"manual": "внесено вручную", "order": f"по наряду №{m.order_id}", "correction": "коррекция счётчика"}.get(m.source, m.source)
|
||
add(m.at, "meter", f"Наработка {service.num(m.hours)} м/ч", src + (f"; {m.note}" if m.note else ""), m.user_id, order_id=m.order_id)
|
||
if "order" in types:
|
||
for o in db.scalars(select(Order).where(Order.equipment_id == eq.id)).all():
|
||
add(o.created_at, "order", f"Наряд №{o.id} создан: {o.title}", "", None, order_id=o.id)
|
||
if o.status == "completed":
|
||
add(o.completed_at, "order", f"Наряд №{o.id} выполнен: {o.title}", o.review_comment, o.executor_id, order_id=o.id, severity="ok")
|
||
elif o.status == "cancelled":
|
||
add(o.updated_at or o.created_at, "order", f"Наряд №{o.id} отменён: {o.title}", "", None, order_id=o.id)
|
||
if "task" in types:
|
||
tasks = {t.id: t for t in db.scalars(select(DailyTask).where(DailyTask.equipment_id == eq.id)).all()}
|
||
if tasks:
|
||
for e in db.scalars(select(TaskEvent).where(TaskEvent.task_id.in_(tasks))).all():
|
||
t = tasks[e.task_id]
|
||
if e.kind == "transfer":
|
||
what = f"передано: {names.get(e.from_user_id, '—')} → {names.get(e.to_user_id, '—')}"
|
||
else:
|
||
what = TASK_RU.get(e.to_status, e.to_status)
|
||
add(e.at, "task", f"Задание «{t.title}»: {what}", e.comment, e.user_id, task_id=t.id,
|
||
severity="ok" if e.to_status == "done" and e.kind != "transfer" else "")
|
||
ev.sort(key=lambda x: x["at"], reverse=True)
|
||
return ev
|
||
|
||
|
||
@router.get("/equipment/{id}/history")
|
||
def equipment_history(id: int, types: Optional[str] = Query(None, description="через запятую: incident,service,meter,order,task"),
|
||
date_from: Optional[date] = Query(None, alias="from"), date_to: Optional[date] = Query(None, alias="to"),
|
||
q: Optional[str] = Query(None, max_length=100), limit: int = Query(300, ge=1, le=2000),
|
||
db: Session = Depends(get_db), _: User = Depends(current_user)):
|
||
"""Хронология машины (новые сверху) с фильтрами по типу события, периоду (даты включительно) и тексту."""
|
||
eq = get_or_404(db, Equipment, id)
|
||
want = set(TYPES) if types is None else {t for t in types.split(",") if t in TYPES} # пустой выбор — пусто
|
||
start = datetime.combine(date_from, time.min) if date_from else None
|
||
end = datetime.combine(date_to + timedelta(days=1), time.min) if date_to else None
|
||
rows = collect(db, eq, want, start, end)
|
||
if q and q.strip():
|
||
s = q.strip().lower()
|
||
rows = [r for r in rows if s in (r["title"] + " " + r["text"] + " " + (r["user_name"] or "")).lower()]
|
||
counts = {t: 0 for t in TYPES}
|
||
for r in rows:
|
||
counts[r["type"]] += 1
|
||
return {"equipment": {"id": eq.id, "name": eq.name}, "total": len(rows), "counts": counts, "types": TYPES,
|
||
"rows": [{**r, "at": config.iso(r["at"])} for r in rows[:limit]]}
|