Gruppen-Gebinde: Mindestbestand/Bestand in Packungen (Backend)

Gruppen bekommen ein eigenes Richt-Gebinde (package_size/label) plus
min_stock_in_packages. Weil die Produkte einer Gruppe unterschiedlich
große Packungen haben können (Pesto 99 g vs. 160 g), legt die Gruppe einen
gemeinsamen Richtwert fest (1 Glas ≈ X g).

- Neuer Helfer group_min_context() zentralisiert, in welcher Einheit der
  Gruppen-Mindestbestand zaehlt (Gebinde ODER verwaltete Einheit).
- Einkaufsliste (gesamt + je Ort), Dashboard-Bedarf und GroupOut nutzen ihn;
  need.text kommt so als 'x Glaeser (y g)'.
- update_group rechnet bestehende Werte beim Umschalten der Einheit um, damit
  der physische Bedarf gleich bleibt.
- Migration: groups.package_size/package_label/min_stock_in_packages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-07 18:38:12 +02:00
parent 28785b0a10
commit ecc1570100
8 changed files with 168 additions and 128 deletions

View File

@@ -15,7 +15,7 @@ from ..schemas import (
LocationMinStockOut,
ProductBarcodeOut,
)
from ..services.conversion import BASE_OF_KIND
from ..services.conversion import group_min_context
from ..services.stock import current_stock, location_subtree_stock_base
router = APIRouter(prefix="/groups", tags=["groups"])
@@ -67,23 +67,21 @@ def _group_to_out(db: Session, group: Group) -> GroupOut:
)
out.product_barcodes = product_codes
# Bestand in derselben Einheit, in der auch der Mindestbestand erfasst ist
# (Gruppen-Gebinde ODER verwaltete Einheit) so ist beides vergleichbar.
ctx = group_min_context(group)
stock_base = float(sum(current_stock(db, p.id) for p in ctx.matching))
out.stock = round(stock_base / ctx.divisor, 3)
unit = group.min_stock_unit
if unit is not None:
# Bestand nur über Produkte gleicher Art, in der Gruppen-Einheit ausgedrückt.
base = BASE_OF_KIND[unit.kind]
matching = [p for p in products if p.base_unit == base]
stock_base = float(sum(current_stock(db, p.id) for p in matching))
out.stock = stock_base / unit.factor
out.min_stock_unit_name = unit.name
out.min_stock_unit_factor = unit.factor
out.kind = unit.kind.value
else:
matching = list(products)
out.stock = float(sum(current_stock(db, p.id) for p in products))
# Bestand je Lagerort (inkl. Unterorte) in derselben Einheit wie out.stock.
def _loc_stock(loc_id: str) -> float:
total = sum(location_subtree_stock_base(db, p, loc_id) for p in matching)
return total / unit.factor if unit is not None else float(total)
total = sum(location_subtree_stock_base(db, p, loc_id) for p in ctx.matching)
return round(total / ctx.divisor, 3)
out.location_min_stocks = [
LocationMinStockOut(
@@ -149,6 +147,9 @@ def create_group(
name=payload.name,
min_stock=payload.min_stock,
min_stock_unit_id=payload.min_stock_unit_id,
package_size=payload.package_size,
package_label=payload.package_label,
min_stock_in_packages=payload.min_stock_in_packages,
)
db.add(group)
db.commit()
@@ -175,8 +176,25 @@ def update_group(
)
if clash:
raise HTTPException(status.HTTP_409_CONFLICT, "Name bereits vergeben")
# Wird nur die Erfassungseinheit umgestellt (Gebinde <-> verwaltete Einheit),
# ohne dass der Aufrufer neue Zahlen mitschickt, rechnen wir die vorhandenen
# Mindestbestände so um, dass der *physische* Bedarf gleich bleibt.
alt = group_min_context(group)
umschaltung = (
any(k in data for k in ("min_stock_in_packages", "package_size", "package_label"))
and "min_stock" not in data
)
for field, value in data.items():
setattr(group, field, value)
if umschaltung:
neu = group_min_context(group)
if neu.divisor != alt.divisor and neu.divisor:
faktor = alt.divisor / neu.divisor
if group.min_stock is not None:
group.min_stock = round(group.min_stock * faktor, 3)
for e in group.location_min_stocks:
e.min_stock = round(e.min_stock * faktor, 3)
db.commit()
db.refresh(group)
return _group_to_out(db, group)