feat(wartung): MAINT-005 Ersatzteile/Kosten je Wartungsauftrag
CI / backend-tests (push) Successful in 2m22s
CI / frontend-build (push) Successful in 25s

Neue Tabelle wartungsauftrag_teil (Migration 0033), CRUD-Endpunkte
/wartungsauftraege/{id}/teile, UI-Panel in WartungSection.tsx mit
Summenanzeige. Backlog Epic 22 (UI-Redesign Runde 2, Kacheln UI2-001..005)
als geplant ergänzt, Umsetzung folgt separat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-08 22:06:02 +02:00
co-authored by Claude Sonnet 5
parent 3b5cf8feab
commit 847c303a97
14 changed files with 5232 additions and 6 deletions
+48 -1
View File
@@ -8,26 +8,31 @@ 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.models.wartung import Wartungsauftrag, WartungsauftragTeil, Wartungsplan, WartungsauftragStatus
from app.schemas.wartung import (
FaelligeWartungRead,
WartungsauftragCreate,
WartungsauftragErledigen,
WartungsauftragRead,
WartungsauftragStatusUpdate,
WartungsauftragTeilCreate,
WartungsauftragTeilRead,
WartungsplanCreate,
WartungsplanPositionRead,
WartungsplanRead,
)
from app.services.wartung import (
aendere_status,
entferne_wartungsauftrag_teil,
erledige_wartungsauftrag,
erstelle_wartungsauftrag,
erstelle_wartungsauftrag_teil,
erstelle_wartungsplan,
faellige_wartungen_fuer_objekt,
hole_intervalle,
hole_positionen_mit_intervallen,
liste_wartungsauftraege,
liste_wartungsauftrag_teile,
)
router = APIRouter()
@@ -151,3 +156,45 @@ async def route_erledigen(
kilometerstand_bei_erledigung=payload.kilometerstand_bei_erledigung,
betriebsstunden_bei_erledigung=payload.betriebsstunden_bei_erledigung,
)
@router.get("/wartungsauftraege/{auftrag_id}/teile", response_model=list[WartungsauftragTeilRead])
async def liste_teile(
auftrag_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
) -> list[WartungsauftragTeil]:
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 liste_wartungsauftrag_teile(db, auftrag_id=auftrag_id)
@router.post(
"/wartungsauftraege/{auftrag_id}/teile",
response_model=WartungsauftragTeilRead,
status_code=status.HTTP_201_CREATED,
)
async def erstelle_teil(
auftrag_id: uuid.UUID,
payload: WartungsauftragTeilCreate,
db: AsyncSession = Depends(get_db),
_=Depends(_verantwortliche),
) -> WartungsauftragTeil:
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 erstelle_wartungsauftrag_teil(
db, auftrag_id=auftrag_id, bezeichnung=payload.bezeichnung, menge=payload.menge, kosten=payload.kosten
)
@router.delete("/wartungsauftraege/{auftrag_id}/teile/{teil_id}", status_code=status.HTTP_204_NO_CONTENT)
async def loesche_teil(
auftrag_id: uuid.UUID,
teil_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
_=Depends(_verantwortliche),
) -> None:
teil = await db.get(WartungsauftragTeil, teil_id)
if teil is None or teil.auftrag_id != auftrag_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ersatzteil nicht gefunden")
await entferne_wartungsauftrag_teil(db, teil=teil)
+10
View File
@@ -78,3 +78,13 @@ class Wartungsauftrag(Base):
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)
class WartungsauftragTeil(Base):
__tablename__ = "wartungsauftrag_teil"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
auftrag_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("wartungsauftrag.id"), nullable=False)
bezeichnung: Mapped[str] = mapped_column(String, nullable=False)
menge: Mapped[Decimal] = mapped_column(Numeric, nullable=False, default=1)
kosten: Mapped[Decimal] = mapped_column(Numeric, nullable=False)
+15
View File
@@ -91,6 +91,21 @@ class WartungsauftragRead(BaseModel):
betriebsstunden_bei_erledigung: Decimal | None
class WartungsauftragTeilCreate(BaseModel):
bezeichnung: str
menge: Decimal = Decimal(1)
kosten: Decimal
class WartungsauftragTeilRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
auftrag_id: uuid.UUID
bezeichnung: str
menge: Decimal
kosten: Decimal
class FaelligeWartungRead(BaseModel):
wartungsplan_position_id: int
wartungsart: str
+22
View File
@@ -10,6 +10,7 @@ from app.models.wartung import (
IntervallTyp,
Wartungsauftrag,
WartungsauftragStatus,
WartungsauftragTeil,
Wartungsintervall,
Wartungsplan,
WartungsplanPosition,
@@ -135,6 +136,27 @@ async def liste_wartungsauftraege(
return list(result.scalars().all())
async def erstelle_wartungsauftrag_teil(
db: AsyncSession, *, auftrag_id, bezeichnung: str, menge: Decimal, kosten: Decimal
) -> WartungsauftragTeil:
teil = WartungsauftragTeil(auftrag_id=auftrag_id, bezeichnung=bezeichnung, menge=menge, kosten=kosten)
db.add(teil)
await db.flush()
return teil
async def liste_wartungsauftrag_teile(db: AsyncSession, *, auftrag_id) -> list[WartungsauftragTeil]:
result = await db.execute(
select(WartungsauftragTeil).where(WartungsauftragTeil.auftrag_id == auftrag_id)
)
return list(result.scalars().all())
async def entferne_wartungsauftrag_teil(db: AsyncSession, *, teil: WartungsauftragTeil) -> None:
await db.delete(teil)
await db.flush()
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