feat(warehouse): WH-005 Umlagerung zwischen Lagerplätzen
CI / backend-tests (push) Successful in 1m58s
CI / frontend-build (push) Successful in 18s

Atomare Aus+Ein-Buchung in einer Materialbewegung-Zeile statt zwei
Einzelschritten. NOTIF-001 war bereits produktiv umgesetzt, Backlog
war nur veraltet - korrigiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-06 09:36:19 +02:00
co-authored by Claude Sonnet 5
parent 0c8f375a51
commit b33d1c83a9
6 changed files with 163 additions and 17 deletions
+40 -1
View File
@@ -17,8 +17,15 @@ from app.schemas.lager import (
LagerplatzCreate,
LagerplatzRead,
MaterialbewegungRead,
UmlagernRequest,
)
from app.services.lager import (
GleicherLagerplatzError,
NichtGenugBestandError,
auslagern,
einlagern,
umlagern,
)
from app.services.lager import NichtGenugBestandError, auslagern, einlagern
router = APIRouter()
@@ -154,6 +161,38 @@ async def route_auslagern(
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Nicht genug Bestand am Lagerplatz") from exc
@router.post(
"/lagerplaetze/{lagerplatz_id}/umlagern",
response_model=MaterialbewegungRead,
status_code=status.HTTP_201_CREATED,
)
async def route_umlagern(
lagerplatz_id: int,
payload: UmlagernRequest,
db: AsyncSession = Depends(get_db),
current_user=Depends(get_current_user),
) -> Materialbewegung:
"""WH-005: Verschieben in einem Schritt statt Aus+Ein manuell."""
await _get_or_404(db, Lagerplatz, lagerplatz_id, "Quell-Lagerplatz nicht gefunden")
await _get_or_404(db, Lagerplatz, payload.nach_lagerplatz_id, "Ziel-Lagerplatz nicht gefunden")
try:
return await umlagern(
db,
von_lagerplatz_id=lagerplatz_id,
nach_lagerplatz_id=payload.nach_lagerplatz_id,
material_id=payload.material_id,
menge=payload.menge,
benutzer_id=current_user.id,
grund=payload.grund,
)
except GleicherLagerplatzError as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Quell- und Ziel-Lagerplatz sind identisch"
) from exc
except NichtGenugBestandError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Nicht genug Bestand am Lagerplatz") from exc
# -- Materialbewegung (WH-007) --------------------------------------------------
@router.get("/materialbewegungen", response_model=list[MaterialbewegungRead])
+7
View File
@@ -55,6 +55,13 @@ class EinAuslagernRequest(BaseModel):
grund: str | None = None
class UmlagernRequest(BaseModel):
nach_lagerplatz_id: int
material_id: int
menge: Decimal
grund: str | None = None
class MaterialbewegungRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
+45
View File
@@ -42,6 +42,51 @@ async def einlagern(
return bewegung
class GleicherLagerplatzError(Exception):
pass
async def umlagern(
db: AsyncSession,
*,
von_lagerplatz_id: int,
nach_lagerplatz_id: int,
material_id: int,
menge: Decimal,
benutzer_id: int,
grund: str | None,
) -> Materialbewegung:
"""WH-005: Aus- und Einlagerung atomar in einer Transaktion (get_db committet
erst nach Request-Ende), eine einzige Materialbewegung-Zeile mit Von+Nach
statt zwei Einzelbuchungen wie bei WH-004."""
if von_lagerplatz_id == nach_lagerplatz_id:
raise GleicherLagerplatzError
quelle = await _bestand_zeile(db, von_lagerplatz_id, material_id)
if quelle is None or quelle.menge < menge:
raise NichtGenugBestandError
quelle.menge -= menge
ziel = await _bestand_zeile(db, nach_lagerplatz_id, material_id)
if ziel is None:
ziel = Bestand(lagerplatz_id=nach_lagerplatz_id, material_id=material_id, menge=Decimal(0))
db.add(ziel)
ziel.menge += menge
bewegung = Materialbewegung(
material_id=material_id,
menge=menge,
von_lagerplatz_id=von_lagerplatz_id,
nach_lagerplatz_id=nach_lagerplatz_id,
benutzer_id=benutzer_id,
zeitpunkt=datetime.now(timezone.utc),
grund=grund,
)
db.add(bewegung)
await db.flush()
return bewegung
async def auslagern(
db: AsyncSession, *, lagerplatz_id: int, material_id: int, menge: Decimal, benutzer_id: int, grund: str | None
) -> Materialbewegung: