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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ba84e4ea01
commit
467ef3c804
@@ -750,6 +750,12 @@ class Item(Base):
|
||||
documents: Mapped[list["ItemDocument"]] = relationship(
|
||||
cascade="all, delete-orphan", order_by="ItemDocument.id"
|
||||
)
|
||||
# Verleih-Historie, neueste zuerst. Der offene Verleih (returned_on NULL)
|
||||
# ist hoechstens einer, siehe ItemLoan.
|
||||
loans: Mapped[list["ItemLoan"]] = relationship(
|
||||
back_populates="item", cascade="all, delete-orphan",
|
||||
order_by="ItemLoan.lent_on.desc(), ItemLoan.id.desc()",
|
||||
)
|
||||
|
||||
|
||||
class ItemDocument(Base):
|
||||
@@ -772,6 +778,56 @@ class ItemDocument(Base):
|
||||
uploaded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
|
||||
class ItemLoan(Base):
|
||||
"""Ein Verleih-Vorgang eines Einzelstücks: an wen, warum, bis wann.
|
||||
|
||||
Bewusst eine eigene Tabelle statt eines Lagerort-Werts „verliehen": Der
|
||||
Lagerort bleibt stehen und sagt weiter, wohin das Stück nach der Rückgabe
|
||||
zurückgehört. Zurückgegebene Verleihe bleiben als Zeile erhalten – damit ist
|
||||
die Tabelle gleichzeitig die Verleih-Historie je Stück.
|
||||
|
||||
``returned_on`` NULL heißt „noch draußen"; ein Stück kann nur einen solchen
|
||||
offenen Verleih haben (Teilindex unten). Verliehene Stücke zählen nirgends
|
||||
als Bestand, siehe ``services/loans.py``.
|
||||
"""
|
||||
|
||||
__tablename__ = "item_loans"
|
||||
__table_args__ = (
|
||||
# Zweimal gleichzeitig verliehen kann ein einzelnes Stück nicht sein.
|
||||
# Als Teilindex, weil nur die OFFENEN Verleihe eindeutig sein dürfen:
|
||||
# zurückgegebene bleiben ja beliebig oft je Stück stehen.
|
||||
Index(
|
||||
"uq_item_loan_offen",
|
||||
"item_id",
|
||||
unique=True,
|
||||
postgresql_where=text("returned_on IS NULL"),
|
||||
sqlite_where=text("returned_on IS NULL"),
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
item_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("items.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
# Freitext: Entleiher sind keine Vorrania-Benutzer (Nachbar, Kollege …).
|
||||
borrower: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
lent_on: Mapped[date] = mapped_column(Date, nullable=False, default=date.today)
|
||||
# Erwartete Rückgabe – bewusst optional: „kurz mal mitgenommen" soll nicht
|
||||
# an einem Datumszwang scheitern.
|
||||
due_on: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
returned_on: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
# Wer herausgegeben hat. SET NULL, damit ein gelöschtes Konto den Vorgang
|
||||
# nicht mitnimmt.
|
||||
user_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
item: Mapped[Item] = relationship(back_populates="loans")
|
||||
user: Mapped[User | None] = relationship()
|
||||
|
||||
|
||||
class ProductLocationMinStock(Base):
|
||||
"""Mindestbestand eines Produkts an EINEM Lagerort (in Basiseinheiten).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user