Files
Vorrania/backend/app/services/items.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

93 lines
3.6 KiB
Python

"""Einzelstücke (Items): UID-Erzeugung und Anreicherung für die Ausgabe."""
from __future__ import annotations
import secrets
from sqlalchemy.orm import Session
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.
_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
def generate_uid(db: Session, length: int = 8) -> str:
"""Kurze, eindeutige UID erzeugen (bei Kollision neu würfeln)."""
while True:
code = "".join(secrets.choice(_ALPHABET) for _ in range(length))
if db.query(Item).filter(Item.uid == code).first() is None:
return code
def _offener(item: Item) -> ItemLoan | None:
"""Der laufende Verleih aus den schon geladenen Verleihen des Stücks.
Bewusst über die Beziehung statt per eigener Abfrage: So bleibt die
Einzelstück-Liste bei einer Abfrage, wenn der Aufrufer ``loans`` mitlädt.
"""
for loan in item.loans:
if loan.returned_on is None:
return loan
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(
id=item.id,
uid=item.uid,
product_id=item.product_id,
product_name=item.product.name if item.product else None,
product_brand=item.product.brand if item.product else None,
location_name=item.location.name if item.location else None,
verliehen=_offener(item) is not None,
)
def item_to_out(item: Item) -> ItemOut:
"""ItemOut inkl. Lagerort-/Shop-/Produktnamen, Verleih und Zubehoer."""
out = ItemOut.model_validate(item)
out.location_name = item.location.name if item.location else None
out.shop_name = item.shop.name if item.shop else None
out.product_name = item.product.name if item.product else None
out.product_brand = item.product.brand if item.product else None
kategorie = item.product.category if item.product else None
out.category_id = kategorie.id if kategorie else None
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.
out.zubehoer = [item_kurz(t) for t in item.zubehoer]
out.gehoert_zu = item_kurz(item.gehoert_zu) if item.gehoert_zu else None
return out