Files
Vorrania/backend/app/routers/verwendung.py
T
ScarriffleandClaude Opus 5 98e2d18142 Verwendungsstatus: Backend fuer "getragen / in Reinigung / in Reparatur"
Einzelstuecke hatten bisher keinen Zustand - der Lagerort sagt, wohin ein
Stueck gehoert, aber nicht, woran es gerade ist. Neue Tabelle item_usages
nach dem Vorbild von item_loans: ein Zeitraum je Zustand, ended_at NULL
heisst "laeuft noch", und ein Teilindex laesst nur einen laufenden zu.

"Verfuegbar" wird bewusst nicht gespeichert - keine offene Zeile IST
verfuegbar, wie bei lots.opened_on und beim Verleih. Damit kostet der
Ruhezustand nichts und die Tabelle fuellt sich nicht mit Rauschen.
Abgeschlossene Zeilen bleiben stehen, sie sind der Verlauf.

Eigener Endpunkt (PUT /items/{id}/verwendung) und ausdruecklich nicht
PATCH /items/{id}: Das ist Admins vorbehalten, und die App schickt dort
immer alle Felder mitsamt null - jedes Speichern wuerde den Status
stillschweigend zuruecksetzen. Umstellen darf deshalb jeder angemeldete
Benutzer, wie Verleihen.

Der Zustand aendert den Bestand nicht und schreibt keine Bewegung: ein
getragenes Hemd ist weiterhin im Haushalt, anders als ein verliehenes.

Sichtbar nur bei Kategorien mit usage_tracking. Ein Haken an einer
Oberkategorie wandert in den Unterbaum, weil die Garderobe als
Kategoriebaum ja schon existiert. Die Markierung fahrt in beiden
Sicherungswegen mit; der Verlauf nicht, weil Einzelstuecke dort
ueberhaupt nicht vorkommen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-20 21:47:02 +02:00

105 lines
3.9 KiB
Python
Raw 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.
"""Verwendungsstatus von Einzelstücken: umstellen und Verlauf.
Woran ist das Stück gerade? Getragen, in der Wäsche, in Reparatur – oder
einfach verfügbar. Gedacht für eine Garderobe, aber bewusst allgemein: ein Auto
in der Waschanlage ist genauso „in Reinigung".
Umstellen ist eine **Buchung**, keine Stammdatenänderung – deshalb darf jeder
angemeldete Benutzer es tun (wie Verleihen, Ein- und Auslagern), und es wird mit
dem Konto protokolliert. Ausdrücklich **nicht** über ``PATCH /items/{id}``:
Diese Route ist Admins vorbehalten, und die App schickt dort immer alle Felder
mitsamt ``null`` (siehe ``ItemUpdateRequest.encode`` in ios/Sources/Models.swift)
– jedes beliebige Speichern würde den Status stillschweigend zurücksetzen.
Verleih und Zubehör haben aus demselben Grund eigene Endpunkte.
Bewusst **keine** Bewegung (``Movement``), aus demselben Grund wie beim Verleih
(siehe ``routers/loans.py``). Und bewusst **keine** Auswirkung auf den Bestand:
Ein getragenes Hemd ist weiterhin im Haushalt – anders als ein verliehenes.
"""
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session, joinedload
from ..database import get_db
from ..deps import get_current_user
from ..models import Item, ItemUsage, User
from ..schemas import ItemOut, VerwendungOut, VerwendungSetzen
from ..services.items import item_to_out, verwendung_to_out
router = APIRouter(tags=["verwendung"])
def _item_or_404(db: Session, item_id: int) -> Item:
item = db.get(Item, item_id)
if item is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Einzelstück nicht gefunden")
return item
def _laufend(db: Session, item_id: int) -> ItemUsage | None:
"""Der laufende Zeitraum dieses Stücks, falls einer offen ist."""
return (
db.query(ItemUsage)
.filter(ItemUsage.item_id == item_id, ItemUsage.ended_at.is_(None))
.first()
)
@router.put("/items/{item_id}/verwendung", response_model=ItemOut)
def setze_verwendung(
item_id: int,
payload: VerwendungSetzen,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> ItemOut:
"""Verwendungszustand setzen. Leerer Status = verfügbar.
Eine Route für alle Zustände, auch für den Ruhezustand: „verfügbar" ist
nichts anderes als „kein laufender Zeitraum", also beendet der leere Status
nur den aktuellen. Damit ist der Aufruf idempotent.
Antwortet mit dem ganzen Stück, damit die Oberflächen nach der Buchung nicht
noch einmal laden müssen.
"""
item = _item_or_404(db, item_id)
neu = payload.status.value if payload.status is not None else None
laufend = _laufend(db, item.id)
if laufend is not None and laufend.status == neu:
# Zweimal dasselbe angetippt – sonst entstünde ein Zeitraum der Länge 0.
return item_to_out(item)
if laufend is not None:
laufend.ended_at = datetime.now(UTC)
# Vor dem Einfügen schreiben: Sonst darf SQLAlchemy das INSERT vor das
# UPDATE stellen, und dann liegen kurzzeitig zwei offene Zeiträume an –
# der Teilindex uq_item_usage_offen schlägt zu.
db.flush()
if neu is not None:
db.add(ItemUsage(item_id=item.id, status=neu, user_id=user.id))
db.commit()
db.refresh(item)
return item_to_out(item)
@router.get("/items/{item_id}/verwendungen", response_model=list[VerwendungOut])
def list_verwendungen(
item_id: int,
db: Session = Depends(get_db),
_: User = Depends(get_current_user),
) -> list[VerwendungOut]:
"""Der ganze Verlauf dieses Stücks, neueste zuerst."""
_item_or_404(db, item_id)
rows = (
db.query(ItemUsage)
.options(joinedload(ItemUsage.user))
.filter(ItemUsage.item_id == item_id)
.order_by(ItemUsage.started_at.desc(), ItemUsage.id.desc())
.all()
)
return [verwendung_to_out(u) for u in rows]