Der Lagerort bleibt beim Verleihen stehen - er sagt weiter, wohin das Stueck nach der Rueckgabe zurueckgehoert. Ein Lagerort-Wert "verliehen" haette diese Information vernichtet. Stattdessen eine eigene Tabelle item_loans mit Entleiher und Grund als Freitext, Verleihdatum und einem optionalen Rueckgabedatum: "kurz mal mitgenommen" soll nicht an einem Datumszwang scheitern. Zurueckgegebene Vorgaenge bleiben stehen, damit die Tabelle gleichzeitig die Verleih-Historie je Stueck ist. Ein Teilindex auf die offenen Vorgaenge haelt fest, dass ein Stueck nicht zweimal gleichzeitig verliehen sein kann - wie bei den "Ueberall"-Mindestbestaenden, weil Postgres NULLs in UNIQUE als verschieden zaehlt. Verliehen heisst nicht im Bestand: Ein Stueck, das nicht im Haus ist, zaehlt nirgends mehr mit - weder am Lagerort noch im Gesamtbestand. Entschieden wird das an einer Stelle (services/loans.py::nicht_verliehen), die alle fuenf Zaehlungen benutzen. Sonst behauptete "Was liegt hier?" weiterhin, die Kamera liege im Regal. Folge: Faellt ein verliehenes Stueck unter den Mindestbestand, erscheint der Artikel auf der Einkaufsliste. Verleihen und Zuruecknehmen darf jeder angemeldete Benutzer - es ist eine Buchung wie Ein- und Auslagern, keine Stammdatenaenderung - und wird mit dem Konto protokolliert. Nur das Loeschen eines Vorgangs bleibt Admins vorbehalten, weil es Historie vernichtet. Bewusst keine Movement-Bewegung je Verleih: Der Verlauf rechnet den Bestand rueckwaerts aus vorzeichenbehafteten Bewegungen, ein Abgang ohne Gegenbuchung wuerde diese Kurve verfaelschen. GET /loans?due_in= liefert die faelligen Verleihe und ist damit schon die Datenquelle fuer die spaetere Rueckgabe-Erinnerung der App. Oberflaeche in Web und App fehlt noch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
50 lines
1.8 KiB
Python
50 lines
1.8 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 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_to_out(item: Item) -> ItemOut:
|
|
"""ItemOut inkl. Lagerort-/Shop-/Produktnamen und laufendem Verleih."""
|
|
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
|
|
return out
|