refactor: Code-Review-Fixes (Reuse/Simplification/Efficiency) aus dieser Session
CI / backend-tests (push) Successful in 1m57s
CI / frontend-build (push) Successful in 18s

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
This commit is contained in:
2026-09-05 23:49:28 +02:00
co-authored by Claude Sonnet 5
parent a6f0c51842
commit 0c8f375a51
5 changed files with 51 additions and 62 deletions
+18 -13
View File
@@ -1,3 +1,5 @@
from typing import TypeVar
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -23,6 +25,15 @@ router = APIRouter()
_admin_only = require_roles(RolleTyp.administration)
_materialverantwortliche = require_roles(RolleTyp.administration, RolleTyp.materialverantwortlicher)
_T = TypeVar("_T")
async def _get_or_404(db: AsyncSession, model: type[_T], id_: int, meldung: str) -> _T:
objekt = await db.get(model, id_)
if objekt is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=meldung)
return objekt
# -- Lagerort (WH-001) ---------------------------------------------------------
@@ -36,8 +47,8 @@ async def liste_lagerorte(db: AsyncSession = Depends(get_db), _=Depends(get_curr
async def erstelle_lagerort(
payload: LagerortCreate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only)
) -> Lagerort:
if payload.parent_id is not None and await db.get(Lagerort, payload.parent_id) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Übergeordneter Lagerort nicht gefunden")
if payload.parent_id is not None:
await _get_or_404(db, Lagerort, payload.parent_id, "Übergeordneter Lagerort nicht gefunden")
lagerort = Lagerort(**payload.model_dump())
db.add(lagerort)
await db.flush()
@@ -48,9 +59,7 @@ async def erstelle_lagerort(
async def aendere_lagerort(
lagerort_id: int, payload: LagerortUpdate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only)
) -> Lagerort:
lagerort = await db.get(Lagerort, lagerort_id)
if lagerort is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Lagerort nicht gefunden")
lagerort = await _get_or_404(db, Lagerort, lagerort_id, "Lagerort nicht gefunden")
daten = payload.model_dump(exclude_unset=True)
if daten.get("parent_id") == lagerort_id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Lagerort kann nicht sein eigener Elternknoten sein")
@@ -77,8 +86,7 @@ async def liste_lagerplaetze(
async def erstelle_lagerplatz(
payload: LagerplatzCreate, db: AsyncSession = Depends(get_db), _=Depends(_materialverantwortliche)
) -> Lagerplatz:
if await db.get(Lagerort, payload.lagerort_id) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Lagerort nicht gefunden")
await _get_or_404(db, Lagerort, payload.lagerort_id, "Lagerort nicht gefunden")
vorhanden = await db.execute(select(Lagerplatz).where(Lagerplatz.code == payload.code))
if vorhanden.scalar_one_or_none() is not None:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Code bereits vergeben")
@@ -94,8 +102,7 @@ async def erstelle_lagerplatz(
async def hole_bestand(
lagerplatz_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
) -> list[Bestand]:
if await db.get(Lagerplatz, lagerplatz_id) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Lagerplatz nicht gefunden")
await _get_or_404(db, Lagerplatz, lagerplatz_id, "Lagerplatz nicht gefunden")
result = await db.execute(select(Bestand).where(Bestand.lagerplatz_id == lagerplatz_id))
return list(result.scalars().all())
@@ -111,8 +118,7 @@ async def route_einlagern(
db: AsyncSession = Depends(get_db),
current_user=Depends(get_current_user),
) -> Materialbewegung:
if await db.get(Lagerplatz, lagerplatz_id) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Lagerplatz nicht gefunden")
await _get_or_404(db, Lagerplatz, lagerplatz_id, "Lagerplatz nicht gefunden")
return await einlagern(
db,
lagerplatz_id=lagerplatz_id,
@@ -134,8 +140,7 @@ async def route_auslagern(
db: AsyncSession = Depends(get_db),
current_user=Depends(get_current_user),
) -> Materialbewegung:
if await db.get(Lagerplatz, lagerplatz_id) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Lagerplatz nicht gefunden")
await _get_or_404(db, Lagerplatz, lagerplatz_id, "Lagerplatz nicht gefunden")
try:
return await auslagern(
db,