Verwendungsstatus: Backend fuer "getragen / in Reinigung / in Reparatur"
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8c752e3d1f
commit
98e2d18142
@@ -98,6 +98,24 @@ class RemovalReason(str, enum.Enum):
|
||||
other = "other" # sonstiges
|
||||
|
||||
|
||||
class UsageStatus(str, enum.Enum):
|
||||
"""Woran ein Einzelstueck gerade ist (Hemd getragen, Auto in der Wasche).
|
||||
|
||||
Bewusst KEIN Wert fuer "verfuegbar": Fehlt die offene Zeile in
|
||||
:class:`ItemUsage`, ist das Stueck verfuegbar - wie bei ``lots.opened_on``
|
||||
und beim Verleih. Ein eigener Wert daneben koennte ihr widersprechen.
|
||||
|
||||
Absichtlich allgemein und nicht auf Kleidung zugeschnitten: "in_use" traegt
|
||||
auch eine Bohrmaschine auf der Baustelle.
|
||||
|
||||
Als kurzer String gespeichert (wie CategoryTracking, RemovalReason), damit
|
||||
sich die Spalte per ADD COLUMN nachziehen laesst.
|
||||
"""
|
||||
in_use = "in_use" # wird gerade benutzt/getragen
|
||||
cleaning = "cleaning" # in der Waesche/Reinigung
|
||||
repair = "repair" # in Reparatur
|
||||
|
||||
|
||||
class Unit(Base):
|
||||
"""Vom Admin verwaltbare Einheit mit Umrechnungsfaktor zur kanonischen Basis.
|
||||
|
||||
@@ -353,6 +371,15 @@ class Category(Base):
|
||||
# NULL = erbt von der Oberkategorie, zuletzt vom globalen Wert
|
||||
# (services/vererbung.py::oeffnungsfrist).
|
||||
open_days: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
# Ob Einzelstuecke dieser Kategorie einen Verwendungsstatus fuehren
|
||||
# (Kleidung, Werkzeug - nicht Konserven). Wie tracking eine Kopie beim
|
||||
# Anlegen und keine Aufloesung beim Lesen: So kommt die Markierung ohne
|
||||
# zusaetzliche Abfrage am Einzelstueck an (services/items.py::item_to_out).
|
||||
# Ein Haken an einer Oberkategorie wandert beim Speichern in den Unterbaum,
|
||||
# siehe routers/categories.py::update_category.
|
||||
usage_tracking: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=False, server_default=text("false")
|
||||
)
|
||||
|
||||
|
||||
class Product(Base):
|
||||
@@ -1069,6 +1096,12 @@ class Item(Base):
|
||||
back_populates="item", cascade="all, delete-orphan",
|
||||
order_by="ItemLoan.lent_on.desc(), ItemLoan.id.desc()",
|
||||
)
|
||||
# Verwendungs-Historie, neueste zuerst. Der laufende Zustand (ended_at NULL)
|
||||
# ist hoechstens einer, siehe ItemUsage. Keine offene Zeile = verfuegbar.
|
||||
usages: Mapped[list["ItemUsage"]] = relationship(
|
||||
back_populates="item", cascade="all, delete-orphan",
|
||||
order_by="ItemUsage.started_at.desc(), ItemUsage.id.desc()",
|
||||
)
|
||||
|
||||
|
||||
class ItemDocument(Base):
|
||||
@@ -1141,6 +1174,59 @@ class ItemLoan(Base):
|
||||
user: Mapped[User | None] = relationship()
|
||||
|
||||
|
||||
class ItemUsage(Base):
|
||||
"""Ein Zeitraum, in dem ein Einzelstueck in einem Verwendungszustand war.
|
||||
|
||||
Bewusst eine eigene Tabelle und keine Spalte am Einzelstueck: So bleibt
|
||||
nachvollziehbar, seit wann ein Hemd in der Waesche liegt und wie oft es
|
||||
getragen wurde. Abgeschlossene Zeitraeume bleiben stehen - damit ist die
|
||||
Tabelle gleichzeitig die Historie je Stueck.
|
||||
|
||||
``ended_at`` NULL heisst "laeuft noch"; ein Stueck kann nur einen solchen
|
||||
laufenden Zustand haben (Teilindex unten). Es gibt keine Zeile fuer
|
||||
"verfuegbar" - keine offene Zeile IST verfuegbar.
|
||||
|
||||
Anders als der Verleih aendert der Zustand den Bestand NICHT: ein getragenes
|
||||
Hemd ist weiterhin im Haushalt. Und wie beim Verleih wird keine Bewegung
|
||||
(Movement) geschrieben, siehe routers/loans.py.
|
||||
"""
|
||||
|
||||
__tablename__ = "item_usages"
|
||||
__table_args__ = (
|
||||
# Zwei Zustaende gleichzeitig kann ein Stueck nicht haben. Als Teilindex,
|
||||
# weil nur die LAUFENDEN eindeutig sein duerfen: abgeschlossene bleiben
|
||||
# beliebig oft je Stueck stehen.
|
||||
Index(
|
||||
"uq_item_usage_offen",
|
||||
"item_id",
|
||||
unique=True,
|
||||
postgresql_where=text("ended_at IS NULL"),
|
||||
sqlite_where=text("ended_at 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
|
||||
)
|
||||
# UsageStatus-Wert als kurzer String (kein Postgres-Enumtyp).
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
# Zeitstempel und nicht nur ein Datum: Ein Hemd kann an einem Tag getragen,
|
||||
# gewaschen und wieder eingeraeumt werden.
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=_now
|
||||
)
|
||||
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
# Wer umgestellt hat. SET NULL, damit ein geloeschtes Konto den Vorgang
|
||||
# nicht mitnimmt.
|
||||
user_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
item: Mapped[Item] = relationship(back_populates="usages")
|
||||
user: Mapped[User | None] = relationship()
|
||||
|
||||
|
||||
class ProductLocationMinStock(Base):
|
||||
"""Mindestbestand eines Produkts an EINEM Lagerort (in Basiseinheiten).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user