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

@@ -14,6 +14,7 @@ from app.models import (
Category,
CategoryTracking,
FieldDefinition,
Item,
Location,
Lot,
Movement,
@@ -21,6 +22,7 @@ from app.models import (
ProductFieldValue,
RemovalReason,
)
from app.services.items import generate_uid
from app.routers.categories import create_category
from app.schemas import CategoryCreate
from app.services.fields import (
@@ -236,3 +238,40 @@ def test_feldwert_setzen_und_leeren(db):
apply_field_values(db, product, {fd.id: ""})
db.commit()
assert db.query(ProductFieldValue).filter_by(product_id=product.id).count() == 0
# ---- Einzelstücke (Items mit UID/QR) ----
def _individual_product(db, name="Powerbank"):
cat = Category(name="Elektronik", tracking=CategoryTracking.object.value)
db.add(cat)
db.flush()
product = Product(name=name, category_id=cat.id, individual=True)
db.add(product)
db.commit()
db.refresh(product)
return product
def test_uid_ist_eindeutig(db):
codes = {generate_uid(db) for _ in range(50)}
assert len(codes) == 50
# Keine verwechselbaren Zeichen (0/O/1/I/L) in der UID.
assert all(ch not in "01OIL" for code in codes for ch in code)
def test_einzelstueck_bestand_zaehlt_items(db):
product = _individual_product(db)
for _ in range(3):
db.add(Item(uid=generate_uid(db), product_id=product.id))
db.commit()
assert current_stock(db, product.id) == 3
def test_mengen_produkt_zaehlt_weiter_lots(db):
# Ein Nicht-Einzelstück-Produkt zählt weiterhin über Lots, nicht über Items.
product, _ = _object_product(db, name="Unterhose")
a = _loc(db, "Schrank A")
object_add(db, product, 5, a.id, None)
db.commit()
assert current_stock(db, product.id) == 5