Backend: Einzelstücke (Items) mit UID je Gegenstand

Gegenstands-Produkte koennen als Einzelstuecke gefuehrt werden (Product.individual):
jedes physische Stueck ist ein Item mit eigener kurzer UID (fuer QR), eigenem
Lagerort, Kaufdatum, Garantie, Bezugsquelle und Notiz. So laesst sich dasselbe
Modell mehrfach getrennt fuehren (Powerbank 2024 + 2025).

Neu: models.Item + Product.individual (+Migration), Schemas, services/items.py
(UID-Erzeugung, Anreicherung), routers/items.py (CRUD, /items/by-uid/{uid} zur
QR-Aufloesung, /items/{id}/remove mit Grund als Bewegung). current_stock zaehlt
bei Einzelstueck-Produkten die Items. Tests + HTTP-Smoke gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-26 00:26:15 +02:00
parent 68e27c1868
commit 5ce4411d1e
8 changed files with 326 additions and 2 deletions

View File

@@ -229,6 +229,10 @@ class Product(Base):
ForeignKey("shops.id", ondelete="SET NULL"), nullable=True
)
product_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
# Gegenstände: als Einzelstücke (Items mit eigener UID/QR) statt als Menge je
# Lagerort verwalten. So bekommt jedes physische Stück ein eigenes Kaufdatum,
# eine eigene Garantie und Bezugsquelle (z.B. dieselbe Powerbank 2024 + 2025).
individual: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
@@ -498,3 +502,36 @@ class ProductFieldValue(Base):
product: Mapped[Product] = relationship(back_populates="field_values")
field_definition: Mapped[FieldDefinition] = relationship()
class Item(Base):
"""Ein physisches Einzelstück eines Gegenstands (Instanz) mit eigener UID/QR.
Nur für Gegenstands-Produkte mit Einzelstück-Verwaltung (``Product.individual``).
Je Stück ein eigener Lagerort, Kaufdatum, Garantie, Bezugsquelle und Notiz
so lässt sich dasselbe Modell mehrfach getrennt führen (Powerbank 2024 + 2025).
Der QR-Code trägt die UID; ein Scan öffnet genau dieses Stück.
"""
__tablename__ = "items"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Kurzer, gut lesbarer Code für den QR (ohne verwechselbare Zeichen).
uid: Mapped[str] = mapped_column(String(16), unique=True, index=True, nullable=False)
product_id: Mapped[int] = mapped_column(
ForeignKey("products.id", ondelete="CASCADE"), nullable=False
)
location_id: Mapped[int | None] = mapped_column(
ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
)
shop_id: Mapped[int | None] = mapped_column(
ForeignKey("shops.id", ondelete="SET NULL"), nullable=True
)
acquired_on: Mapped[date | None] = mapped_column(Date, nullable=True) # gekauft am
warranty_until: Mapped[date | None] = mapped_column(Date, nullable=True) # Garantie bis
note: Mapped[str | None] = mapped_column(String(255), nullable=True) # Notiz/Zustand
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
product: Mapped[Product] = relationship()
location: Mapped[Location | None] = relationship()
shop: Mapped[Shop | None] = relationship()