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>
93 lines
3.6 KiB
Python
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
|