Files
Vorrania/backend/app/services/items.py
T
ScarriffleandClaude Opus 5 a3fd9819c4 Rueckgabe sichtbar machen, Zubehoerteile am Einzelstueck
RUECKGABE
Der Knopf war da, nur nicht zu finden: ein 16-px-Symbol in einer Spalte ohne
Ueberschrift, 52 px breit, ganz rechts in einer horizontal scrollenden Tabelle.
Beschriftet war er allein ueber title= - ein Tooltip, den auf einem Touchgeraet
nie jemand sieht.

Jetzt traegt er Text ("Zuruecknehmen"), die Spalte heisst "Verleih" und ist
breit genug, und beide Knoepfe haben ein aria-label. Dazu eine neue Seite
"Verliehen" in der Seitenleiste: alles Draussene an einem Ort, Rueckgabe direkt
aus der Liste. Bisher gab es diese Uebersicht nur als zwei Dashboard-Karten, und
die stehen nicht im Standard-Layout - wer sie nicht selbst hinzufuegte, hatte
gar keine.

In der App fuehrte der einzige Weg ueber das Bearbeiten-Formular des Stuecks;
jetzt geht es per Wischgeste und Kontextmenue direkt aus der Liste. Der
Verleih-Filter dort sass hinter einem nackten Symbol und ist beschriftet.

Zurueckgenommen wird nur noch nach Rueckfrage, in Web und App.

ZUBEHOER
Ein Netzteil gehoert zu einem Laptop: items.parent_item_id, genau ein Geraet je
Teil, beliebig viele Teile je Geraet. Eine Spalte genuegt dafuer, es braucht
keine Verknuepfungstabelle. ON DELETE SET NULL, damit das Netzteil den Laptop
ueberlebt statt mitgeloescht zu werden - und weil SQLite Fremdschluessel nicht
erzwingt, loest der Router die Zuordnung beim Loeschen zusaetzlich selbst.

Ringe (A traegt B, B traegt A) weist die Zuordnung mit 409 ab; die Elternkette
wird dafuer hochgelaufen, wie beim Kategoriebaum.

Die Beziehung laeuft ueber eigene Endpunkte und bewusst NICHT ueber ItemUpdate:
dessen Schema sendet immer alle Felder mit, auch leere, damit sich Felder
loeschen lassen. Genau so gingen schon einmal Kaufpreis und Waehrung verloren,
als ein Aufrufer ein Feld vergass.

Beim Verleihen fragt die Oberflaeche, welche Teile mitgehen (vorausgewaehlt:
alles, was im Haus ist), beim Zuruecknehmen kommen sie zusammen zurueck. Ohne
das Spiegelbild kaeme das Geraet zurueck und sein Netzteil bliebe ewig
"verliehen", weil niemand mehr danach sucht. Ungefragt nimmt der Server nie
etwas mit.

In der App sind zubehoer und gehoert_zu optional dekodiert: Die App spricht mit
mehreren Serverprofilen, und ein aelterer Server schickt die Felder nicht - ein
Vorgabewert reicht dafuer nicht, Swift nimmt ihn beim Dekodieren nicht her.

12 neue Tests (320 gesamt). Der Umlauf ist ausserdem gegen ein lokales Backend
im Browser durchgespielt: verleihen mit Zubehoer, Rueckfrage, beide zusammen
zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:08:11 +02:00

68 lines
2.5 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
from ..schemas import ItemKurz, ItemOut
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 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
# 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