Gruppen: Obergruppen + Mindestbestaende nur noch je Lagerort (Backend)
Zwei zusammenhaengende Umbauten, weil sie dieselben Stellen betreffen.
Obergruppen: Gruppen bilden jetzt einen gerichteten azyklischen Graphen statt
einer flachen Liste. Eine Gruppe darf unter MEHREREN Obergruppen haengen -
"Grillwurst" unter "Wurst" UND unter "Grillgut"; mit einem einzelnen parent_id
waere genau das nicht abbildbar. Bestand und Mindestbestand einer Gruppe zaehlen
den gesamten Untergraphen, wobei eine ueber zwei Wege erreichbare Untergruppe
nur einmal zaehlt (services/gruppen.py arbeitet durchgaengig mit Mengen).
Product.group_id bleibt unveraendert - ein Artikel haengt weiter an genau einer
Gruppe.
Mindestbestaende: der separate Gesamt-Mindestbestand entfaellt. Er wird zur
Zeile mit location_id NULL ("Ueberall") und ist damit die Wurzel ueber allen
Lagerorten - dieselbe Verrechnung wie bei verschachtelten Orten greift jetzt
auch zwischen Ueberall und Kueche, wodurch derselbe Artikel nicht mehr doppelt
in der Einkaufsliste steht. Alle Werte liegen einheitlich in Basiseinheiten
statt in drei verschiedenen Einheiten nebeneinander; das Umrechnen beim
Umschalten der Erfassungseinheit entfaellt dadurch ersatzlos.
_netted_topups nimmt die Hierarchie jetzt als Parameter und faltet damit
Lagerort-Baum und Gruppen-Graph. Verrechnet wird zwischen zwei Gruppen nur,
wenn die zaehlenden Artikel der Untergruppe eine Teilmenge der Obergruppe sind -
zaehlt die Obergruppe in Kilogramm und die Untergruppe in Stueck, kommt ein Kauf
dort oben nicht an.
Die vierfach kopierte Bestandssumme wandert in Sammelabfragen
(summe_bestand_base), sonst vervielfacht der transitive Teilgraph die Abfragen.
Einmalige Datenwanderung beim Start (Merker in den Einstellungen), 18 neue
Tests - darunter Doppelzaehlung ueber zwei Wege, Ringschutz und die bewusst
offene Grenze bei zwei Obergruppen mit gemeinsamer Untergruppe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
236
backend/tests/test_gruppen_hierarchie.py
Normal file
236
backend/tests/test_gruppen_hierarchie.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""Ober- und Untergruppen: Bestand, Verrechnung und Ringschutz.
|
||||
|
||||
Gruppen bilden einen gerichteten azyklischen Graphen, keinen Baum: „Grillwurst"
|
||||
haengt unter „Wurst" UND unter „Grillgut". Der wichtigste Test hier ist deshalb
|
||||
``test_zwei_wege_zaehlen_nur_einmal`` – ein Baum-Walk statt einer Mengenbildung
|
||||
wuerde den Bestand einer ueber zwei Wege erreichbaren Untergruppe verdoppeln.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.models import (
|
||||
BaseUnit,
|
||||
Group,
|
||||
GroupLocationMinStock,
|
||||
Location,
|
||||
Lot,
|
||||
Product,
|
||||
Role,
|
||||
Unit,
|
||||
User,
|
||||
)
|
||||
from app.routers.groups import _group_to_out, create_group, delete_group, update_group
|
||||
from app.routers.views import group_shopping_list, shopping_list_by_location
|
||||
from app.schemas import GroupCreate, GroupUpdate
|
||||
from app.services.min_stock import schreibe_ueberall
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user(db):
|
||||
person = User(username="tester", password_hash="x", role=Role.admin)
|
||||
db.add(person)
|
||||
db.commit()
|
||||
db.refresh(person)
|
||||
return person
|
||||
|
||||
|
||||
def _gramm(db) -> Unit:
|
||||
return db.query(Unit).filter(Unit.name == "Gramm").one()
|
||||
|
||||
|
||||
def _gruppe(db, name: str, *, eltern: list[Group] | None = None) -> Group:
|
||||
g = Group(name=name, min_stock_unit=_gramm(db))
|
||||
if eltern:
|
||||
g.parents = list(eltern)
|
||||
db.add(g)
|
||||
db.commit()
|
||||
db.refresh(g)
|
||||
return g
|
||||
|
||||
|
||||
def _artikel(db, name: str, gruppe: Group, bestand: float = 0.0, ort=None) -> Product:
|
||||
p = Product(name=name, base_unit=BaseUnit.gram, group=gruppe)
|
||||
db.add(p)
|
||||
db.flush()
|
||||
if bestand:
|
||||
db.add(Lot(product_id=p.id, quantity=bestand,
|
||||
location_id=ort.id if ort is not None else None))
|
||||
db.commit()
|
||||
return p
|
||||
|
||||
|
||||
def _ueberall(db, besitzer, menge_base: float) -> None:
|
||||
schreibe_ueberall(db, besitzer, menge_base)
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_bestand_zaehlt_untergruppen_transitiv(db, user):
|
||||
"""Bestand nur im Blatt – die Obergruppe zwei Ebenen darueber sieht ihn."""
|
||||
wurst = _gruppe(db, "Wurst")
|
||||
grillwurst = _gruppe(db, "Grillwurst", eltern=[wurst])
|
||||
bratwurst = _gruppe(db, "Bratwurst", eltern=[grillwurst])
|
||||
_artikel(db, "Bell Bratwurst", bratwurst, bestand=800)
|
||||
|
||||
assert _group_to_out(db, bratwurst).stock == pytest.approx(800)
|
||||
assert _group_to_out(db, grillwurst).stock == pytest.approx(800)
|
||||
assert _group_to_out(db, wurst).stock == pytest.approx(800)
|
||||
# product_count zaehlt transitiv, direct_product_count nur die eigenen.
|
||||
oben = _group_to_out(db, wurst)
|
||||
assert oben.product_count == 1
|
||||
assert oben.direct_product_count == 0
|
||||
|
||||
|
||||
def test_zwei_wege_zaehlen_nur_einmal(db, user):
|
||||
"""Grillwurst haengt unter Wurst UND Grillgut, beide unter Fleisch.
|
||||
|
||||
„Fleisch" erreicht Grillwurst ueber zwei Wege – der Bestand darf trotzdem
|
||||
nur einmal zaehlen.
|
||||
"""
|
||||
fleisch = _gruppe(db, "Fleisch")
|
||||
wurst = _gruppe(db, "Wurst", eltern=[fleisch])
|
||||
grillgut = _gruppe(db, "Grillgut", eltern=[fleisch])
|
||||
grillwurst = _gruppe(db, "Grillwurst", eltern=[wurst, grillgut])
|
||||
_artikel(db, "Bell Grillwurst", grillwurst, bestand=500)
|
||||
|
||||
assert _group_to_out(db, grillwurst).stock == pytest.approx(500)
|
||||
assert _group_to_out(db, wurst).stock == pytest.approx(500)
|
||||
assert _group_to_out(db, grillgut).stock == pytest.approx(500)
|
||||
# Der eigentliche Punkt: NICHT 1000.
|
||||
assert _group_to_out(db, fleisch).stock == pytest.approx(500)
|
||||
assert _group_to_out(db, fleisch).product_count == 1
|
||||
|
||||
|
||||
def test_ring_wird_abgelehnt(db, user):
|
||||
"""Weder sich selbst noch einer eigenen Untergruppe unterordnen."""
|
||||
wurst = _gruppe(db, "Wurst")
|
||||
grillwurst = _gruppe(db, "Grillwurst", eltern=[wurst])
|
||||
|
||||
with pytest.raises(HTTPException) as fehler:
|
||||
update_group(group_id=wurst.id, payload=GroupUpdate(parent_ids=[grillwurst.id]),
|
||||
db=db, _=user)
|
||||
assert fehler.value.status_code == 409
|
||||
|
||||
with pytest.raises(HTTPException):
|
||||
update_group(group_id=wurst.id, payload=GroupUpdate(parent_ids=[wurst.id]),
|
||||
db=db, _=user)
|
||||
|
||||
|
||||
def test_obergruppe_beim_anlegen_setzen(db, user):
|
||||
"""parent_ids wirkt auch schon beim Anlegen (braucht die frische ID)."""
|
||||
wurst = _gruppe(db, "Wurst")
|
||||
out = create_group(
|
||||
payload=GroupCreate(name="Salami", parent_ids=[wurst.id]), db=db, _=user
|
||||
)
|
||||
assert out.parent_ids == [wurst.id]
|
||||
assert _group_to_out(db, wurst).child_ids == [out.id]
|
||||
|
||||
|
||||
def test_bedarf_ober_und_untergruppe_wird_verrechnet(db, user):
|
||||
"""Wurst 5 kg, Grillwurst 2 kg, 1 kg vorhanden.
|
||||
|
||||
Fuer Grillwurst fehlt 1 kg. Das deckt bei Wurst mit, dort fehlen also
|
||||
5 - (1 vorhanden + 1 gekauft) = 3 kg – nicht 4.
|
||||
"""
|
||||
wurst = _gruppe(db, "Wurst")
|
||||
grillwurst = _gruppe(db, "Grillwurst", eltern=[wurst])
|
||||
_artikel(db, "Bell Grillwurst", grillwurst, bestand=1000)
|
||||
_ueberall(db, wurst, 5000)
|
||||
_ueberall(db, grillwurst, 2000)
|
||||
|
||||
nach_name = {i.name: i for i in group_shopping_list(db=db, _=user)}
|
||||
assert nach_name["Grillwurst"].deficit == pytest.approx(1000)
|
||||
assert nach_name["Wurst"].deficit == pytest.approx(3000)
|
||||
assert nach_name["Wurst"].subgroup_count == 1
|
||||
|
||||
|
||||
def test_untergruppen_kauf_deckt_obergruppe_ganz(db, user):
|
||||
"""Deckt der Kauf fuer die Untergruppe alles, verschwindet die Obergruppe."""
|
||||
wurst = _gruppe(db, "Wurst")
|
||||
grillwurst = _gruppe(db, "Grillwurst", eltern=[wurst])
|
||||
_artikel(db, "Bell Grillwurst", grillwurst)
|
||||
_ueberall(db, wurst, 2000)
|
||||
_ueberall(db, grillwurst, 2000)
|
||||
|
||||
namen = {i.name for i in group_shopping_list(db=db, _=user)}
|
||||
assert namen == {"Grillwurst"}
|
||||
|
||||
|
||||
def test_ueberlappung_bleibt_stehen(db, user):
|
||||
"""Bekannte Grenze, absichtlich festgehalten.
|
||||
|
||||
„Wurst" und „Grillgut" sind keine Vorfahren voneinander, teilen sich aber
|
||||
„Grillwurst". Ohne eigenen Mindestbestand auf der gemeinsamen Untergruppe
|
||||
gibt es keinen Schluessel, ueber den verrechnet werden koennte – beide
|
||||
Bedarfe stehen einzeln da, obwohl ein Kauf beide decken wuerde.
|
||||
"""
|
||||
wurst = _gruppe(db, "Wurst")
|
||||
grillgut = _gruppe(db, "Grillgut")
|
||||
grillwurst = _gruppe(db, "Grillwurst", eltern=[wurst, grillgut])
|
||||
_artikel(db, "Bell Grillwurst", grillwurst)
|
||||
_ueberall(db, wurst, 2000)
|
||||
_ueberall(db, grillgut, 2000)
|
||||
|
||||
nach_name = {i.name: i for i in group_shopping_list(db=db, _=user)}
|
||||
assert nach_name["Wurst"].deficit == pytest.approx(2000)
|
||||
assert nach_name["Grillgut"].deficit == pytest.approx(2000)
|
||||
|
||||
|
||||
def test_einheitenfilter_gilt_auch_fuer_untergruppen(db, user):
|
||||
"""Eine Obergruppe in Gramm zaehlt stueckweise Artikel der Untergruppe nicht."""
|
||||
stueck = db.query(Unit).filter(Unit.name == "Stück").one()
|
||||
wurst = _gruppe(db, "Wurst") # zaehlt in Gramm
|
||||
dosen = Group(name="Wurstdosen", min_stock_unit=stueck, parents=[wurst])
|
||||
db.add(dosen)
|
||||
db.commit()
|
||||
_artikel(db, "Wurst im Glas", wurst, bestand=500)
|
||||
p = Product(name="Wurstdose", base_unit=BaseUnit.piece, group=dosen)
|
||||
db.add(p)
|
||||
db.flush()
|
||||
db.add(Lot(product_id=p.id, quantity=4))
|
||||
db.commit()
|
||||
|
||||
# Die 4 Stueck zaehlen bei „Wurst" (Gramm) nicht mit.
|
||||
assert _group_to_out(db, wurst).stock == pytest.approx(500)
|
||||
assert _group_to_out(db, dosen).stock == pytest.approx(4)
|
||||
|
||||
|
||||
def test_gruppenbedarf_je_ort_verrechnet_beide_hierarchien(db, user):
|
||||
"""Kauf fuer die Untergruppe im Unterort deckt die Obergruppe im Oberort."""
|
||||
haus = Location(name="Haus")
|
||||
db.add(haus)
|
||||
db.flush()
|
||||
kueche = Location(name="Küche", parent_id=haus.id)
|
||||
db.add(kueche)
|
||||
db.commit()
|
||||
|
||||
wurst = _gruppe(db, "Wurst")
|
||||
grillwurst = _gruppe(db, "Grillwurst", eltern=[wurst])
|
||||
_artikel(db, "Bell Grillwurst", grillwurst, bestand=1000, ort=kueche)
|
||||
db.add_all([
|
||||
GroupLocationMinStock(group_id=wurst.id, location_id=haus.id, min_stock=5000),
|
||||
GroupLocationMinStock(group_id=grillwurst.id, location_id=kueche.id, min_stock=2000),
|
||||
])
|
||||
db.commit()
|
||||
|
||||
nach_ort = {n.location_name: n for n in shopping_list_by_location(db=db, _=user)}
|
||||
kueche_bedarf = {g.name: g for g in nach_ort["Küche"].groups}
|
||||
haus_bedarf = {g.name: g for g in nach_ort["Haus"].groups}
|
||||
assert kueche_bedarf["Grillwurst"].deficit == pytest.approx(1000)
|
||||
# 5000 - (1000 vorhanden + 1000 fuer die Kueche gekauft)
|
||||
assert haus_bedarf["Wurst"].deficit == pytest.approx(3000)
|
||||
|
||||
|
||||
def test_loeschen_einer_gruppe_mit_kindern(db, user):
|
||||
"""Untergruppen bleiben bestehen und verlieren nur die Verbindung."""
|
||||
wurst = _gruppe(db, "Wurst")
|
||||
grillwurst = _gruppe(db, "Grillwurst", eltern=[wurst])
|
||||
p = _artikel(db, "Bell Grillwurst", grillwurst, bestand=500)
|
||||
|
||||
delete_group(group_id=wurst.id, db=db, _=user)
|
||||
|
||||
db.refresh(grillwurst)
|
||||
assert db.get(Group, wurst.id) is None
|
||||
assert _group_to_out(db, grillwurst).parent_ids == []
|
||||
db.refresh(p)
|
||||
assert p.group_id == grillwurst.id
|
||||
192
backend/tests/test_min_stock_ueberall.py
Normal file
192
backend/tests/test_min_stock_ueberall.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""Mindestbestände hängen nur noch an Lagerorten – „Überall" ist Ort NULL.
|
||||
|
||||
„Überall" (egal wo, Hauptsache im Haus) ersetzt den früheren Gesamt-Mindest-
|
||||
bestand und liegt als Wurzel über allen Lagerorten. Dadurch verrechnet dieselbe
|
||||
Faltung wie bei verschachtelten Orten auch Überall gegen Küche & Co. – ein
|
||||
Artikel steht nicht mehr doppelt in der Einkaufsliste.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.models import (
|
||||
BaseUnit,
|
||||
Group,
|
||||
GroupLocationMinStock,
|
||||
Location,
|
||||
Lot,
|
||||
Product,
|
||||
ProductLocationMinStock,
|
||||
Role,
|
||||
Setting,
|
||||
Unit,
|
||||
User,
|
||||
)
|
||||
from app.crud import product_to_out
|
||||
from app.routers.views import shopping_list, shopping_list_by_location
|
||||
from app.services.min_stock import (
|
||||
MIGRATION_KEY,
|
||||
UEBERALL_NAME,
|
||||
lies_ueberall,
|
||||
migriere_mindestbestaende,
|
||||
schreibe_ueberall,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user(db):
|
||||
person = User(username="tester", password_hash="x", role=Role.admin)
|
||||
db.add(person)
|
||||
db.commit()
|
||||
db.refresh(person)
|
||||
return person
|
||||
|
||||
|
||||
def _artikel(db, name="Gurken", **kwargs) -> Product:
|
||||
# package_size=1 -> Artikeleinheit == Basiseinheit, macht die Rechnung klar.
|
||||
p = Product(name=name, base_unit=BaseUnit.gram, package_size=1, **kwargs)
|
||||
db.add(p)
|
||||
db.commit()
|
||||
db.refresh(p)
|
||||
return p
|
||||
|
||||
|
||||
def test_ueberall_zeile_wird_gelesen(db, user):
|
||||
p = _artikel(db)
|
||||
schreibe_ueberall(db, p, 500)
|
||||
db.commit()
|
||||
|
||||
out = product_to_out(db, p)
|
||||
assert out.min_stock == pytest.approx(500)
|
||||
(zeile,) = out.location_min_stocks
|
||||
assert zeile.location_id is None
|
||||
assert zeile.location_name == UEBERALL_NAME
|
||||
assert lies_ueberall(p.location_min_stocks) == pytest.approx(500)
|
||||
|
||||
|
||||
def test_ueberall_auf_null_setzen_entfernt_die_zeile(db, user):
|
||||
p = _artikel(db)
|
||||
schreibe_ueberall(db, p, 500)
|
||||
db.commit()
|
||||
schreibe_ueberall(db, p, None)
|
||||
db.commit()
|
||||
|
||||
assert p.location_min_stocks == []
|
||||
assert product_to_out(db, p).min_stock is None
|
||||
|
||||
|
||||
def test_zwei_ueberall_zeilen_werden_abgelehnt(db, user):
|
||||
"""Der Teilindex ersetzt den UNIQUE-Schutz, den NULLs nicht bekommen."""
|
||||
p = _artikel(db)
|
||||
db.add(ProductLocationMinStock(product_id=p.id, location_id=None, min_stock=500))
|
||||
db.commit()
|
||||
db.add(ProductLocationMinStock(product_id=p.id, location_id=None, min_stock=900))
|
||||
with pytest.raises(IntegrityError):
|
||||
db.commit()
|
||||
db.rollback()
|
||||
|
||||
|
||||
def test_ueberall_wird_gegen_lagerort_verrechnet(db, user):
|
||||
"""Überall 5, Küche 2, je 1 vorhanden -> nicht 6 kaufen.
|
||||
|
||||
Für die Küche fehlt 1. Das liegt danach auch im Haus und deckt Überall mit:
|
||||
dort fehlen 5 - (1 vorhanden + 1 gekauft) = 3.
|
||||
"""
|
||||
p = _artikel(db, "Gurken")
|
||||
kueche = Location(name="Küche")
|
||||
db.add(kueche)
|
||||
db.flush()
|
||||
db.add(Lot(product_id=p.id, quantity=1, location_id=kueche.id))
|
||||
db.add_all([
|
||||
ProductLocationMinStock(product_id=p.id, location_id=None, min_stock=5),
|
||||
ProductLocationMinStock(product_id=p.id, location_id=kueche.id, min_stock=2),
|
||||
])
|
||||
db.commit()
|
||||
|
||||
(gesamt,) = shopping_list(db=db, _=user)
|
||||
assert gesamt.deficit == pytest.approx(3)
|
||||
|
||||
nach_ort = {n.location_name: n for n in shopping_list_by_location(db=db, _=user)}
|
||||
assert nach_ort["Küche"].products[0].deficit == pytest.approx(1)
|
||||
# „Überall" gehört nicht in die Ortsliste.
|
||||
assert UEBERALL_NAME not in nach_ort
|
||||
|
||||
|
||||
def test_ueberall_zaehlt_chargen_ohne_ort_mit(db, user):
|
||||
"""Eine Charge ohne Lagerort liegt in keinem Ort-Subtree, aber im Haus."""
|
||||
p = _artikel(db, "Butter")
|
||||
db.add(Lot(product_id=p.id, quantity=4, location_id=None))
|
||||
db.add(ProductLocationMinStock(product_id=p.id, location_id=None, min_stock=5))
|
||||
db.commit()
|
||||
|
||||
(gesamt,) = shopping_list(db=db, _=user)
|
||||
assert gesamt.stock == pytest.approx(4)
|
||||
assert gesamt.deficit == pytest.approx(1)
|
||||
|
||||
|
||||
# ---- Datenwanderung ----
|
||||
|
||||
def _gramm(db) -> Unit:
|
||||
return db.query(Unit).filter(Unit.name == "Gramm").one()
|
||||
|
||||
|
||||
def test_migration_macht_gesamtwerte_zu_ueberall(db, user):
|
||||
"""Alter Stand: min_stock am Artikel, Ort-Werte in Artikeleinheiten."""
|
||||
p = Product(name="Pesto", base_unit=BaseUnit.gram, package_size=150,
|
||||
package_label="Glas", min_stock=600)
|
||||
db.add(p)
|
||||
db.flush()
|
||||
keller = Location(name="Keller")
|
||||
db.add(keller)
|
||||
db.flush()
|
||||
# 2 Gläser à 150 g – frueher so gespeichert (Artikeleinheiten).
|
||||
db.add(ProductLocationMinStock(product_id=p.id, location_id=keller.id, min_stock=2))
|
||||
db.commit()
|
||||
|
||||
migriere_mindestbestaende(db)
|
||||
db.refresh(p)
|
||||
|
||||
assert p.min_stock is None # Quelle geleert
|
||||
werte = {e.location_id: e.min_stock for e in p.location_min_stocks}
|
||||
assert werte[None] == pytest.approx(600) # war schon Basiseinheiten
|
||||
assert werte[keller.id] == pytest.approx(300) # 2 Gläser * 150 g
|
||||
|
||||
|
||||
def test_migration_rechnet_gruppenwerte_ueber_den_divisor(db, user):
|
||||
gruppe = Group(name="Pesto", min_stock=3, min_stock_unit=_gramm(db),
|
||||
package_size=150, package_label="Glas", min_stock_in_packages=True)
|
||||
db.add(gruppe)
|
||||
db.flush()
|
||||
keller = Location(name="Keller")
|
||||
db.add(keller)
|
||||
db.flush()
|
||||
db.add(GroupLocationMinStock(group_id=gruppe.id, location_id=keller.id, min_stock=2))
|
||||
db.commit()
|
||||
|
||||
migriere_mindestbestaende(db)
|
||||
db.refresh(gruppe)
|
||||
|
||||
assert gruppe.min_stock is None
|
||||
werte = {e.location_id: e.min_stock for e in gruppe.location_min_stocks}
|
||||
assert werte[None] == pytest.approx(450) # 3 Gläser * 150 g
|
||||
assert werte[keller.id] == pytest.approx(300) # 2 Gläser * 150 g
|
||||
|
||||
|
||||
def test_migration_laeuft_nur_einmal(db, user):
|
||||
"""Der zweite Lauf darf die Werte nicht erneut hochrechnen."""
|
||||
p = Product(name="Pesto", base_unit=BaseUnit.gram, package_size=150, min_stock=600)
|
||||
db.add(p)
|
||||
db.flush()
|
||||
keller = Location(name="Keller")
|
||||
db.add(keller)
|
||||
db.flush()
|
||||
db.add(ProductLocationMinStock(product_id=p.id, location_id=keller.id, min_stock=2))
|
||||
db.commit()
|
||||
|
||||
migriere_mindestbestaende(db)
|
||||
migriere_mindestbestaende(db)
|
||||
db.refresh(p)
|
||||
|
||||
werte = {e.location_id: e.min_stock for e in p.location_min_stocks}
|
||||
assert werte[keller.id] == pytest.approx(300) # nicht 45000
|
||||
assert db.get(Setting, MIGRATION_KEY) is not None
|
||||
@@ -9,9 +9,10 @@ Packungen haben können).
|
||||
import pytest
|
||||
|
||||
from app.models import BaseUnit, Group, PackageType, Product, Role, Unit, User
|
||||
from app.routers.groups import update_group
|
||||
from app.routers.groups import _group_to_out, update_group
|
||||
from app.routers.views import group_shopping_list, shopping_list
|
||||
from app.schemas import GroupUpdate
|
||||
from app.services.min_stock import schreibe_ueberall
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -27,12 +28,20 @@ def _gramm(db) -> Unit:
|
||||
return db.query(Unit).filter(Unit.name == "Gramm").one()
|
||||
|
||||
|
||||
def _ueberall(db, besitzer, menge_base: float) -> None:
|
||||
"""Mindestbestand „Überall" setzen – Mengen immer in Basiseinheiten."""
|
||||
schreibe_ueberall(db, besitzer, menge_base)
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_produkt_bedarf_in_ganzen_gebinden(db, user):
|
||||
"""500 g fehlen bei 195-g-Gläsern -> 3 Gläser (aufgerundet), 500 g als Hinweis."""
|
||||
db.add(PackageType(singular="Glas", plural="Gläser"))
|
||||
db.add(Product(name="Pesto", base_unit=BaseUnit.gram, package_size=195,
|
||||
package_label="Glas", min_stock=500))
|
||||
pesto = Product(name="Pesto", base_unit=BaseUnit.gram, package_size=195,
|
||||
package_label="Glas")
|
||||
db.add(pesto)
|
||||
db.commit() # kein Bestand -> es fehlt der volle Mindestbestand
|
||||
_ueberall(db, pesto, 500)
|
||||
|
||||
(item,) = shopping_list(db=db, _=user)
|
||||
assert item.need is not None
|
||||
@@ -45,11 +54,12 @@ def test_produkt_bedarf_in_ganzen_gebinden(db, user):
|
||||
def test_gruppe_mit_gruppen_gebinde(db, user):
|
||||
"""Gruppe mit eigenem Richt-Gebinde (1 Glas ≈ 150 g), Mindestbestand 3 Gläser."""
|
||||
db.add(PackageType(singular="Glas", plural="Gläser"))
|
||||
gruppe = Group(name="Pesto alla Genovese", min_stock=3, min_stock_unit=_gramm(db),
|
||||
gruppe = Group(name="Pesto alla Genovese", min_stock_unit=_gramm(db),
|
||||
package_size=150, package_label="Glas", min_stock_in_packages=True)
|
||||
db.add(gruppe)
|
||||
db.add(Product(name="Pesto Barilla", base_unit=BaseUnit.gram, package_size=99, group=gruppe))
|
||||
db.commit() # kein Bestand -> es fehlen 3 Gläser (450 g)
|
||||
_ueberall(db, gruppe, 3 * 150) # 3 Gläser à 150 g
|
||||
|
||||
(item,) = group_shopping_list(db=db, _=user)
|
||||
assert item.need is not None
|
||||
@@ -61,10 +71,11 @@ def test_gruppe_mit_gruppen_gebinde(db, user):
|
||||
|
||||
def test_gruppe_ohne_gebinde_bleibt_basiseinheit(db, user):
|
||||
"""Ohne aktives Gruppen-Gebinde bleibt es bei der verwalteten Einheit."""
|
||||
gruppe = Group(name="Mehl", min_stock=500, min_stock_unit=_gramm(db))
|
||||
gruppe = Group(name="Mehl", min_stock_unit=_gramm(db))
|
||||
db.add(gruppe)
|
||||
db.add(Product(name="Mehl 405", base_unit=BaseUnit.gram, group=gruppe))
|
||||
db.commit()
|
||||
_ueberall(db, gruppe, 500)
|
||||
|
||||
(item,) = group_shopping_list(db=db, _=user)
|
||||
assert item.need is not None
|
||||
@@ -74,11 +85,17 @@ def test_gruppe_ohne_gebinde_bleibt_basiseinheit(db, user):
|
||||
|
||||
|
||||
def test_gruppe_umschalten_haelt_physischen_bedarf(db, user):
|
||||
"""Umschalten g -> Gebinde rechnet den gespeicherten Wert um (500 g / 150 = 3,33 Glas)."""
|
||||
gruppe = Group(name="Pesto", min_stock=500, min_stock_unit=_gramm(db))
|
||||
"""Umschalten g -> Gebinde laesst den physischen Bedarf unveraendert.
|
||||
|
||||
Gespeichert wird in Basiseinheiten, deshalb bleiben die 500 g stehen und es
|
||||
ist NUR die Anzeige, die auf ~3,33 Glaeser wechselt. Frueher musste der
|
||||
gespeicherte Wert dafuer umgerechnet werden.
|
||||
"""
|
||||
gruppe = Group(name="Pesto", min_stock_unit=_gramm(db))
|
||||
db.add(gruppe)
|
||||
db.add(Product(name="Pesto Barilla", base_unit=BaseUnit.gram, group=gruppe))
|
||||
db.commit()
|
||||
_ueberall(db, gruppe, 500)
|
||||
|
||||
update_group(
|
||||
group_id=gruppe.id,
|
||||
@@ -87,4 +104,7 @@ def test_gruppe_umschalten_haelt_physischen_bedarf(db, user):
|
||||
)
|
||||
db.refresh(gruppe)
|
||||
assert gruppe.min_stock_in_packages is True
|
||||
assert gruppe.min_stock == pytest.approx(500 / 150, abs=0.01) # ~3,33 Gläser
|
||||
# Gespeichert unveraendert in Basiseinheiten …
|
||||
assert gruppe.location_min_stocks[0].min_stock == pytest.approx(500)
|
||||
# … angezeigt jetzt in Glaesern.
|
||||
assert _group_to_out(db, gruppe).min_stock == pytest.approx(500 / 150, abs=0.01)
|
||||
|
||||
Reference in New Issue
Block a user