Nachschlagen: Lagerort-QR zeigt alle Artikel an diesem Ort

Neuer Endpunkt GET /location/{code} liefert alle Artikel an einem Lagerort inkl. der Unterorte (Lot-Bestaende und Einzelstuecke), sortiert nach Name mit Menge in Artikeleinheiten. In der App loest der /l/-QR beim Nachschlagen jetzt diese Liste auf; ein Tipp auf einen Eintrag oeffnet den Artikel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 13:10:29 +02:00
parent 4607e3a6a8
commit 21ab0a825a
6 changed files with 258 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
from collections import defaultdict
from datetime import date, timedelta
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ..database import get_db
@@ -9,6 +9,7 @@ from ..deps import get_current_user
from ..models import (
Group,
GroupLocationMinStock,
Item,
Location,
Lot,
Movement,
@@ -19,6 +20,8 @@ from ..models import (
from ..schemas import (
ExpiringItem,
GroupShoppingItem,
LocationContentEntry,
LocationContents,
LocationNeedGroup,
LocationNeedProduct,
LocationNeeds,
@@ -26,7 +29,11 @@ from ..schemas import (
ShoppingItem,
)
from ..services.conversion import BASE_OF_KIND, article_unit, display_unit_info
from ..services.stock import current_stock, location_subtree_stock_base
from ..services.stock import (
current_stock,
descendant_location_ids,
location_subtree_stock_base,
)
from .settings import get_expiry_warning_days
router = APIRouter(tags=["views"])
@@ -159,6 +166,47 @@ def shopping_list_by_location(
return result
@router.get("/location/{code}", response_model=LocationContents)
def location_contents(
code: str,
db: Session = Depends(get_db),
_: User = Depends(get_current_user),
) -> LocationContents:
"""Alle Artikel, die an einem Lagerort liegen inklusive der Unterorte.
Ziel des Lagerort-QR (`/l/<code>`) beim Nachschlagen: einmal scannen und
sehen, was im Regal/Fach (und allem darunter) steht. Zählt sowohl
Lot-Bestände (Lebensmittel/Objekte) als auch Einzelstücke.
"""
loc = db.get(Location, code)
if loc is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Lagerort nicht gefunden")
ids = {code} | descendant_location_ids(db, code)
product_ids: set[int] = {
pid for (pid,) in db.query(Lot.product_id).filter(Lot.location_id.in_(ids)).distinct()
}
product_ids |= {
pid for (pid,) in db.query(Item.product_id).filter(Item.location_id.in_(ids)).distinct()
}
entries: list[LocationContentEntry] = []
for pid in product_ids:
product = db.get(Product, pid)
if product is None:
continue
faktor, label = article_unit(product)
stock = location_subtree_stock_base(db, product, code) / (faktor or 1.0)
if stock <= 0:
continue
entries.append(LocationContentEntry(
product_id=product.id, name=product.name, brand=product.brand,
stock=round(stock, 3), unit_label=label, individual=product.individual,
))
entries.sort(key=lambda e: e.name.lower())
return LocationContents(location_id=code, location_name=loc.name, products=entries)
@router.get("/expiring", response_model=list[ExpiringItem])
def expiring(
days: int | None = None,

View File

@@ -658,6 +658,23 @@ class LocationNeeds(BaseModel):
groups: list[LocationNeedGroup] = []
class LocationContentEntry(BaseModel):
"""Ein Artikel mit seinem Bestand an einem Lagerort (in Artikeleinheiten)."""
product_id: int
name: str
brand: str | None = None
stock: float
unit_label: str = ""
individual: bool = False
class LocationContents(BaseModel):
"""Alle Artikel, die an einem Lagerort liegen inklusive der Unterorte."""
location_id: str
location_name: str
products: list[LocationContentEntry] = []
class MovementOut(BaseModel):
id: int
product_id: int