Lagerorte und Einheiten umbenennen

Beide kannte die API bisher nur als Anlegen und Loeschen. Wer sich vertippt
hatte, musste den Eintrag wegwerfen und neu anlegen - und verlor dabei genau
das, was daran haengt: Chargen zeigen auf die Lagerort-ID, Produkte und Gruppen
auf die Einheiten-ID. Ein Tippfehler kostete also Zuordnungen.

Neu ist je ein PATCH nach dem Vorbild der Gebinde, samt Pruefung auf doppelte
Namen ohne Ruecksicht auf Gross- und Kleinschreibung. Anders als beim Loeschen
duerfen auch eingebaute Einheiten umbenannt werden - auch das wie bei den
Gebinden. Art und Faktor einer Einheit bleiben dagegen fest: Sie stecken in
bereits umgerechneten Bestaenden, eine Aenderung wuerde die still verfaelschen.

Die Tests halten fest, worum es eigentlich geht - dass die Verweise das
Umbenennen ueberleben.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 18:23:51 +02:00
parent 4730d791e8
commit 794a81f58b
4 changed files with 175 additions and 2 deletions

View File

@@ -1,10 +1,11 @@
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
from ..schemas import LocationCreate, LocationOut, LocationUpdate
router = APIRouter(prefix="/locations", tags=["locations"])
@@ -29,6 +30,33 @@ def create_location(
return loc
@router.patch("/{location_id}", response_model=LocationOut)
def update_location(
location_id: int,
payload: LocationUpdate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> Location:
"""Umbenennen. 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")
name = payload.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
db.commit()
db.refresh(loc)
return loc
@router.delete("/{location_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_location(
location_id: int,

View File

@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, require_admin
from ..models import Group, Product, Unit, User
from ..schemas import UnitCreate, UnitOut
from ..schemas import UnitCreate, UnitOut, UnitUpdate
router = APIRouter(prefix="/units", tags=["units"])
@@ -33,6 +33,34 @@ def create_unit(
return unit
@router.patch("/{unit_id}", response_model=UnitOut)
def update_unit(
unit_id: int,
payload: UnitUpdate,
db: Session = Depends(get_db),
_: User = Depends(require_admin),
) -> Unit:
"""Umbenennen auch bei eingebauten Einheiten, wie bei den Gebinden.
Produkte und Gruppen verweisen ueber die ID und bleiben unberuehrt."""
unit = db.get(Unit, unit_id)
if unit is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Einheit nicht gefunden")
name = payload.name.strip()
doppelt = (
db.query(Unit)
.filter(func.lower(Unit.name) == name.lower(), Unit.id != unit_id)
.first()
)
if doppelt is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "Einheit existiert bereits")
unit.name = name
db.commit()
db.refresh(unit)
return unit
@router.delete("/{unit_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_unit(
unit_id: int,

View File

@@ -23,6 +23,13 @@ class UnitCreate(BaseModel):
factor: float = Field(gt=0)
class UnitUpdate(BaseModel):
"""Nur der Name. Art und Faktor bleiben fest sie stecken bereits in
umgerechneten Bestaenden, eine Aenderung wuerde die still verfaelschen."""
name: str = Field(min_length=1, max_length=64)
# ---- API-Tokens (externe Zugriffe, z.B. Home Assistant) ----
class ApiTokenOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
@@ -156,6 +163,10 @@ class LocationCreate(BaseModel):
parent_id: int | None = None
class LocationUpdate(BaseModel):
name: str = Field(min_length=1, max_length=120)
# ---- Gebinde (Packung, Glas, …) ----
class PackageTypeCreate(BaseModel):
singular: str = Field(min_length=1, max_length=32)

View File

@@ -0,0 +1,106 @@
"""Umbenennen von Lagerorten und Einheiten.
Bis dahin gab es nur Anlegen und Loeschen. Wer sich vertippt hatte, musste den
Eintrag wegwerfen und neu anlegen - und verlor dabei die Zuordnung der Chargen
bzw. Produkte. Diese Tests halten fest, dass Umbenennen die Verweise behaelt.
"""
import pytest
from fastapi import HTTPException
from app.models import BaseUnit, Lot, Product, Role, Unit, User
from app.routers import locations, units
from app.schemas import LocationCreate, LocationUpdate, UnitCreate, UnitUpdate
@pytest.fixture()
def admin(db):
person = User(username="chef", password_hash="x", role=Role.admin)
db.add(person)
db.commit()
db.refresh(person)
return person
# ---- Lagerorte ----
def test_lagerort_umbenennen_behaelt_die_chargen(db, admin, rice):
keller = locations.create_location(LocationCreate(name="Keler"), db=db, _=admin)
charge = Lot(product_id=rice.id, quantity=500, location_id=keller.id)
db.add(charge)
db.commit()
locations.update_location(keller.id, LocationUpdate(name="Keller"), db=db, _=admin)
db.refresh(charge)
assert charge.location_id == keller.id
assert db.get(type(keller), keller.id).name == "Keller"
def test_lagerort_doppelter_name_wird_abgelehnt(db, admin):
locations.create_location(LocationCreate(name="Keller"), db=db, _=admin)
speis = locations.create_location(LocationCreate(name="Speis"), db=db, _=admin)
with pytest.raises(HTTPException) as fehler:
# Gross-/Kleinschreibung darf keinen Unterschied machen.
locations.update_location(speis.id, LocationUpdate(name="keller"), db=db, _=admin)
assert fehler.value.status_code == 409
def test_lagerort_gleicher_name_bleibt_erlaubt(db, admin):
"""Nur die Schreibweise aendern darf nicht am eigenen Eintrag scheitern."""
keller = locations.create_location(LocationCreate(name="keller"), db=db, _=admin)
geaendert = locations.update_location(
keller.id, LocationUpdate(name="Keller"), db=db, _=admin
)
assert geaendert.name == "Keller"
def test_lagerort_unbekannt(db, admin):
with pytest.raises(HTTPException) as fehler:
locations.update_location(999, LocationUpdate(name="Keller"), db=db, _=admin)
assert fehler.value.status_code == 404
# ---- Einheiten ----
def test_einheit_umbenennen_behaelt_die_produkte(db, admin):
einheit = units.create_unit(
UnitCreate(name="Beutle", kind="weight", factor=1000), db=db, _=admin
)
produkt = Product(name="Mehl", base_unit=BaseUnit.gram, display_unit_id=einheit.id)
db.add(produkt)
db.commit()
units.update_unit(einheit.id, UnitUpdate(name="Beutel"), db=db, _=admin)
db.refresh(produkt)
assert produkt.display_unit_id == einheit.id
assert db.get(Unit, einheit.id).name == "Beutel"
def test_eingebaute_einheit_laesst_sich_umbenennen(db, admin):
"""Anders als beim Loeschen ist Umbenennen auch eingebaut erlaubt -
genauso wie bei den Gebinden."""
gramm = db.query(Unit).filter(Unit.name == "Gramm").one()
assert gramm.is_builtin is True
units.update_unit(gramm.id, UnitUpdate(name="Gramm (g)"), db=db, _=admin)
assert db.get(Unit, gramm.id).name == "Gramm (g)"
def test_einheit_doppelter_name_wird_abgelehnt(db, admin):
einheit = units.create_unit(
UnitCreate(name="Beutel", kind="weight", factor=1000), db=db, _=admin
)
with pytest.raises(HTTPException) as fehler:
units.update_unit(einheit.id, UnitUpdate(name="gramm"), db=db, _=admin)
assert fehler.value.status_code == 409
def test_einheit_unbekannt(db, admin):
with pytest.raises(HTTPException) as fehler:
units.update_unit(999, UnitUpdate(name="Beutel"), db=db, _=admin)
assert fehler.value.status_code == 404