Die Mehrfachauswahl gab es schon, sie konnte aber nur den Lagerort setzen. Jetzt kommen drei Aktionen dazu: komplett ausbuchen, von jeder Charge dieselbe Teilmenge abbuchen, und loeschen als Korrektur. Anders als bulk-location kein Sammel-UPDATE: Je Charge muss eine Bewegung entstehen, sonst fehlte der Vorgang im Verlauf und in der Verbrauchsstatistik. Schlaegt eine Charge fehl, weil sie zu wenig enthaelt, wird alles zurueckgerollt - ein halb gebuchter Stand waere schlimmer als gar keiner, dieselbe Haltung wie beim Stapel-Einlagern. Die gemeinsame Teilmenge gibt es nur, wenn alle ausgewaehlten Chargen zum selben Artikel gehoeren. "1 Packung" bedeutet bei Milch und Broetchen Verschiedenes; der Server lehnt es ab, und die Oberflaeche sperrt das Feld gleich mit einem Hinweis, statt den Fehler erst nach dem Klick zu zeigen. Loeschen schreibt wie das einzelne Loeschen eine Korrektur-Bewegung (adjust, negative Menge) statt einer Entnahme - es ist eine Fehleingabe, kein Verbrauch, und darf die Verbrauchsstatistik nicht verfaelschen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
200 lines
6.1 KiB
Python
200 lines
6.1 KiB
Python
"""Mehrere Chargen auf einmal: komplett ausbuchen, Teilmenge, löschen."""
|
|
|
|
from datetime import date, timedelta
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from app.models import BaseUnit, Lot, Movement, MovementType, Product, Role, User
|
|
from app.routers.stock import bulk_lot_checkout, bulk_lot_delete
|
|
from app.schemas import BulkLotCheckout, BulkLotDelete
|
|
from app.services.stock import current_stock
|
|
|
|
|
|
@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
|
|
|
|
|
|
@pytest.fixture()
|
|
def milch(db):
|
|
product = Product(
|
|
name="Vollmilch", base_unit=BaseUnit.milliliter, package_size=1000,
|
|
source="manual",
|
|
)
|
|
db.add(product)
|
|
db.commit()
|
|
db.refresh(product)
|
|
return product
|
|
|
|
|
|
def _lot(db, product, menge, tage=None):
|
|
lot = Lot(
|
|
product_id=product.id, quantity=menge,
|
|
best_before=(date.today() + timedelta(days=tage)) if tage is not None else None,
|
|
)
|
|
db.add(lot)
|
|
db.commit()
|
|
db.refresh(lot)
|
|
return lot
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Komplett ausbuchen
|
|
# --------------------------------------------------------------------------
|
|
def test_mehrere_chargen_komplett_ausbuchen(db, user, milch):
|
|
a = _lot(db, milch, 1000, 5)
|
|
b = _lot(db, milch, 2000, 9)
|
|
|
|
anzahl = bulk_lot_checkout(BulkLotCheckout(lot_ids=[a.id, b.id]), db=db, user=user)
|
|
|
|
assert anzahl == 2
|
|
assert db.query(Lot).count() == 0
|
|
assert current_stock(db, milch.id) == 0
|
|
|
|
|
|
def test_je_charge_eine_bewegung(db, user, milch):
|
|
a = _lot(db, milch, 1000, 5)
|
|
b = _lot(db, milch, 2000, 9)
|
|
|
|
bulk_lot_checkout(BulkLotCheckout(lot_ids=[a.id, b.id]), db=db, user=user)
|
|
|
|
bewegungen = db.query(Movement).all()
|
|
assert len(bewegungen) == 2
|
|
assert {m.quantity for m in bewegungen} == {1000, 2000}
|
|
assert all(m.type == MovementType.out for m in bewegungen)
|
|
|
|
|
|
def test_notiz_landet_an_der_bewegung(db, user, milch):
|
|
a = _lot(db, milch, 1000, 5)
|
|
|
|
bulk_lot_checkout(
|
|
BulkLotCheckout(lot_ids=[a.id], note="Grossputz"), db=db, user=user
|
|
)
|
|
|
|
assert db.query(Movement).one().note == "Grossputz"
|
|
|
|
|
|
def test_leere_auswahl_tut_nichts(db, user, milch):
|
|
_lot(db, milch, 1000, 5)
|
|
assert bulk_lot_checkout(BulkLotCheckout(lot_ids=[]), db=db, user=user) == 0
|
|
assert current_stock(db, milch.id) == 1000
|
|
|
|
|
|
def test_unbekannte_ids_werden_uebergangen(db, user, milch):
|
|
a = _lot(db, milch, 1000, 5)
|
|
|
|
anzahl = bulk_lot_checkout(BulkLotCheckout(lot_ids=[a.id, 999]), db=db, user=user)
|
|
|
|
assert anzahl == 1
|
|
assert current_stock(db, milch.id) == 0
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Teilmenge je Charge
|
|
# --------------------------------------------------------------------------
|
|
def test_teilmenge_je_charge(db, user, milch):
|
|
a = _lot(db, milch, 1000, 5)
|
|
b = _lot(db, milch, 2000, 9)
|
|
|
|
bulk_lot_checkout(
|
|
BulkLotCheckout(lot_ids=[a.id, b.id], quantity=200, unit="ml"), db=db, user=user
|
|
)
|
|
|
|
assert db.get(Lot, a.id).quantity == 800
|
|
assert db.get(Lot, b.id).quantity == 1800
|
|
|
|
|
|
def test_teilmenge_bei_gemischten_artikeln_wird_abgelehnt(db, user, milch):
|
|
"""„1 Packung" bei Milch und Brötchen sind verschiedene Dinge."""
|
|
broetchen = Product(name="Brötchen", base_unit=BaseUnit.piece, package_size=6,
|
|
source="manual")
|
|
db.add(broetchen)
|
|
db.commit()
|
|
a = _lot(db, milch, 1000, 5)
|
|
b = _lot(db, broetchen, 6, 2)
|
|
|
|
with pytest.raises(HTTPException) as fehler:
|
|
bulk_lot_checkout(
|
|
BulkLotCheckout(lot_ids=[a.id, b.id], quantity=1, unit="package"),
|
|
db=db, user=user,
|
|
)
|
|
|
|
assert fehler.value.status_code == 400
|
|
assert db.get(Lot, a.id).quantity == 1000 # nichts gebucht
|
|
assert db.get(Lot, b.id).quantity == 6
|
|
|
|
|
|
def test_zu_wenig_bestand_rollt_alles_zurueck(db, user, milch):
|
|
"""Ein halb gebuchter Stand wäre schlimmer als gar keiner."""
|
|
viel = _lot(db, milch, 1000, 5)
|
|
wenig = _lot(db, milch, 100, 9)
|
|
|
|
with pytest.raises(HTTPException) as fehler:
|
|
bulk_lot_checkout(
|
|
BulkLotCheckout(lot_ids=[viel.id, wenig.id], quantity=500, unit="ml"),
|
|
db=db, user=user,
|
|
)
|
|
|
|
assert fehler.value.status_code == 409
|
|
assert current_stock(db, milch.id) == 1100 # unverändert
|
|
assert db.query(Movement).count() == 0
|
|
|
|
|
|
def test_unbekannte_einheit_bucht_nichts(db, user, milch):
|
|
a = _lot(db, milch, 1000, 5)
|
|
|
|
with pytest.raises(HTTPException) as fehler:
|
|
bulk_lot_checkout(
|
|
BulkLotCheckout(lot_ids=[a.id], quantity=1, unit="furlong"), db=db, user=user
|
|
)
|
|
|
|
assert fehler.value.status_code == 400
|
|
assert current_stock(db, milch.id) == 1000
|
|
|
|
|
|
def test_teilmenge_leert_und_entfernt_die_charge(db, user, milch):
|
|
a = _lot(db, milch, 500, 5)
|
|
|
|
bulk_lot_checkout(
|
|
BulkLotCheckout(lot_ids=[a.id], quantity=500, unit="ml"), db=db, user=user
|
|
)
|
|
|
|
assert db.get(Lot, a.id) is None
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Löschen als Korrektur
|
|
# --------------------------------------------------------------------------
|
|
def test_mehrere_chargen_loeschen(db, user, milch):
|
|
a = _lot(db, milch, 1000, 5)
|
|
b = _lot(db, milch, 2000, 9)
|
|
|
|
anzahl = bulk_lot_delete(BulkLotDelete(lot_ids=[a.id, b.id]), db=db, user=user)
|
|
|
|
assert anzahl == 2
|
|
assert db.query(Lot).count() == 0
|
|
|
|
|
|
def test_loeschen_schreibt_korrektur_nicht_verbrauch(db, user, milch):
|
|
a = _lot(db, milch, 1000, 5)
|
|
|
|
bulk_lot_delete(BulkLotDelete(lot_ids=[a.id]), db=db, user=user)
|
|
|
|
bewegung = db.query(Movement).one()
|
|
assert bewegung.type == MovementType.adjust
|
|
assert bewegung.quantity == -1000
|
|
assert bewegung.note == "Charge gelöscht"
|
|
# Keine Charge mehr, an der die Bewegung haengen koennte.
|
|
assert bewegung.lot_id is None
|
|
|
|
|
|
def test_loeschen_mit_leerer_auswahl(db, user, milch):
|
|
_lot(db, milch, 1000, 5)
|
|
assert bulk_lot_delete(BulkLotDelete(lot_ids=[]), db=db, user=user) == 0
|
|
assert current_stock(db, milch.id) == 1000
|