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>
This commit is contained in:
Scarriffle
2026-10-20 21:47:02 +02:00
co-authored by Claude Opus 5
parent 8c752e3d1f
commit 98e2d18142
10 changed files with 691 additions and 6 deletions
+27 -2
View File
@@ -6,8 +6,8 @@ import secrets
from sqlalchemy.orm import Session
from ..models import Item, ItemLoan
from ..schemas import ItemKurz, ItemOut
from ..models import Item, ItemLoan, ItemUsage
from ..schemas import ItemKurz, ItemOut, VerwendungOut
from .loans import loan_to_out
# Ohne 0/O/1/I/L, damit die UID auf einem Etikett eindeutig lesbar bleibt.
@@ -34,6 +34,26 @@ def _offener(item: Item) -> ItemLoan | None:
return None
def _offene_verwendung(item: Item) -> ItemUsage | None:
"""Der laufende Verwendungszustand aus den schon geladenen Zeitraeumen.
Wie ``_offener``: ueber die Beziehung statt per eigener Abfrage, damit die
Einzelstueck-Liste nicht je Zeile nachfragt. Keine offene Zeile heisst
verfuegbar - dafuer gibt es keinen gespeicherten Wert.
"""
for usage in item.usages:
if usage.ended_at is None:
return usage
return None
def verwendung_to_out(usage: ItemUsage) -> VerwendungOut:
"""VerwendungOut samt Konto, das umgestellt hat."""
out = VerwendungOut.model_validate(usage)
out.username = usage.user.username if usage.user else None
return out
def item_kurz(item: Item) -> ItemKurz:
"""Ein verknuepftes Stueck in Kurzform (Zubehoer, Geraet)."""
return ItemKurz(
@@ -59,6 +79,11 @@ def item_to_out(item: Item) -> ItemOut:
out.category_name = kategorie.name if kategorie else None
loan = _offener(item)
out.loan = loan_to_out(loan) if loan is not None else None
# Die Kategorie steckt schon oben in ``kategorie`` - die Markierung kostet
# deshalb keine zusaetzliche Abfrage (siehe Category.usage_tracking).
out.verwendung_moeglich = bool(kategorie.usage_tracking) if kategorie else False
verwendung = _offene_verwendung(item)
out.verwendung = verwendung_to_out(verwendung) if verwendung is not None else None
# Beide Richtungen ausdruecklich setzen: model_validate wuerde die Beziehung
# zwar finden, aber nur die Spalten fuellen - Artikelname und Lagerort des
# verknuepften Stuecks blieben leer.
+3 -1
View File
@@ -35,7 +35,8 @@ def export_master_data(db: Session) -> dict:
return {
FORMAT_KEY: FORMAT_VERSION,
"categories": [
{"id": c.id, "name": c.name, "parent_id": c.parent_id, "tracking": c.tracking}
{"id": c.id, "name": c.name, "parent_id": c.parent_id,
"tracking": c.tracking, "usage_tracking": c.usage_tracking}
for c in db.query(Category).order_by(Category.id).all()
],
"field_definitions": [
@@ -152,6 +153,7 @@ def import_master_data(db: Session, data: dict, mode: str = "skip") -> dict:
for row in cats:
upsert(Category, row, "categories", lambda r: {
"name": r["name"], "tracking": r.get("tracking") or "food",
"usage_tracking": bool(r.get("usage_tracking", False)),
}, is_tree=True)
set_parents(Category, cats)