feat(maintenance): MAINT-Epic (Wartungspläne/-aufträge, Werkstatt) + Akte-Redesign
Neues Wartungskonzept: Wartungsplan+Position+Intervall (zeit/km/ Betriebsstunden, kombinierbar - fällig sobald ein Kriterium erreicht ist), Wartungsauftrag mit intern/externer Werkstatt, Status offen/ in_arbeit/erledigt, Kosten, Historie, Dokument-Anhang. Ad-hoc-Aufträge ohne Plan möglich (z.B. kleinere Reparatur). Admin-Tab "Wartungspläne". Objektakte-Redesign (Epic 21, Nutzer-Feedback "wirkt nicht modern"): CSS-Grid statt Karten-Stapel, einspaltig mobil, zweispaltig ab 900px; Fahrzeug-Objekte zeigen Wartung/Fahrzeugdaten prominent, Geräte/ Rucksäcke die Beladung. Dabei gefundene Lücke: Beladung (Soll/Ist je Position) fehlte in der Akte komplett, nur über eine laufende Kontrolle sichtbar - jetzt eigene Karte. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""MAINT-001..006: Wartungsplaene, Intervalle, Wartungsauftraege.
|
||||
|
||||
Revision ID: 0022_wartung
|
||||
Revises: 0021_ausgabe
|
||||
Create Date: 2026-09-06
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0022_wartung"
|
||||
down_revision: Union[str, None] = "0021_ausgabe"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("CREATE TYPE intervall_typ AS ENUM ('zeit', 'km', 'betriebsstunden')")
|
||||
op.execute("CREATE TYPE wartungsauftrag_status AS ENUM ('offen', 'in_arbeit', 'erledigt')")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE wartungsplan (
|
||||
id SERIAL PRIMARY KEY,
|
||||
objekttyp_id INTEGER NOT NULL REFERENCES objekttyp(id),
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE wartungsplan_position (
|
||||
id SERIAL PRIMARY KEY,
|
||||
wartungsplan_id INTEGER NOT NULL REFERENCES wartungsplan(id),
|
||||
wartungsart TEXT NOT NULL,
|
||||
beschreibung TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE wartungsintervall (
|
||||
id SERIAL PRIMARY KEY,
|
||||
wartungsplan_position_id INTEGER NOT NULL REFERENCES wartungsplan_position(id),
|
||||
typ intervall_typ NOT NULL,
|
||||
wert INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE wartungsauftrag (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
|
||||
geraet_instanz_id UUID REFERENCES geraet_instanz(id),
|
||||
wartungsplan_position_id INTEGER REFERENCES wartungsplan_position(id),
|
||||
wartungsart TEXT NOT NULL,
|
||||
beschreibung TEXT,
|
||||
status wartungsauftrag_status NOT NULL DEFAULT 'offen',
|
||||
werkstatt_intern BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
werkstatt_name TEXT,
|
||||
faelligkeit_am DATE,
|
||||
erstellt_am TIMESTAMPTZ NOT NULL,
|
||||
erledigt_am TIMESTAMPTZ,
|
||||
durchgefuehrt_von INTEGER REFERENCES benutzer(id),
|
||||
kosten NUMERIC,
|
||||
kilometerstand_bei_erledigung INTEGER,
|
||||
betriebsstunden_bei_erledigung NUMERIC
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX idx_wartungsauftrag_objekt ON wartungsauftrag (objekt_id)")
|
||||
op.execute("CREATE INDEX idx_wartungsauftrag_status ON wartungsauftrag (status)")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE wartungsauftrag")
|
||||
op.execute("DROP TABLE wartungsintervall")
|
||||
op.execute("DROP TABLE wartungsplan_position")
|
||||
op.execute("DROP TABLE wartungsplan")
|
||||
op.execute("DROP TYPE wartungsauftrag_status")
|
||||
op.execute("DROP TYPE intervall_typ")
|
||||
@@ -21,6 +21,7 @@ from app.api.v1.endpoints import (
|
||||
personal,
|
||||
stammdaten,
|
||||
vorlagen,
|
||||
wartung,
|
||||
zustaendigkeit,
|
||||
)
|
||||
|
||||
@@ -46,3 +47,4 @@ api_router.include_router(permission.router, tags=["permission"])
|
||||
api_router.include_router(akte.router, tags=["akte"])
|
||||
api_router.include_router(lager.router, tags=["lager"])
|
||||
api_router.include_router(ausgabe.router, tags=["ausgabe"])
|
||||
api_router.include_router(wartung.router, tags=["wartung"])
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_current_user, require_roles
|
||||
from app.db.session import get_db
|
||||
from app.models.auth import Benutzer, RolleTyp
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.wartung import Wartungsauftrag, Wartungsplan, WartungsauftragStatus
|
||||
from app.schemas.wartung import (
|
||||
FaelligeWartungRead,
|
||||
WartungsauftragCreate,
|
||||
WartungsauftragErledigen,
|
||||
WartungsauftragRead,
|
||||
WartungsauftragStatusUpdate,
|
||||
WartungsplanCreate,
|
||||
WartungsplanPositionRead,
|
||||
WartungsplanRead,
|
||||
)
|
||||
from app.services.wartung import (
|
||||
aendere_status,
|
||||
erledige_wartungsauftrag,
|
||||
erstelle_wartungsauftrag,
|
||||
erstelle_wartungsplan,
|
||||
faellige_wartungen_fuer_objekt,
|
||||
hole_intervalle,
|
||||
hole_positionen_mit_intervallen,
|
||||
liste_wartungsauftraege,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_verantwortliche = require_roles(
|
||||
RolleTyp.administration, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher
|
||||
)
|
||||
|
||||
|
||||
async def _wartungsplan_read(db: AsyncSession, plan: Wartungsplan) -> WartungsplanRead:
|
||||
positionen = await hole_positionen_mit_intervallen(db, plan.id)
|
||||
positionen_read = []
|
||||
for position in positionen:
|
||||
intervalle = await hole_intervalle(db, position.id)
|
||||
positionen_read.append(
|
||||
WartungsplanPositionRead(
|
||||
id=position.id,
|
||||
wartungsplan_id=position.wartungsplan_id,
|
||||
wartungsart=position.wartungsart,
|
||||
beschreibung=position.beschreibung,
|
||||
intervalle=list(intervalle),
|
||||
)
|
||||
)
|
||||
return WartungsplanRead(
|
||||
id=plan.id, objekttyp_id=plan.objekttyp_id, name=plan.name, positionen=positionen_read
|
||||
)
|
||||
|
||||
|
||||
@router.get("/wartungsplaene", response_model=list[WartungsplanRead])
|
||||
async def liste_wartungsplaene(
|
||||
objekttyp_id: int | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
) -> list[WartungsplanRead]:
|
||||
stmt = select(Wartungsplan)
|
||||
if objekttyp_id is not None:
|
||||
stmt = stmt.where(Wartungsplan.objekttyp_id == objekttyp_id)
|
||||
result = await db.execute(stmt)
|
||||
return [await _wartungsplan_read(db, p) for p in result.scalars().all()]
|
||||
|
||||
|
||||
@router.post("/wartungsplaene", response_model=WartungsplanRead, status_code=status.HTTP_201_CREATED)
|
||||
async def erstelle_neuer_wartungsplan(
|
||||
payload: WartungsplanCreate, db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)
|
||||
) -> WartungsplanRead:
|
||||
plan = await erstelle_wartungsplan(
|
||||
db, objekttyp_id=payload.objekttyp_id, name=payload.name, positionen=payload.positionen
|
||||
)
|
||||
return await _wartungsplan_read(db, plan)
|
||||
|
||||
|
||||
@router.get("/objekte/{objekt_id}/faellige-wartungen", response_model=list[FaelligeWartungRead])
|
||||
async def hole_faellige_wartungen(
|
||||
objekt_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
||||
) -> list[dict]:
|
||||
objekt = await db.get(Objekt, objekt_id)
|
||||
if objekt is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
||||
return await faellige_wartungen_fuer_objekt(db, objekt)
|
||||
|
||||
|
||||
@router.get("/wartungsauftraege", response_model=list[WartungsauftragRead])
|
||||
async def liste_auftraege(
|
||||
objekt_id: int | None = None,
|
||||
status_filter: WartungsauftragStatus | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
) -> list[Wartungsauftrag]:
|
||||
return await liste_wartungsauftraege(db, objekt_id=objekt_id, status=status_filter)
|
||||
|
||||
|
||||
@router.post("/wartungsauftraege", response_model=WartungsauftragRead, status_code=status.HTTP_201_CREATED)
|
||||
async def erstelle_auftrag(
|
||||
payload: WartungsauftragCreate, db: AsyncSession = Depends(get_db), _=Depends(_verantwortliche)
|
||||
) -> Wartungsauftrag:
|
||||
objekt = await db.get(Objekt, payload.objekt_id)
|
||||
if objekt is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
||||
return await erstelle_wartungsauftrag(
|
||||
db,
|
||||
objekt_id=payload.objekt_id,
|
||||
geraet_instanz_id=payload.geraet_instanz_id,
|
||||
wartungsplan_position_id=payload.wartungsplan_position_id,
|
||||
wartungsart=payload.wartungsart,
|
||||
beschreibung=payload.beschreibung,
|
||||
werkstatt_intern=payload.werkstatt_intern,
|
||||
werkstatt_name=payload.werkstatt_name,
|
||||
faelligkeit_am=payload.faelligkeit_am,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/wartungsauftraege/{auftrag_id}/status", response_model=WartungsauftragRead)
|
||||
async def route_status_aendern(
|
||||
auftrag_id: uuid.UUID,
|
||||
payload: WartungsauftragStatusUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(_verantwortliche),
|
||||
) -> Wartungsauftrag:
|
||||
auftrag = await db.get(Wartungsauftrag, auftrag_id)
|
||||
if auftrag is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Wartungsauftrag nicht gefunden")
|
||||
return await aendere_status(db, auftrag=auftrag, status=payload.status)
|
||||
|
||||
|
||||
@router.post("/wartungsauftraege/{auftrag_id}/erledigen", response_model=WartungsauftragRead)
|
||||
async def route_erledigen(
|
||||
auftrag_id: uuid.UUID,
|
||||
payload: WartungsauftragErledigen,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Benutzer = Depends(_verantwortliche),
|
||||
) -> Wartungsauftrag:
|
||||
auftrag = await db.get(Wartungsauftrag, auftrag_id)
|
||||
if auftrag is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Wartungsauftrag nicht gefunden")
|
||||
return await erledige_wartungsauftrag(
|
||||
db,
|
||||
auftrag=auftrag,
|
||||
benutzer_id=current_user.id,
|
||||
erledigt_am=payload.erledigt_am,
|
||||
kosten=payload.kosten,
|
||||
kilometerstand_bei_erledigung=payload.kilometerstand_bei_erledigung,
|
||||
betriebsstunden_bei_erledigung=payload.betriebsstunden_bei_erledigung,
|
||||
)
|
||||
@@ -24,6 +24,14 @@ from app.models.personal import (
|
||||
)
|
||||
from app.models.stammdaten import Bereich, Kategorie, Material, MaterialTyp, Objekttyp, Standort
|
||||
from app.models.vorlage import Beladungsvorlage, VorlageStatus, Vorlagenposition
|
||||
from app.models.wartung import (
|
||||
IntervallTyp,
|
||||
Wartungsauftrag,
|
||||
WartungsauftragStatus,
|
||||
Wartungsintervall,
|
||||
Wartungsplan,
|
||||
WartungsplanPosition,
|
||||
)
|
||||
from app.models.zustaendigkeit import Kontrollverantwortung, Zustaendigkeit
|
||||
|
||||
__all__ = [
|
||||
@@ -81,4 +89,10 @@ __all__ = [
|
||||
"Vorlagenposition",
|
||||
"Kontrollverantwortung",
|
||||
"Zustaendigkeit",
|
||||
"IntervallTyp",
|
||||
"Wartungsauftrag",
|
||||
"WartungsauftragStatus",
|
||||
"Wartungsintervall",
|
||||
"Wartungsplan",
|
||||
"WartungsplanPosition",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, Numeric, String, Text
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PgEnum, TIMESTAMP, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class IntervallTyp(str, enum.Enum):
|
||||
zeit = "zeit"
|
||||
km = "km"
|
||||
betriebsstunden = "betriebsstunden"
|
||||
|
||||
|
||||
class WartungsauftragStatus(str, enum.Enum):
|
||||
offen = "offen"
|
||||
in_arbeit = "in_arbeit"
|
||||
erledigt = "erledigt"
|
||||
|
||||
|
||||
intervall_typ_pg = PgEnum(IntervallTyp, name="intervall_typ", create_type=False)
|
||||
wartungsauftrag_status_pg = PgEnum(WartungsauftragStatus, name="wartungsauftrag_status", create_type=False)
|
||||
|
||||
|
||||
class Wartungsplan(Base):
|
||||
__tablename__ = "wartungsplan"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
objekttyp_id: Mapped[int] = mapped_column(ForeignKey("objekttyp.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
|
||||
|
||||
class WartungsplanPosition(Base):
|
||||
__tablename__ = "wartungsplan_position"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
wartungsplan_id: Mapped[int] = mapped_column(ForeignKey("wartungsplan.id"), nullable=False)
|
||||
wartungsart: Mapped[str] = mapped_column(String, nullable=False)
|
||||
beschreibung: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class Wartungsintervall(Base):
|
||||
__tablename__ = "wartungsintervall"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
wartungsplan_position_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("wartungsplan_position.id"), nullable=False
|
||||
)
|
||||
typ: Mapped[IntervallTyp] = mapped_column(intervall_typ_pg, nullable=False)
|
||||
# zeit: Monate. km/betriebsstunden: Einheit des jeweiligen Fahrzeugdetails-Feldes.
|
||||
wert: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
|
||||
class Wartungsauftrag(Base):
|
||||
__tablename__ = "wartungsauftrag"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
objekt_id: Mapped[int] = mapped_column(ForeignKey("objekt.id"), nullable=False)
|
||||
geraet_instanz_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("geraet_instanz.id")
|
||||
)
|
||||
wartungsplan_position_id: Mapped[int | None] = mapped_column(ForeignKey("wartungsplan_position.id"))
|
||||
wartungsart: Mapped[str] = mapped_column(String, nullable=False)
|
||||
beschreibung: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[WartungsauftragStatus] = mapped_column(
|
||||
wartungsauftrag_status_pg, nullable=False, default=WartungsauftragStatus.offen
|
||||
)
|
||||
werkstatt_intern: Mapped[bool] = mapped_column(nullable=False, default=True)
|
||||
werkstatt_name: Mapped[str | None] = mapped_column(String)
|
||||
faelligkeit_am: Mapped[date | None] = mapped_column()
|
||||
erstellt_am: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
|
||||
erledigt_am: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True))
|
||||
durchgefuehrt_von: Mapped[int | None] = mapped_column(ForeignKey("benutzer.id"))
|
||||
kosten: Mapped[Decimal | None] = mapped_column(Numeric)
|
||||
kilometerstand_bei_erledigung: Mapped[int | None] = mapped_column(Integer)
|
||||
betriebsstunden_bei_erledigung: Mapped[Decimal | None] = mapped_column(Numeric)
|
||||
@@ -1,4 +1,6 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
@@ -17,6 +19,16 @@ class ZustaendigerBenutzer(BaseModel):
|
||||
quelle: str # "standort" oder "objekt" (Vereinigungslogik Karte 04)
|
||||
|
||||
|
||||
class BeladungsPosition(BaseModel):
|
||||
objektposition_id: uuid.UUID
|
||||
material_id: int
|
||||
material_name: str
|
||||
einheit: str
|
||||
fach: str | None
|
||||
sollmenge: Decimal
|
||||
istmenge: Decimal
|
||||
|
||||
|
||||
class AkteRead(BaseModel):
|
||||
"""FILE-001..006: gebündelte Aggregations-Sicht über alle bereits existierenden
|
||||
Teilbereiche einer Ressource (aktuell nur Objekttyp "objekt" - Prompt 24/Backlog-
|
||||
@@ -36,3 +48,4 @@ class AkteRead(BaseModel):
|
||||
dokumente: list[DokumentRead]
|
||||
geraete: list[GeraetInstanzRead]
|
||||
maengel: list[MangelRead]
|
||||
beladung: list[BeladungsPosition]
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict
|
||||
# Validierung) - jeder Ressourcentyp, an den Dokumente angehängt werden dürfen,
|
||||
# muss hier explizit freigeschaltet werden.
|
||||
EntitaetTyp = Literal[
|
||||
"objekt", "objektposition", "geraet_instanz", "mangel", "fahrzeugdetails", "benutzer"
|
||||
"objekt", "objektposition", "geraet_instanz", "mangel", "fahrzeugdetails", "benutzer", "wartungsauftrag"
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.models.wartung import IntervallTyp, WartungsauftragStatus
|
||||
|
||||
|
||||
class WartungsintervallCreate(BaseModel):
|
||||
typ: IntervallTyp
|
||||
wert: int
|
||||
|
||||
|
||||
class WartungsintervallRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
wartungsplan_position_id: int
|
||||
typ: IntervallTyp
|
||||
wert: int
|
||||
|
||||
|
||||
class WartungsplanPositionCreate(BaseModel):
|
||||
wartungsart: str
|
||||
beschreibung: str | None = None
|
||||
intervalle: list[WartungsintervallCreate] = []
|
||||
|
||||
|
||||
class WartungsplanPositionRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
wartungsplan_id: int
|
||||
wartungsart: str
|
||||
beschreibung: str | None
|
||||
intervalle: list[WartungsintervallRead] = []
|
||||
|
||||
|
||||
class WartungsplanCreate(BaseModel):
|
||||
objekttyp_id: int
|
||||
name: str
|
||||
positionen: list[WartungsplanPositionCreate] = []
|
||||
|
||||
|
||||
class WartungsplanRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
objekttyp_id: int
|
||||
name: str
|
||||
positionen: list[WartungsplanPositionRead] = []
|
||||
|
||||
|
||||
class WartungsauftragCreate(BaseModel):
|
||||
objekt_id: int
|
||||
geraet_instanz_id: uuid.UUID | None = None
|
||||
wartungsplan_position_id: int | None = None
|
||||
wartungsart: str
|
||||
beschreibung: str | None = None
|
||||
werkstatt_intern: bool = True
|
||||
werkstatt_name: str | None = None
|
||||
faelligkeit_am: date | None = None
|
||||
|
||||
|
||||
class WartungsauftragErledigen(BaseModel):
|
||||
erledigt_am: date | None = None
|
||||
kosten: Decimal | None = None
|
||||
kilometerstand_bei_erledigung: int | None = None
|
||||
betriebsstunden_bei_erledigung: Decimal | None = None
|
||||
|
||||
|
||||
class WartungsauftragStatusUpdate(BaseModel):
|
||||
status: WartungsauftragStatus
|
||||
|
||||
|
||||
class WartungsauftragRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
objekt_id: int
|
||||
geraet_instanz_id: uuid.UUID | None
|
||||
wartungsplan_position_id: int | None
|
||||
wartungsart: str
|
||||
beschreibung: str | None
|
||||
status: WartungsauftragStatus
|
||||
werkstatt_intern: bool
|
||||
werkstatt_name: str | None
|
||||
faelligkeit_am: date | None
|
||||
erstellt_am: datetime
|
||||
erledigt_am: datetime | None
|
||||
durchgefuehrt_von: int | None
|
||||
kosten: Decimal | None
|
||||
kilometerstand_bei_erledigung: int | None
|
||||
betriebsstunden_bei_erledigung: Decimal | None
|
||||
|
||||
|
||||
class FaelligeWartungRead(BaseModel):
|
||||
wartungsplan_position_id: int
|
||||
wartungsart: str
|
||||
objekt_id: int
|
||||
faellig: bool
|
||||
faellig_am: date | None
|
||||
faellig_bei_km: int | None
|
||||
faellig_bei_betriebsstunden: Decimal | None
|
||||
@@ -9,9 +9,10 @@ from app.models.historie import Historie
|
||||
from app.models.mangel import Mangel
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.objektposition import Objektposition
|
||||
from app.models.stammdaten import Objekttyp, Standort
|
||||
from app.models.stammdaten import Material, Objekttyp, Standort
|
||||
from app.models.vorlage import Vorlagenposition
|
||||
from app.models.zustaendigkeit import Kontrollverantwortung, Zustaendigkeit
|
||||
from app.schemas.akte import AkteRead, ZustaendigerBenutzer
|
||||
from app.schemas.akte import AkteRead, BeladungsPosition, ZustaendigerBenutzer
|
||||
from app.schemas.dokument import DokumentRead
|
||||
from app.schemas.geraet_instanz import GeraetInstanzRead
|
||||
from app.schemas.historie import HistorieRead
|
||||
@@ -19,7 +20,7 @@ from app.schemas.mangel import MangelRead
|
||||
from app.schemas.objekt import FahrzeugdetailsRead, ObjektRead
|
||||
from app.schemas.zustaendigkeit import KontrollverantwortungRead
|
||||
from app.services.dashboard import objekt_readiness
|
||||
from app.services.objekte import letzte_kontrollen_map
|
||||
from app.services.objekte import letzte_kontrollen_map, sollmenge_effektiv
|
||||
|
||||
|
||||
async def _zustaendige_benutzer(db: AsyncSession, objekt: Objekt) -> list[ZustaendigerBenutzer]:
|
||||
@@ -90,6 +91,31 @@ async def akte_fuer_objekt(db: AsyncSession, objekt_id: int) -> AkteRead | None:
|
||||
|
||||
maengel_result = await db.execute(select(Mangel).where(Mangel.objekt_id == objekt_id))
|
||||
|
||||
fach_by_material_id: dict[int, str | None] = {}
|
||||
if objekt.vorlage_id is not None:
|
||||
vorlagenpositionen_result = await db.execute(
|
||||
select(Vorlagenposition).where(Vorlagenposition.vorlage_id == objekt.vorlage_id)
|
||||
)
|
||||
fach_by_material_id = {vp.material_id: vp.fach for vp in vorlagenpositionen_result.scalars().all()}
|
||||
|
||||
beladung_result = await db.execute(
|
||||
select(Objektposition, Material)
|
||||
.join(Material, Objektposition.material_id == Material.id)
|
||||
.where(Objektposition.objekt_id == objekt_id)
|
||||
)
|
||||
beladung = [
|
||||
BeladungsPosition(
|
||||
objektposition_id=position.id,
|
||||
material_id=material.id,
|
||||
material_name=material.name,
|
||||
einheit=material.einheit,
|
||||
fach=fach_by_material_id.get(material.id),
|
||||
sollmenge=sollmenge_effektiv(position),
|
||||
istmenge=position.istmenge,
|
||||
)
|
||||
for position, material in beladung_result.all()
|
||||
]
|
||||
|
||||
readiness = await objekt_readiness(db, objekt_id)
|
||||
|
||||
objekt_read = ObjektRead.model_validate(objekt).model_copy(
|
||||
@@ -115,4 +141,5 @@ async def akte_fuer_objekt(db: AsyncSession, objekt_id: int) -> AkteRead | None:
|
||||
dokumente=[DokumentRead.model_validate(d) for d in dokumente_result.scalars().all()],
|
||||
geraete=[GeraetInstanzRead.model_validate(g) for g in geraete],
|
||||
maengel=[MangelRead.model_validate(m) for m in maengel_result.scalars().all()],
|
||||
beladung=beladung,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import calendar
|
||||
from datetime import date
|
||||
|
||||
|
||||
def plus_monate(stichtag: date, monate: int) -> date:
|
||||
monats_index = stichtag.month - 1 + monate
|
||||
jahr = stichtag.year + monats_index // 12
|
||||
monat = monats_index % 12 + 1
|
||||
letzter_tag = calendar.monthrange(jahr, monat)[1]
|
||||
return date(jahr, monat, min(stichtag.day, letzter_tag))
|
||||
@@ -1,4 +1,3 @@
|
||||
import calendar
|
||||
import uuid
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
@@ -12,17 +11,7 @@ from app.models.objekt import Objekt
|
||||
from app.models.objektposition import Objektposition
|
||||
from app.services import benachrichtigung as benachrichtigung_service
|
||||
from app.services import historie as historie_service
|
||||
|
||||
|
||||
def _plus_monate(stichtag: date, monate: int) -> date:
|
||||
"""Kein python-dateutil als Dependency für eine einzelne Monatsaddition -
|
||||
Tag wird auf das Monatsende gekappt, falls der Zielmonat kürzer ist
|
||||
(z.B. 31. Januar + 1 Monat -> 28./29. Februar)."""
|
||||
monats_index = stichtag.month - 1 + monate
|
||||
jahr = stichtag.year + monats_index // 12
|
||||
monat = monats_index % 12 + 1
|
||||
letzter_tag = calendar.monthrange(jahr, monat)[1]
|
||||
return date(jahr, monat, min(stichtag.day, letzter_tag))
|
||||
from app.services.datumshilfen import plus_monate as _plus_monate
|
||||
|
||||
|
||||
async def liste_fuer_position(db: AsyncSession, objektposition_id) -> list[GeraetInstanz]:
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.fahrzeugdetails import Fahrzeugdetails
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.wartung import (
|
||||
IntervallTyp,
|
||||
Wartungsauftrag,
|
||||
WartungsauftragStatus,
|
||||
Wartungsintervall,
|
||||
Wartungsplan,
|
||||
WartungsplanPosition,
|
||||
)
|
||||
from app.schemas.wartung import WartungsplanPositionCreate
|
||||
from app.services.datumshilfen import plus_monate
|
||||
|
||||
|
||||
async def erstelle_wartungsplan(
|
||||
db: AsyncSession, *, objekttyp_id: int, name: str, positionen: list[WartungsplanPositionCreate]
|
||||
) -> Wartungsplan:
|
||||
plan = Wartungsplan(objekttyp_id=objekttyp_id, name=name)
|
||||
db.add(plan)
|
||||
await db.flush()
|
||||
for pos in positionen:
|
||||
position = WartungsplanPosition(
|
||||
wartungsplan_id=plan.id, wartungsart=pos.wartungsart, beschreibung=pos.beschreibung
|
||||
)
|
||||
db.add(position)
|
||||
await db.flush()
|
||||
for intervall in pos.intervalle:
|
||||
db.add(
|
||||
Wartungsintervall(
|
||||
wartungsplan_position_id=position.id, typ=intervall.typ, wert=intervall.wert
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
return plan
|
||||
|
||||
|
||||
async def hole_positionen_mit_intervallen(db: AsyncSession, wartungsplan_id: int) -> list[WartungsplanPosition]:
|
||||
result = await db.execute(
|
||||
select(WartungsplanPosition).where(WartungsplanPosition.wartungsplan_id == wartungsplan_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def hole_intervalle(db: AsyncSession, wartungsplan_position_id: int) -> list[Wartungsintervall]:
|
||||
result = await db.execute(
|
||||
select(Wartungsintervall).where(Wartungsintervall.wartungsplan_position_id == wartungsplan_position_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def erstelle_wartungsauftrag(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
objekt_id: int,
|
||||
geraet_instanz_id,
|
||||
wartungsplan_position_id: int | None,
|
||||
wartungsart: str,
|
||||
beschreibung: str | None,
|
||||
werkstatt_intern: bool,
|
||||
werkstatt_name: str | None,
|
||||
faelligkeit_am: date | None,
|
||||
) -> Wartungsauftrag:
|
||||
auftrag = Wartungsauftrag(
|
||||
objekt_id=objekt_id,
|
||||
geraet_instanz_id=geraet_instanz_id,
|
||||
wartungsplan_position_id=wartungsplan_position_id,
|
||||
wartungsart=wartungsart,
|
||||
beschreibung=beschreibung,
|
||||
status=WartungsauftragStatus.offen,
|
||||
werkstatt_intern=werkstatt_intern,
|
||||
werkstatt_name=werkstatt_name,
|
||||
faelligkeit_am=faelligkeit_am,
|
||||
erstellt_am=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(auftrag)
|
||||
await db.flush()
|
||||
return auftrag
|
||||
|
||||
|
||||
async def aendere_status(db: AsyncSession, *, auftrag: Wartungsauftrag, status: WartungsauftragStatus) -> Wartungsauftrag:
|
||||
auftrag.status = status
|
||||
await db.flush()
|
||||
return auftrag
|
||||
|
||||
|
||||
async def erledige_wartungsauftrag(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
auftrag: Wartungsauftrag,
|
||||
benutzer_id: int,
|
||||
erledigt_am: date | None,
|
||||
kosten: Decimal | None,
|
||||
kilometerstand_bei_erledigung: int | None,
|
||||
betriebsstunden_bei_erledigung: Decimal | None,
|
||||
) -> Wartungsauftrag:
|
||||
auftrag.status = WartungsauftragStatus.erledigt
|
||||
auftrag.erledigt_am = (
|
||||
datetime.combine(erledigt_am, datetime.min.time(), tzinfo=timezone.utc)
|
||||
if erledigt_am
|
||||
else datetime.now(timezone.utc)
|
||||
)
|
||||
auftrag.durchgefuehrt_von = benutzer_id
|
||||
auftrag.kosten = kosten
|
||||
|
||||
if kilometerstand_bei_erledigung is None or betriebsstunden_bei_erledigung is None:
|
||||
fahrzeugdetails = await db.get(Fahrzeugdetails, auftrag.objekt_id)
|
||||
if fahrzeugdetails is not None:
|
||||
if kilometerstand_bei_erledigung is None:
|
||||
kilometerstand_bei_erledigung = fahrzeugdetails.kilometerstand
|
||||
if betriebsstunden_bei_erledigung is None:
|
||||
betriebsstunden_bei_erledigung = fahrzeugdetails.betriebsstunden
|
||||
auftrag.kilometerstand_bei_erledigung = kilometerstand_bei_erledigung
|
||||
auftrag.betriebsstunden_bei_erledigung = betriebsstunden_bei_erledigung
|
||||
|
||||
await db.flush()
|
||||
return auftrag
|
||||
|
||||
|
||||
async def liste_wartungsauftraege(
|
||||
db: AsyncSession, *, objekt_id: int | None = None, status: WartungsauftragStatus | None = None
|
||||
) -> list[Wartungsauftrag]:
|
||||
stmt = select(Wartungsauftrag)
|
||||
if objekt_id is not None:
|
||||
stmt = stmt.where(Wartungsauftrag.objekt_id == objekt_id)
|
||||
if status is not None:
|
||||
stmt = stmt.where(Wartungsauftrag.status == status)
|
||||
stmt = stmt.order_by(Wartungsauftrag.erstellt_am.desc())
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def faellige_wartungen_fuer_objekt(db: AsyncSession, objekt: Objekt) -> list[dict]:
|
||||
"""MAINT-002: kombiniert Zeit-/km-/Betriebsstunden-Intervalle je Position -
|
||||
fällig, sobald das erste Kriterium erreicht ist. Nie durchgeführt gilt
|
||||
immer als fällig (wie "nie kontrolliert" bei der Einsatzbereitschaft)."""
|
||||
plaene = (
|
||||
await db.execute(select(Wartungsplan).where(Wartungsplan.objekttyp_id == objekt.objekttyp_id))
|
||||
).scalars().all()
|
||||
if not plaene:
|
||||
return []
|
||||
|
||||
fahrzeugdetails = await db.get(Fahrzeugdetails, objekt.id)
|
||||
heute = datetime.now(timezone.utc).date()
|
||||
ergebnisse: list[dict] = []
|
||||
|
||||
for plan in plaene:
|
||||
positionen = await hole_positionen_mit_intervallen(db, plan.id)
|
||||
for position in positionen:
|
||||
intervalle = await hole_intervalle(db, position.id)
|
||||
letzter = (
|
||||
await db.execute(
|
||||
select(Wartungsauftrag)
|
||||
.where(
|
||||
Wartungsauftrag.objekt_id == objekt.id,
|
||||
Wartungsauftrag.wartungsplan_position_id == position.id,
|
||||
Wartungsauftrag.status == WartungsauftragStatus.erledigt,
|
||||
)
|
||||
.order_by(Wartungsauftrag.erledigt_am.desc())
|
||||
.limit(1)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if letzter is None:
|
||||
ergebnisse.append(
|
||||
{
|
||||
"wartungsplan_position_id": position.id,
|
||||
"wartungsart": position.wartungsart,
|
||||
"objekt_id": objekt.id,
|
||||
"faellig": True,
|
||||
"faellig_am": None,
|
||||
"faellig_bei_km": None,
|
||||
"faellig_bei_betriebsstunden": None,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
faellig = False
|
||||
faellig_am = None
|
||||
faellig_bei_km = None
|
||||
faellig_bei_bs = None
|
||||
|
||||
for intervall in intervalle:
|
||||
if intervall.typ == IntervallTyp.zeit:
|
||||
faellig_am = plus_monate(letzter.erledigt_am.date(), intervall.wert)
|
||||
if heute >= faellig_am:
|
||||
faellig = True
|
||||
elif intervall.typ == IntervallTyp.km and fahrzeugdetails is not None:
|
||||
basis_km = letzter.kilometerstand_bei_erledigung or 0
|
||||
faellig_bei_km = basis_km + intervall.wert
|
||||
if (fahrzeugdetails.kilometerstand or 0) >= faellig_bei_km:
|
||||
faellig = True
|
||||
elif intervall.typ == IntervallTyp.betriebsstunden and fahrzeugdetails is not None:
|
||||
basis_bs = letzter.betriebsstunden_bei_erledigung or Decimal(0)
|
||||
faellig_bei_bs = basis_bs + intervall.wert
|
||||
if (fahrzeugdetails.betriebsstunden or Decimal(0)) >= faellig_bei_bs:
|
||||
faellig = True
|
||||
|
||||
ergebnisse.append(
|
||||
{
|
||||
"wartungsplan_position_id": position.id,
|
||||
"wartungsart": position.wartungsart,
|
||||
"objekt_id": objekt.id,
|
||||
"faellig": faellig,
|
||||
"faellig_am": faellig_am,
|
||||
"faellig_bei_km": faellig_bei_km,
|
||||
"faellig_bei_betriebsstunden": faellig_bei_bs,
|
||||
}
|
||||
)
|
||||
|
||||
return ergebnisse
|
||||
@@ -50,6 +50,10 @@ async def test_akte_buendelt_teilbereiche(client, db_session, objekt_mit_positio
|
||||
assert body["historie"][0]["entitaet_typ"] == "objekt"
|
||||
assert body["einsatzbereitschaft_status"] == "not-ready"
|
||||
assert "mangel_kritisch_offen" in body["einsatzbereitschaft_gruende"]
|
||||
assert len(body["beladung"]) == 1
|
||||
assert body["beladung"][0]["material_id"] == _material.id
|
||||
assert body["beladung"][0]["sollmenge"] == "10"
|
||||
assert body["beladung"][0]["istmenge"] == "0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models.objekt import Objekt
|
||||
from app.services.datumshilfen import plus_monate
|
||||
from tests.conftest import auth_header, login
|
||||
|
||||
|
||||
async def _objekt_mit_typ(db_session, hauptserver_id, *, code, name, objekttyp, standort):
|
||||
objekt = Objekt(
|
||||
code=code,
|
||||
name=name,
|
||||
objekttyp_id=objekttyp.id,
|
||||
standort_id=standort.id,
|
||||
zustaendiger_server_id=hauptserver_id,
|
||||
)
|
||||
db_session.add(objekt)
|
||||
await db_session.flush()
|
||||
return objekt
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wartungsplan_mit_intervall_anlegen(client, admin_user, objekttyp_factory):
|
||||
token = await login(client, "admin1")
|
||||
objekttyp = await objekttyp_factory("RTW-Test")
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/wartungsplaene",
|
||||
json={
|
||||
"objekttyp_id": objekttyp.id,
|
||||
"name": "RTW Standard",
|
||||
"positionen": [
|
||||
{
|
||||
"wartungsart": "Ölwechsel",
|
||||
"intervalle": [{"typ": "zeit", "wert": 12}, {"typ": "km", "wert": 15000}],
|
||||
}
|
||||
],
|
||||
},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 201
|
||||
body = response.json()
|
||||
assert len(body["positionen"]) == 1
|
||||
assert len(body["positionen"][0]["intervalle"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nie_durchgefuehrte_wartung_ist_faellig(
|
||||
client, db_session, hauptserver_id, admin_user, objekttyp_factory, standort_factory
|
||||
):
|
||||
token = await login(client, "admin1")
|
||||
objekttyp = await objekttyp_factory("RTW-Test2")
|
||||
standort = await standort_factory("Standort-W2")
|
||||
objekt = await _objekt_mit_typ(
|
||||
db_session, hauptserver_id, code="RTW-W2", name="RTW W2", objekttyp=objekttyp, standort=standort
|
||||
)
|
||||
objekt_id = objekt.id
|
||||
|
||||
await client.post(
|
||||
"/api/v1/wartungsplaene",
|
||||
json={"objekttyp_id": objekttyp.id, "name": "Plan", "positionen": [{"wartungsart": "Ölwechsel"}]},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
|
||||
response = await client.get(f"/api/v1/objekte/{objekt_id}/faellige-wartungen", headers=auth_header(token))
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert len(body) == 1
|
||||
assert body[0]["faellig"] is True
|
||||
assert body[0]["wartungsart"] == "Ölwechsel"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wartungsauftrag_erledigen_macht_zeit_intervall_nicht_faellig(
|
||||
client, db_session, hauptserver_id, admin_user, objekttyp_factory, standort_factory
|
||||
):
|
||||
token = await login(client, "admin1")
|
||||
objekttyp = await objekttyp_factory("RTW-Test3")
|
||||
standort = await standort_factory("Standort-W3")
|
||||
objekt = await _objekt_mit_typ(
|
||||
db_session, hauptserver_id, code="RTW-W3", name="RTW W3", objekttyp=objekttyp, standort=standort
|
||||
)
|
||||
objekt_id = objekt.id
|
||||
|
||||
plan = await client.post(
|
||||
"/api/v1/wartungsplaene",
|
||||
json={
|
||||
"objekttyp_id": objekttyp.id,
|
||||
"name": "Plan",
|
||||
"positionen": [{"wartungsart": "Ölwechsel", "intervalle": [{"typ": "zeit", "wert": 12}]}],
|
||||
},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
position_id = plan.json()["positionen"][0]["id"]
|
||||
|
||||
auftrag = await client.post(
|
||||
"/api/v1/wartungsauftraege",
|
||||
json={"objekt_id": objekt_id, "wartungsplan_position_id": position_id, "wartungsart": "Ölwechsel"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert auftrag.status_code == 201
|
||||
auftrag_id = auftrag.json()["id"]
|
||||
|
||||
erledigt = await client.post(
|
||||
f"/api/v1/wartungsauftraege/{auftrag_id}/erledigen",
|
||||
json={"erledigt_am": date.today().isoformat()},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert erledigt.status_code == 200
|
||||
assert erledigt.json()["status"] == "erledigt"
|
||||
|
||||
faellig = await client.get(f"/api/v1/objekte/{objekt_id}/faellige-wartungen", headers=auth_header(token))
|
||||
assert faellig.json()[0]["faellig"] is False
|
||||
assert faellig.json()[0]["faellig_am"] == plus_monate(date.today(), 12).isoformat()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_km_intervall_wird_zuerst_faellig_trotz_gueltigem_zeit_intervall(
|
||||
client, db_session, hauptserver_id, admin_user, objekttyp_factory, standort_factory
|
||||
):
|
||||
""""je nachdem was zuerst eintritt" - km-Schwelle erreicht, obwohl das
|
||||
Zeit-Intervall noch nicht abgelaufen ist."""
|
||||
from app.models.fahrzeugdetails import Fahrzeugdetails
|
||||
|
||||
token = await login(client, "admin1")
|
||||
objekttyp = await objekttyp_factory("RTW-Test4")
|
||||
standort = await standort_factory("Standort-W4")
|
||||
objekt = await _objekt_mit_typ(
|
||||
db_session, hauptserver_id, code="RTW-W4", name="RTW W4", objekttyp=objekttyp, standort=standort
|
||||
)
|
||||
objekt_id = objekt.id
|
||||
|
||||
db_session.add(Fahrzeugdetails(objekt_id=objekt_id, kilometerstand=20000))
|
||||
await db_session.flush()
|
||||
|
||||
plan = await client.post(
|
||||
"/api/v1/wartungsplaene",
|
||||
json={
|
||||
"objekttyp_id": objekttyp.id,
|
||||
"name": "Plan",
|
||||
"positionen": [
|
||||
{
|
||||
"wartungsart": "Ölwechsel",
|
||||
"intervalle": [{"typ": "zeit", "wert": 12}, {"typ": "km", "wert": 15000}],
|
||||
}
|
||||
],
|
||||
},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
position_id = plan.json()["positionen"][0]["id"]
|
||||
|
||||
auftrag = await client.post(
|
||||
"/api/v1/wartungsauftraege",
|
||||
json={"objekt_id": objekt_id, "wartungsplan_position_id": position_id, "wartungsart": "Ölwechsel"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
auftrag_id = auftrag.json()["id"]
|
||||
await client.post(
|
||||
f"/api/v1/wartungsauftraege/{auftrag_id}/erledigen",
|
||||
json={"erledigt_am": date.today().isoformat(), "kilometerstand_bei_erledigung": 5000},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
|
||||
faellig = await client.get(f"/api/v1/objekte/{objekt_id}/faellige-wartungen", headers=auth_header(token))
|
||||
body = faellig.json()[0]
|
||||
assert body["faellig"] is True
|
||||
assert body["faellig_bei_km"] == 20000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ad_hoc_reparatur_ohne_wartungsplan_mit_externer_werkstatt(
|
||||
client, admin_user, standort_factory, objekt_factory
|
||||
):
|
||||
token = await login(client, "admin1")
|
||||
standort = await standort_factory("Standort-W5")
|
||||
await objekt_factory(name="RTW W5", code="RTW-W5", standort=standort)
|
||||
objekte = await client.get("/api/v1/objekte", headers=auth_header(token))
|
||||
objekt_id = [o for o in objekte.json() if o["code"] == "RTW-W5"][0]["id"]
|
||||
|
||||
auftrag = await client.post(
|
||||
"/api/v1/wartungsauftraege",
|
||||
json={
|
||||
"objekt_id": objekt_id,
|
||||
"wartungsart": "Bremsleuchte defekt",
|
||||
"werkstatt_intern": False,
|
||||
"werkstatt_name": "KFZ Meyer GmbH",
|
||||
},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert auftrag.status_code == 201
|
||||
body = auftrag.json()
|
||||
assert body["wartungsplan_position_id"] is None
|
||||
assert body["werkstatt_intern"] is False
|
||||
assert body["werkstatt_name"] == "KFZ Meyer GmbH"
|
||||
assert body["status"] == "offen"
|
||||
|
||||
in_arbeit = await client.patch(
|
||||
f"/api/v1/wartungsauftraege/{body['id']}/status",
|
||||
json={"status": "in_arbeit"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert in_arbeit.json()["status"] == "in_arbeit"
|
||||
|
||||
erledigt = await client.post(
|
||||
f"/api/v1/wartungsauftraege/{body['id']}/erledigen",
|
||||
json={"kosten": "89.90"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert erledigt.json()["status"] == "erledigt"
|
||||
assert erledigt.json()["kosten"] == "89.90"
|
||||
|
||||
historie = await client.get(f"/api/v1/wartungsauftraege?objekt_id={objekt_id}", headers=auth_header(token))
|
||||
assert len(historie.json()) == 1
|
||||
Reference in New Issue
Block a user