Files
MABEA/backend/app/api/v1/endpoints/vorlagen.py
T
patrickandClaude Sonnet 5 0c8f375a51
CI / backend-tests (push) Successful in 1m57s
CI / frontend-build (push) Successful in 18s
refactor: Code-Review-Fixes (Reuse/Simplification/Efficiency) aus dieser Session
4 parallele Review-Agenten (Reuse/Simplification/Efficiency/Altitude) gegen den
Diff dieser Session (c7dd47f...HEAD) laufen lassen, echte Funde angewendet:

- lager.py: 5x wiederholtes "db.get(...) or 404 raisen" durch _get_or_404()-
  Helper ersetzt
- akte.py: zwei Queries für Geräte-Instanzen (erst Positions-IDs, dann Geräte)
  zu einer Query mit Subquery zusammengefasst
- akte.py: manuelle Feld-für-Feld-Rekonstruktion von ObjektRead/HistorieRead
  durch model_validate()+model_copy() ersetzt (HistorieRead.benutzer_name
  bekommt dafür einen Default, harmlos für den bestehenden Endpunkt)
- vorlagen.py: doppelte "Positionen löschen + flush"-Logik (Vorlage-Löschen
  und Positionen-Ersetzen) in loesche_alle_positionen() zusammengeführt

Bewusst nicht angewendet:
- Zentrale Session-Rollback-Vereinheitlichung für die drei Pre-Check-Stellen
  (objekte.py Selbstbezug, geraet_instanz.py SN-Duplikat, lager.py Eltern-
  Selbstbezug) - der Altitude-Review schlug das vor, aber get_db() rollt in
  Produktion bei jeder Exception bereits korrekt zurück (jede Anfrage hat eine
  eigene Session); das PendingRollbackError-Problem trat nur in der geteilten
  Test-Session auf und wurde bereits gezielt per Pre-Check vermieden - eine
  zusätzliche zentrale Rollback-Schicht würde nichts mehr reparieren, was
  nicht schon repariert ist.
- Postgres-Cluster-Encoding (template1) auf beiden Hosts fixen, nicht nur in
  CI/für die eine migrierte DB - echter Infra-Eingriff, braucht Rücksprache.
- LagerSection.tsx "inkonsistente" Formular-Resets - beim genaueren Hinsehen
  gewolltes UX (Ort/Typ bleiben ausgewählt für mehrere Anlagen hintereinander).

140 Tests weiterhin grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-05 23:49:28 +02:00

128 lines
5.2 KiB
Python

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 RolleTyp
from app.models.objekt import Objekt
from app.models.vorlage import Beladungsvorlage
from app.schemas.vorlage import BeladungsvorlageCreate, BeladungsvorlageRead, VorlagenAenderung
from app.services.vorlagen import (
aktualisiere_positionen,
erstelle_vorlage,
hole_positionen,
loesche_alle_positionen,
neue_version,
)
router = APIRouter()
_ersteller = require_roles(RolleTyp.administration, RolleTyp.materialverantwortlicher)
async def _mit_positionen(db: AsyncSession, vorlage: Beladungsvorlage) -> BeladungsvorlageRead:
positionen = await hole_positionen(db, vorlage.id)
return BeladungsvorlageRead(
id=vorlage.id,
objekttyp_id=vorlage.objekttyp_id,
name=vorlage.name,
version=vorlage.version,
status=vorlage.status,
positionen=positionen,
)
@router.get("/vorlagen", response_model=list[BeladungsvorlageRead])
async def liste_vorlagen(
objekttyp_id: int | None = None,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
) -> list[BeladungsvorlageRead]:
stmt = select(Beladungsvorlage)
if objekttyp_id is not None:
stmt = stmt.where(Beladungsvorlage.objekttyp_id == objekttyp_id)
result = await db.execute(stmt)
return [await _mit_positionen(db, v) for v in result.scalars().all()]
@router.get("/vorlagen/{vorlage_id}", response_model=BeladungsvorlageRead)
async def hole_vorlage(
vorlage_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
) -> BeladungsvorlageRead:
vorlage = await db.get(Beladungsvorlage, vorlage_id)
if vorlage is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
return await _mit_positionen(db, vorlage)
@router.post("/vorlagen", response_model=BeladungsvorlageRead, status_code=status.HTTP_201_CREATED)
async def erstelle_neue_vorlage(
payload: BeladungsvorlageCreate, db: AsyncSession = Depends(get_db), _=Depends(_ersteller)
) -> BeladungsvorlageRead:
vorlage = await erstelle_vorlage(
db, objekttyp_id=payload.objekttyp_id, name=payload.name, positionen=payload.positionen
)
return await _mit_positionen(db, vorlage)
@router.post(
"/vorlagen/{vorlage_id}/neue-version",
response_model=BeladungsvorlageRead,
status_code=status.HTTP_201_CREATED,
)
async def erzeuge_neue_version(
vorlage_id: int,
payload: VorlagenAenderung,
db: AsyncSession = Depends(get_db),
_=Depends(_ersteller),
) -> BeladungsvorlageRead:
"""Prompt 08 Punkt 4: erzeugt neue Version, alte bleibt unverändert/lesbar."""
aktuelle_vorlage = await db.get(Beladungsvorlage, vorlage_id)
if aktuelle_vorlage is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
neue = await neue_version(db, aktuelle_vorlage=aktuelle_vorlage, positionen=payload.positionen)
return await _mit_positionen(db, neue)
@router.put("/vorlagen/{vorlage_id}/positionen", response_model=BeladungsvorlageRead)
async def aktualisiere_vorlage_positionen(
vorlage_id: int,
payload: VorlagenAenderung,
db: AsyncSession = Depends(get_db),
_=Depends(_ersteller),
) -> BeladungsvorlageRead:
"""Nutzer-Vorgabe 2026-09-05: Vorlage muss keine Versionen haben - ersetzt
die Positionen der bestehenden Vorlage direkt, ohne neue Version anzulegen.
Bereits angelegte Objekte sind unbetroffen (sollmenge_vorlage ist an der
Objektposition fest kopiert, siehe app/services/objekte.py)."""
vorlage = await db.get(Beladungsvorlage, vorlage_id)
if vorlage is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
aktualisiert = await aktualisiere_positionen(db, vorlage=vorlage, positionen=payload.positionen)
return await _mit_positionen(db, aktualisiert)
@router.delete("/vorlagen/{vorlage_id}", status_code=status.HTTP_204_NO_CONTENT)
async def loesche_vorlage(
vorlage_id: int, db: AsyncSession = Depends(get_db), _=Depends(_ersteller)
) -> None:
"""Vorlagen sind bewusst versioniert (Prompt 08) - normalerweise wird eine
Version nie gelöscht, nur durch eine neue ersetzt (status=veraltet). Echtes
Löschen ist nur erlaubt, wenn KEIN Objekt diese Version referenziert (sonst
FK-Verletzung bzw. Datenverlust an echten Objekten) - z.B. versehentlich
angelegte/Test-Versionen entfernen."""
vorlage = await db.get(Beladungsvorlage, vorlage_id)
if vorlage is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vorlage nicht gefunden")
result = await db.execute(select(Objekt.id).where(Objekt.vorlage_id == vorlage_id).limit(1))
if result.scalar_one_or_none() is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Vorlage wird von mindestens einem Objekt verwendet und kann nicht gelöscht werden",
)
await loesche_alle_positionen(db, vorlage_id)
await db.delete(vorlage)