Nutzer-Vorgabe: "Ablauf/Charge/SN gehört mit zu den Kontrollen wie auch die
Menge". PUT .../positionen/{material_id} akzeptiert jetzt optional
ablaufdatum/chargennummer/seriennummer und schreibt sie auf die
Objektposition (Vier-Kernbegriffe bleiben unberührt: nur diese drei Felder,
niemals istmenge - das bleibt exklusiv Nachfüllung vorbehalten). Offline-
Queue und PositionCard erweitert: Felder erscheinen nur passend zum
Materialtyp (ablauf_charge -> Datum+Charge, geraet_sn -> SN). Admin-Panel
(ObjektPositionenPanel) bleibt zusätzlich für nachträgliche Korrekturen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
188 lines
6.9 KiB
Python
188 lines
6.9 KiB
Python
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, KnotenTyp, RolleTyp, Systemknoten
|
||
from app.models.kontrolle import Kontrolle, KontrollStatus, Kontrollposition
|
||
from app.models.objekt import Objekt
|
||
from app.schemas.kontrolle import (
|
||
KontrolleAbbrechenRequest,
|
||
KontrolleCreate,
|
||
KontrolleFortschritt,
|
||
KontrolleRead,
|
||
KontrollpositionInput,
|
||
KontrollpositionRead,
|
||
)
|
||
from app.services.kontrolle import (
|
||
ObjektGesperrtError,
|
||
UnvollstaendigError,
|
||
abbreche_kontrolle,
|
||
abschliesse_kontrolle,
|
||
erfasse_position,
|
||
fehlende_material_ids,
|
||
starte_kontrolle,
|
||
)
|
||
|
||
router = APIRouter()
|
||
|
||
_mitarbeiter_plus = require_roles(
|
||
RolleTyp.mitarbeiter,
|
||
RolleTyp.materialverantwortlicher,
|
||
RolleTyp.leitungsverantwortlicher,
|
||
RolleTyp.administration,
|
||
)
|
||
|
||
|
||
async def _hauptserver_id(db: AsyncSession) -> int:
|
||
result = await db.execute(select(Systemknoten.id).where(Systemknoten.typ == KnotenTyp.haupt))
|
||
return result.scalar_one()
|
||
|
||
|
||
async def _hole_kontrolle_oder_404(db: AsyncSession, kontrolle_id: uuid.UUID) -> Kontrolle:
|
||
kontrolle = await db.get(Kontrolle, kontrolle_id)
|
||
if kontrolle is None:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kontrolle nicht gefunden")
|
||
return kontrolle
|
||
|
||
|
||
def _pruefe_besitz(kontrolle: Kontrolle, current_user: Benutzer) -> None:
|
||
"""Objekt-Sperre (Prompt 02.8) wirkt auch hier: nur wer die Kontrolle gestartet
|
||
(oder übernommen) hat, darf sie fortführen/abschließen/abbrechen."""
|
||
if kontrolle.benutzer_id != current_user.id:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="Diese Kontrolle gehört einem anderen Benutzer – ggf. Übernahme nötig",
|
||
)
|
||
if kontrolle.status != KontrollStatus.in_bearbeitung:
|
||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Kontrolle ist nicht mehr aktiv")
|
||
|
||
|
||
@router.post(
|
||
"/objekte/{objekt_id}/kontrollen", response_model=KontrolleRead, status_code=status.HTTP_201_CREATED
|
||
)
|
||
async def starte_neue_kontrolle(
|
||
objekt_id: int,
|
||
payload: KontrolleCreate,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: Benutzer = Depends(_mitarbeiter_plus),
|
||
) -> Kontrolle:
|
||
objekt = await db.get(Objekt, objekt_id)
|
||
if objekt is None:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
||
|
||
zustaendiger_server_id = await _hauptserver_id(db)
|
||
try:
|
||
return await starte_kontrolle(
|
||
db,
|
||
objekt_id=objekt_id,
|
||
benutzer_id=current_user.id,
|
||
uebernehmen=payload.uebernehmen,
|
||
zustaendiger_server_id=zustaendiger_server_id,
|
||
)
|
||
except ObjektGesperrtError as exc:
|
||
aktive = exc.aktive_kontrolle
|
||
besitzer = await db.get(Benutzer, aktive.benutzer_id)
|
||
raise HTTPException(
|
||
status_code=status.HTTP_409_CONFLICT,
|
||
detail={
|
||
"nachricht": "wird gerade kontrolliert",
|
||
"benutzer": besitzer.name if besitzer else None,
|
||
"seit": aktive.gestartet_am.isoformat(),
|
||
},
|
||
) from exc
|
||
|
||
|
||
@router.get("/kontrollen/{kontrolle_id}", response_model=KontrolleFortschritt)
|
||
async def hole_kontroll_fortschritt(
|
||
kontrolle_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
||
) -> KontrolleFortschritt:
|
||
kontrolle = await _hole_kontrolle_oder_404(db, kontrolle_id)
|
||
result = await db.execute(
|
||
select(Kontrollposition).where(Kontrollposition.kontrolle_id == kontrolle_id)
|
||
)
|
||
positionen = list(result.scalars().all())
|
||
fehlende = await fehlende_material_ids(db, kontrolle)
|
||
return KontrolleFortschritt(
|
||
kontrolle=KontrolleRead.model_validate(kontrolle),
|
||
positionen=[KontrollpositionRead.model_validate(p) for p in positionen],
|
||
anzahl_positionen_gesamt=len(positionen) + len(fehlende),
|
||
anzahl_positionen_erfasst=len(positionen),
|
||
)
|
||
|
||
|
||
@router.put("/kontrollen/{kontrolle_id}/positionen/{material_id}", response_model=KontrollpositionRead)
|
||
async def erfasse_kontrollposition(
|
||
kontrolle_id: uuid.UUID,
|
||
material_id: int,
|
||
payload: KontrollpositionInput,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: Benutzer = Depends(_mitarbeiter_plus),
|
||
) -> KontrollpositionRead:
|
||
kontrolle = await _hole_kontrolle_oder_404(db, kontrolle_id)
|
||
_pruefe_besitz(kontrolle, current_user)
|
||
|
||
objekt = await db.get(Objekt, kontrolle.objekt_id)
|
||
zustaendiger_server_id = await _hauptserver_id(db)
|
||
kontrollposition, fehlbestand_id = await erfasse_position(
|
||
db,
|
||
kontrolle=kontrolle,
|
||
objekt=objekt,
|
||
material_id=material_id,
|
||
istmenge=payload.istmenge,
|
||
zustaendiger_server_id=zustaendiger_server_id,
|
||
ablaufdatum=payload.ablaufdatum,
|
||
chargennummer=payload.chargennummer,
|
||
seriennummer=payload.seriennummer,
|
||
)
|
||
return KontrollpositionRead(
|
||
id=kontrollposition.id,
|
||
kontrolle_id=kontrollposition.kontrolle_id,
|
||
material_id=kontrollposition.material_id,
|
||
sollmenge_snapshot=kontrollposition.sollmenge_snapshot,
|
||
istmenge_erfasst=kontrollposition.istmenge_erfasst,
|
||
abweichung=kontrollposition.abweichung,
|
||
fehlbestand_id=fehlbestand_id,
|
||
)
|
||
|
||
|
||
@router.post("/kontrollen/{kontrolle_id}/abschliessen", response_model=KontrolleRead)
|
||
async def abschliesse(
|
||
kontrolle_id: uuid.UUID,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: Benutzer = Depends(_mitarbeiter_plus),
|
||
) -> Kontrolle:
|
||
kontrolle = await _hole_kontrolle_oder_404(db, kontrolle_id)
|
||
_pruefe_besitz(kontrolle, current_user)
|
||
zustaendiger_server_id = await _hauptserver_id(db)
|
||
try:
|
||
await abschliesse_kontrolle(db, kontrolle=kontrolle, zustaendiger_server_id=zustaendiger_server_id)
|
||
except UnvollstaendigError as exc:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_409_CONFLICT,
|
||
detail={
|
||
"nachricht": "nicht alle Positionen erfasst",
|
||
"fehlende_material_ids": exc.fehlende_material_ids,
|
||
},
|
||
) from exc
|
||
return kontrolle
|
||
|
||
|
||
@router.post("/kontrollen/{kontrolle_id}/abbrechen", response_model=KontrolleRead)
|
||
async def abbreche(
|
||
kontrolle_id: uuid.UUID,
|
||
payload: KontrolleAbbrechenRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: Benutzer = Depends(_mitarbeiter_plus),
|
||
) -> Kontrolle:
|
||
kontrolle = await _hole_kontrolle_oder_404(db, kontrolle_id)
|
||
_pruefe_besitz(kontrolle, current_user)
|
||
zustaendiger_server_id = await _hauptserver_id(db)
|
||
await abbreche_kontrolle(
|
||
db, kontrolle=kontrolle, grund=payload.grund, zustaendiger_server_id=zustaendiger_server_id
|
||
)
|
||
return kontrolle
|