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:
31
backend/app/services/items.py
Normal file
31
backend/app/services/items.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Einzelstücke (Items): UID-Erzeugung und Anreicherung für die Ausgabe."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import Item
|
||||
from ..schemas import ItemOut
|
||||
|
||||
# Ohne 0/O/1/I/L, damit die UID auf einem Etikett eindeutig lesbar bleibt.
|
||||
_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
|
||||
|
||||
|
||||
def generate_uid(db: Session, length: int = 8) -> str:
|
||||
"""Kurze, eindeutige UID erzeugen (bei Kollision neu würfeln)."""
|
||||
while True:
|
||||
code = "".join(secrets.choice(_ALPHABET) for _ in range(length))
|
||||
if db.query(Item).filter(Item.uid == code).first() is None:
|
||||
return code
|
||||
|
||||
|
||||
def item_to_out(item: Item) -> ItemOut:
|
||||
"""ItemOut inkl. Lagerort-/Shop-/Produktnamen für die Anzeige."""
|
||||
out = ItemOut.model_validate(item)
|
||||
out.location_name = item.location.name if item.location else None
|
||||
out.shop_name = item.shop.name if item.shop else None
|
||||
out.product_name = item.product.name if item.product else None
|
||||
out.product_brand = item.product.brand if item.product else None
|
||||
return out
|
||||
@@ -7,7 +7,7 @@ from datetime import date
|
||||
from sqlalchemy import asc
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import DatePrecision, Lot, Movement, MovementType, Product, User
|
||||
from ..models import DatePrecision, Item, Lot, Movement, MovementType, Product, User
|
||||
from .conversion import to_base
|
||||
from .dates import clean_precision, normalize_best_before
|
||||
|
||||
@@ -192,7 +192,14 @@ def removal_stats(db: Session, product_id: int) -> dict[str, dict]:
|
||||
|
||||
|
||||
def current_stock(db: Session, product_id: int) -> float:
|
||||
"""Summe der Lot-Mengen eines Produkts (in Basiseinheiten)."""
|
||||
"""Bestand eines Produkts (in Basiseinheiten).
|
||||
|
||||
Einzelstück-Produkte zählen die Anzahl ihrer Items, alle anderen summieren
|
||||
die Lot-Mengen.
|
||||
"""
|
||||
product = db.get(Product, product_id)
|
||||
if product is not None and product.individual:
|
||||
return float(db.query(Item).filter(Item.product_id == product_id).count())
|
||||
total = (
|
||||
db.query(Lot).with_entities(Lot.quantity).filter(Lot.product_id == product_id).all()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user