Bisher wurde die Sollmenge einer Objektposition live aus der Vorlagenposition
gelesen (objekt.vorlage_id -> vorlagenposition.sollmenge), Bearbeiten der
Vorlage erzeugte deshalb zwingend eine neue Version - sonst hätte sich jede
Änderung rückwirkend auf alle Objekte ausgewirkt, die diese Vorlage nutzen.
- Neue Spalte objektposition.sollmenge_vorlage (Migration 0020): kopiert die
Sollmenge fest bei Anlage/Duplizieren statt live nachzuschlagen
- sollmenge_effektiv(position) liest jetzt nur noch override/sollmenge_vorlage,
kein DB-Lookup mehr nötig (sollmenge_map() entfernt)
- Neuer Endpoint PUT /vorlagen/{id}/positionen: ersetzt Positionen direkt,
ohne neue Version. Bestehende Objekte unbetroffen, nur neu angelegte nutzen
die aktualisierten Werte (Test: test_aktualisiere_positionen_wirkt_nicht_
rueckwirkend_auf_bestehendes_objekt)
- POST /vorlagen/{id}/neue-version bleibt zusätzlich bestehen (nicht entfernt)
- Frontend: "Neue Version speichern" -> "Positionen speichern" (PUT statt POST)
- 140 Tests grün
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
129 lines
5.6 KiB
Python
129 lines
5.6 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, Vorlagenposition
|
|
from app.schemas.vorlage import BeladungsvorlageCreate, BeladungsvorlageRead, VorlagenAenderung
|
|
from app.services.vorlagen import aktualisiere_positionen, erstelle_vorlage, hole_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",
|
|
)
|
|
|
|
positionen = await db.execute(select(Vorlagenposition).where(Vorlagenposition.vorlage_id == vorlage_id))
|
|
for position in positionen.scalars().all():
|
|
await db.delete(position)
|
|
# Flush zwischen den Deletes nötig: Beladungsvorlage/Vorlagenposition haben
|
|
# keine ORM-relationship() (nur rohe FK-Spalten), SQLAlchemy kennt die
|
|
# Abhängigkeit also nicht automatisch und könnte beide DELETEs in falscher
|
|
# Reihenfolge absetzen (FK-Verletzung, beim echten Löschversuch gefunden).
|
|
await db.flush()
|
|
await db.delete(vorlage)
|