Files
Vorrania/backend/app/routers/locations.py
Scarriffle d518b4aa23 Lagerorte per 10-Zeichen-Code statt fortlaufender ID; iOS-Einlagern ohne Kamerazwang
Lagerort-IDs sind jetzt ein zufaelliger 10-Zeichen-Code (wie die Einzelstueck-UIDs) statt einer fortlaufenden Zahl - so kollidiert die Stammdaten-Sicherung zwischen zwei Instanzen praktisch nie mehr, und der Code ist zugleich der Inhalt des QR /l/<code>. Alle Fremdschluessel (lots, movements, items, Mindestbestaende, parent_id) ziehen mit; die Umstellung laeuft einmalig und transaktional beim Serverstart (_migrate_locations_to_code) und rollt bei Fehlern komplett zurueck. Vor dem Deploy ein DB-Backup machen.

iOS-Einlagern oeffnet nicht mehr sofort die Kamera, sondern ein Formular mit Artikelsuche; die Kamera kommt erst per Button. Im Formular laesst sich der Lagerort zusaetzlich per /l/-QR scannen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:15:25 +02:00

105 lines
3.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, require_admin
from ..models import Location, User
from ..schemas import LocationCreate, LocationOut, LocationUpdate
router = APIRouter(prefix="/locations", tags=["locations"])
def _descendant_ids(db: Session, location_id: str) -> set[str]:
"""Alle Unterorte (rekursiv) als Ziel beim Umhängen ausgeschlossen, sonst
entstünde ein Ring."""
result: set[str] = set()
stack = [location_id]
while stack:
cur = stack.pop()
for kid in db.query(Location).filter(Location.parent_id == cur).all():
if kid.id not in result:
result.add(kid.id)
stack.append(kid.id)
return result
@router.get("", response_model=list[LocationOut])
def list_locations(
db: Session = Depends(get_db), _: User = Depends(get_current_user)
) -> list[Location]:
return db.query(Location).order_by(Location.name).all()
@router.post("", response_model=LocationOut, status_code=status.HTTP_201_CREATED)
def create_location(
payload: LocationCreate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> Location:
loc = Location(name=payload.name, parent_id=payload.parent_id)
db.add(loc)
db.commit()
db.refresh(loc)
return loc
@router.patch("/{location_id}", response_model=LocationOut)
def update_location(
location_id: str,
payload: LocationUpdate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> Location:
"""Umbenennen und/oder umhängen. Chargen haengen an der ID, behalten ihren
Lagerort also."""
loc = db.get(Location, location_id)
if loc is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Lagerort nicht gefunden")
data = payload.model_dump(exclude_unset=True)
if "name" in data and data["name"]:
name = data["name"].strip()
doppelt = (
db.query(Location)
.filter(func.lower(Location.name) == name.lower(), Location.id != location_id)
.first()
)
if doppelt is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "Diesen Lagerort gibt es bereits")
loc.name = name
# parent_id nur anfassen, wenn ausdrücklich mitgeschickt (None = oberste Ebene).
if "parent_id" in data:
neu = data["parent_id"]
if neu is not None:
if db.get(Location, neu) is None:
raise HTTPException(
status.HTTP_404_NOT_FOUND, "Übergeordneter Lagerort nicht gefunden"
)
if neu == location_id or neu in _descendant_ids(db, location_id):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
"Ein Lagerort kann nicht sich selbst oder einem seiner Unterorte "
"untergeordnet werden.",
)
loc.parent_id = neu
db.commit()
db.refresh(loc)
return loc
@router.delete("/{location_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_location(
location_id: str,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> None:
loc = db.get(Location, location_id)
if loc is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Lagerort nicht gefunden")
db.delete(loc)
db.commit()