"""Регламентное обслуживание: узлы машин, их правила, наработка (моточасы), сброс и история.""" from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select from sqlalchemy.orm import Session from .. import config, schemas as S, service from ..db import get_db from ..models import Equipment, MeterReading, ServiceEvent, ServiceUnit, User from ..security import current_user, require_engineer from .catalog import commit, get_or_404 router = APIRouter(tags=["service"]) def unit_out(u: ServiceUnit, eq: Equipment, rate: float | None) -> S.ServiceUnitOut: out = S.ServiceUnitOut.model_validate(u) for k, v in service.evaluate(u, eq.meter_hours or 0, rate).items(): setattr(out, k, v) return out def check_rule(u: ServiceUnit): """Проверка правила после создания/правки (в PATCH поля приходят частично).""" try: S.ServiceUnitIn(name=u.name, mode=u.mode, interval_hours=u.interval_hours, interval_value=u.interval_value, interval_unit=u.interval_unit).check_rule() except ValueError as e: raise HTTPException(422, str(e)) @router.get("/equipment/{id}/service-units", response_model=list[S.ServiceUnitOut]) def list_units(id: int, db: Session = Depends(get_db), _: User = Depends(current_user)): eq = get_or_404(db, Equipment, id) rate = service.daily_rate(db, id) units = db.scalars(select(ServiceUnit).where(ServiceUnit.equipment_id == id).order_by(ServiceUnit.id)).all() out = [unit_out(u, eq, rate) for u in units] return sorted(out, key=lambda x: (not x.is_active, service.state_rank(x.state), -x.percent)) @router.post("/equipment/{id}/service-units", response_model=S.ServiceUnitOut, status_code=201) def create_unit(id: int, data: S.ServiceUnitIn, db: Session = Depends(get_db), me: User = Depends(require_engineer)): eq = get_or_404(db, Equipment, id) try: data.check_rule() except ValueError as e: raise HTTPException(422, str(e)) d = data.model_dump(exclude={"last_service_at", "last_service_hours"}) if data.mode == "hours": d["interval_value"] = d["interval_unit"] = None elif data.mode == "calendar": d["interval_hours"] = None u = ServiceUnit(equipment_id=id, **d) at = data.last_service_at or config.now() hours = eq.meter_hours or 0 if data.last_service_hours is None else data.last_service_hours if hours > (eq.meter_hours or 0): raise HTTPException(422, "Наработка при последнем обслуживании не может быть больше текущей наработки машины") u.last_service_at, u.last_service_hours = at, hours db.add(u) db.flush() db.add(ServiceEvent(unit_id=u.id, at=at, hours=hours, kind="initial", user_id=me.id, note="Начальное состояние")) commit(db) return unit_out(u, eq, service.daily_rate(db, id)) @router.patch("/service-units/{id}", response_model=S.ServiceUnitOut) def update_unit(id: int, data: S.ServiceUnitPatch, db: Session = Depends(get_db), _: User = Depends(require_engineer)): u = get_or_404(db, ServiceUnit, id) for k, v in data.model_dump(exclude_unset=True).items(): if v is None and k in ("name", "mode", "warn_percent", "is_active"): continue setattr(u, k, v) if u.mode == "hours": u.interval_value = u.interval_unit = None elif u.mode == "calendar": u.interval_hours = None check_rule(u) commit(db) eq = db.get(Equipment, u.equipment_id) return unit_out(u, eq, service.daily_rate(db, eq.id)) @router.delete("/service-units/{id}", status_code=204) def delete_unit(id: int, db: Session = Depends(get_db), _: User = Depends(require_engineer)): u = get_or_404(db, ServiceUnit, id) service.unlink_unit(db, id) db.delete(u) commit(db) @router.post("/service-units/{id}/reset", response_model=S.ServiceUnitOut) def reset(id: int, data: S.ServiceResetIn, db: Session = Depends(get_db), me: User = Depends(require_engineer)): """Сброс узла: обслуживание или замена выполнены отдельно от наряда либо при внештатной ситуации.""" u = get_or_404(db, ServiceUnit, id) eq = db.get(Equipment, u.equipment_id) if data.hours is not None and data.hours > (eq.meter_hours or 0): raise HTTPException(422, "Наработка не может быть больше текущей наработки машины — сначала внесите показание счётчика") if data.at and data.at > config.now(): raise HTTPException(422, "Дата обслуживания не может быть в будущем") service.reset_unit(db, u, at=data.at, hours=data.hours, kind="reset", user_id=me.id, note=data.note.strip()) commit(db) return unit_out(u, eq, service.daily_rate(db, eq.id)) @router.get("/service-units/{id}/events", response_model=list[S.ServiceEventOut]) def unit_events(id: int, limit: int = Query(100, ge=1, le=500), db: Session = Depends(get_db), _: User = Depends(current_user)): get_or_404(db, ServiceUnit, id) names = {i: (n or un) for i, n, un in db.execute(select(User.id, User.full_name, User.username))} rows = db.scalars(select(ServiceEvent).where(ServiceEvent.unit_id == id).order_by(ServiceEvent.at.desc(), ServiceEvent.id.desc()).limit(limit)).all() out = [] for e in rows: o = S.ServiceEventOut.model_validate(e) o.user_name = names.get(e.user_id) out.append(o) return out # ---------- наработка (моточасы) ---------- @router.get("/equipment/{id}/meter", response_model=list[S.MeterOut]) def meter_history(id: int, limit: int = Query(50, ge=1, le=500), db: Session = Depends(get_db), _: User = Depends(current_user)): get_or_404(db, Equipment, id) return db.scalars(select(MeterReading).where(MeterReading.equipment_id == id) .order_by(MeterReading.at.desc(), MeterReading.id.desc()).limit(limit)).all() @router.post("/equipment/{id}/meter", response_model=S.EquipmentOut, status_code=201) def add_meter(id: int, data: S.MeterIn, db: Session = Depends(get_db), me: User = Depends(require_engineer)): eq = get_or_404(db, Equipment, id) if data.at and data.at > config.now(): raise HTTPException(422, "Время показания не может быть в будущем") try: service.record_meter(db, eq, data.hours, at=data.at, user_id=me.id, note=data.note.strip(), correction=data.correction) except ValueError as e: raise HTTPException(422, str(e)) commit(db) return eq