Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""История машины: всё, что с ней происходило — инциденты, обслуживание узлов, наработка, наряды, суточные задания."""
|
||||
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]]}
|
||||
Reference in New Issue
Block a user