Files
Vorrania/backend/app/services/loans.py
T
ScarriffleandClaude Opus 5 467ef3c804 Einzelstuecke verleihen: eigener Vorgang statt Lagerort-Wert
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>
2026-09-17 09:16:47 +02:00

63 lines
2.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Verleih von Einzelstücken: Bestandsfilter, offener Verleih, Ausgabe.
Ein verliehenes Stück ist nicht im Haus. Es zählt deshalb **nirgends** als
Bestand – weder am Lagerort noch im Gesamtbestand; ``nicht_verliehen`` ist die
eine Stelle, an der das steht, und wird von allen Zählungen benutzt
(``services/stock.py``, ``crud.products_to_out_bulk``, Dashboard-Auswertungen).
Der Lagerort am Stück bleibt dabei erhalten: Er sagt weiter, wohin es nach der
Rückgabe zurückgehört.
"""
from __future__ import annotations
from datetime import date
from sqlalchemy.orm import Query, Session
from ..models import Item, ItemLoan
from ..schemas import LoanOut
def nicht_verliehen(query: Query) -> Query:
"""Verliehene Einzelstücke aus einer Item-Abfrage nehmen.
Arbeitet als korreliertes NOT EXISTS und ist damit unabhängig davon, ob die
Abfrage ganze Items oder nur Spalten (``count``) auswählt.
"""
return query.filter(~Item.loans.any(ItemLoan.returned_on.is_(None)))
def offener_verleih(db: Session, item_id: int) -> ItemLoan | None:
"""Der laufende Verleih eines Stücks, falls es gerade draußen ist."""
return (
db.query(ItemLoan)
.filter(ItemLoan.item_id == item_id, ItemLoan.returned_on.is_(None))
.first()
)
def tage_ueberfaellig(loan: ItemLoan, heute: date | None = None) -> int | None:
"""Tage über dem Rückgabedatum – None ohne Datum, zurück oder noch nicht fällig."""
if loan.due_on is None or loan.returned_on is not None:
return None
tage = ((heute or date.today()) - loan.due_on).days
return tage if tage > 0 else None
def loan_to_out(loan: ItemLoan, heute: date | None = None) -> LoanOut:
"""LoanOut inkl. Überfälligkeit und – für die Übersicht – Stück und Artikel."""
out = LoanOut.model_validate(loan)
out.days_overdue = tage_ueberfaellig(loan, heute)
out.username = loan.user.username if loan.user else None
item = loan.item
if item is not None:
out.item_uid = item.uid
out.product_id = item.product_id
out.location_id = item.location_id
out.location_name = item.location.name if item.location else None
if item.product is not None:
out.product_name = item.product.name
out.product_brand = item.product.brand
return out