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:
@@ -281,6 +281,8 @@ class ProductBase(BaseModel):
|
||||
# Nur für Gegenstände: Bezugsquelle und Onlineshop-Link.
|
||||
shop_id: int | None = None
|
||||
product_url: str | None = Field(default=None, max_length=1024)
|
||||
# Gegenstände als Einzelstücke (Items mit UID/QR) statt als Menge führen.
|
||||
individual: bool = False
|
||||
# Selbst definierte Feldwerte: {field_definition_id: Wert-als-Text}.
|
||||
field_values: dict[int, str | None] | None = None
|
||||
|
||||
@@ -306,6 +308,7 @@ class ProductUpdate(BaseModel):
|
||||
min_stock_in_packages: bool | None = None
|
||||
shop_id: int | None = None
|
||||
product_url: str | None = Field(default=None, max_length=1024)
|
||||
individual: bool | None = None
|
||||
field_values: dict[int, str | None] | None = None
|
||||
|
||||
|
||||
@@ -330,6 +333,7 @@ class ProductOut(BaseModel):
|
||||
source: str
|
||||
shop_id: int | None = None
|
||||
product_url: str | None = None
|
||||
individual: bool = False
|
||||
created_at: datetime
|
||||
# angereichert:
|
||||
stock: float = 0.0
|
||||
@@ -498,6 +502,48 @@ class RemovalSummary(BaseModel):
|
||||
history: list[RemovalHistoryItem] = []
|
||||
|
||||
|
||||
# ---- Einzelstücke (Items mit UID/QR) ----
|
||||
class ItemCreate(BaseModel):
|
||||
"""Ein oder mehrere Einzelstücke mit gemeinsamen Startwerten anlegen."""
|
||||
count: int = Field(default=1, ge=1, le=200)
|
||||
location_id: int | None = None
|
||||
shop_id: int | None = None
|
||||
acquired_on: date | None = None
|
||||
warranty_until: date | None = None
|
||||
note: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class ItemUpdate(BaseModel):
|
||||
location_id: int | None = None
|
||||
shop_id: int | None = None
|
||||
acquired_on: date | None = None
|
||||
warranty_until: date | None = None
|
||||
note: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class ItemRemove(BaseModel):
|
||||
reason: RemovalReason
|
||||
note: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class ItemOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
uid: str
|
||||
product_id: int
|
||||
location_id: int | None = None
|
||||
location_name: str | None = None
|
||||
shop_id: int | None = None
|
||||
shop_name: str | None = None
|
||||
acquired_on: date | None = None
|
||||
warranty_until: date | None = None
|
||||
note: str | None = None
|
||||
created_at: datetime
|
||||
# Für QR-Auflösung/Anzeige mitgeliefert:
|
||||
product_name: str | None = None
|
||||
product_brand: str | None = None
|
||||
|
||||
|
||||
# ---- Views ----
|
||||
class ShoppingItem(BaseModel):
|
||||
product_id: int
|
||||
|
||||
Reference in New Issue
Block a user