Backend: globale Einzelstueck-Liste (GET /items) + Bild-Version an Produkten

Neuer Endpoint GET /items liefert alle Einzelstuecke ueber alle Produkte, angereichert mit Produkt-, Kategorie-, Lagerort- und Shop-Angaben (Beziehungen vorgeladen). ItemOut bekommt category_id/category_name. ProductOut bekommt image_version (Epoch der letzten Bildaenderung, None ohne Bild) - identisch zum ETag der Bild-Route, damit Clients Bilder cachen und nur bei Aenderung neu laden. Grundlage fuer die neue Einzelstueck-Liste (Web+iOS) und den iOS-Bild-Cache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 14:37:17 +02:00
parent 34149ff633
commit 72256f4fe9
6 changed files with 87 additions and 4 deletions

View File

@@ -7,7 +7,7 @@ from datetime import date
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from .models import Barcode, Category, CategoryTracking, Lot, Product
from .models import Barcode, Category, CategoryTracking, Lot, Product, ProductImage
from .schemas import BarcodeOut, LocationMinStockOut, ProductOut
from .services.conversion import KIND_OF_BASE, display_unit_info
from .services.stock import current_stock
@@ -44,6 +44,14 @@ def product_to_out(db: Session, product: Product) -> ProductOut:
for b in db.query(Barcode).filter(Barcode.product_id == product.id).order_by(Barcode.id).all()
]
out.category_name = product.category.name if product.category else None
# Bild-Version = Epoch der letzten Bildänderung (identisch zum ETag der
# Bild-Route), damit der Client nur geänderte Bilder neu lädt. None = kein Bild.
bild_ts = (
db.query(ProductImage.updated_at)
.filter(ProductImage.product_id == product.id)
.scalar()
)
out.image_version = int(bild_ts.timestamp()) if bild_ts is not None else None
out.tracking = CategoryTracking(product_tracking(db, product))
out.shop_name = product.shop.name if product.shop else None
out.kind = KIND_OF_BASE[product.base_unit].value

View File

@@ -9,7 +9,7 @@ import re
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import Response
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from ..database import get_db
from ..deps import get_current_user, require_admin
@@ -115,6 +115,30 @@ def create_items(
return [item_to_out(i) for i in created]
@router.get("/items", response_model=list[ItemOut])
def list_all_items(
db: Session = Depends(get_db),
_: User = Depends(get_current_user),
) -> list[ItemOut]:
"""Alle Einzelstücke über alle Produkte Grundlage der Einzelstück-Liste.
Angereichert mit Produkt-, Kategorie-, Lagerort- und Shop-Angaben; die
Beziehungen werden vorgeladen, damit die Liste nicht in N+1-Abfragen zerfällt.
"""
rows = (
db.query(Item)
.options(
joinedload(Item.product).joinedload(Product.category),
joinedload(Item.location),
joinedload(Item.shop),
joinedload(Item.documents),
)
.order_by(Item.id)
.all()
)
return [item_to_out(i) for i in rows]
@router.get("/items/by-uid/{uid}", response_model=ItemOut)
def item_by_uid(
uid: str,

View File

@@ -354,6 +354,9 @@ class ProductOut(BaseModel):
# angereichert:
stock: float = 0.0
expired_count: int = 0
# Epoch (Sekunden) der letzten Bildänderung, sonst None (= kein Bild). Der
# Client cacht Bilder danach und lädt nur bei geänderter Version neu.
image_version: int | None = None
kind: str = ""
unit_name: str = ""
unit_factor: float = 1.0
@@ -591,6 +594,8 @@ class ItemOut(BaseModel):
# Für QR-Auflösung/Anzeige mitgeliefert:
product_name: str | None = None
product_brand: str | None = None
category_id: int | None = None
category_name: str | None = None
documents: list[ItemDocumentOut] = []

View File

@@ -28,4 +28,7 @@ def item_to_out(item: Item) -> ItemOut:
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
kategorie = item.product.category if item.product else None
out.category_id = kategorie.id if kategorie else None
out.category_name = kategorie.name if kategorie else None
return out