Chargen: mehrere auf einmal ausbuchen, teilweise abbuchen oder loeschen

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>
This commit is contained in:
Scarriffle
2026-10-02 12:41:23 +02:00
co-authored by Claude Opus 5
parent a182037153
commit c66bd0b09e
5 changed files with 408 additions and 4 deletions
+87
View File
@@ -8,6 +8,8 @@ from ..models import CategoryTracking, Location, Lot, Movement, MovementType, Pr
from ..schemas import (
BatchCheckInRequest,
BatchCheckInResponse,
BulkLotCheckout,
BulkLotDelete,
BulkLotLocation,
CheckInRequest,
CheckInResponse,
@@ -313,6 +315,91 @@ def bulk_lot_location(
return n
@router.post("/lots/bulk-checkout", response_model=int)
def bulk_lot_checkout(
payload: BulkLotCheckout,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> int:
"""Mehrere Chargen ausbuchen – komplett oder je Charge dieselbe Teilmenge.
Anders als ``bulk-location`` **kein** Sammel-UPDATE: Je Charge muss eine
Bewegung entstehen, sonst fehlte der Vorgang im Verlauf und in der
Verbrauchsstatistik. Schlägt eine Charge fehl (zu wenig Bestand), wird
alles zurückgerollt – ein halb gebuchter Stand wäre schlimmer als gar
keiner; dieselbe Haltung wie beim Stapel-Einlagern.
Gibt die Anzahl der betroffenen Chargen zurück.
"""
if not payload.lot_ids:
return 0
lots = db.query(Lot).filter(Lot.id.in_(payload.lot_ids)).all()
if not lots:
return 0
if payload.quantity is not None:
# Eine gemeinsame Menge setzt eine gemeinsame Einheit voraus: „1 Packung"
# bei Milch und Broetchen sind verschiedene Dinge.
artikel = {lot.product_id for lot in lots}
if len(artikel) > 1:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
"Eine gemeinsame Menge geht nur, wenn alle ausgewählten Chargen "
"zum selben Artikel gehören. Sonst bitte komplett ausbuchen.",
)
betroffen = 0
try:
for lot in lots:
product = db.get(Product, lot.product_id)
if product is None:
continue
menge = payload.quantity if payload.quantity is not None else lot.quantity
einheit = payload.unit if payload.quantity is not None else product.base_unit.value
check_out_lot(db, product, lot, menge, einheit, user, payload.note)
betroffen += 1
except ConversionError as fehler:
db.rollback()
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(fehler)) from fehler
except StockError as fehler:
db.rollback()
raise HTTPException(status.HTTP_409_CONFLICT, str(fehler)) from fehler
db.commit()
return betroffen
@router.post("/lots/bulk-delete", response_model=int)
def bulk_lot_delete(
payload: BulkLotDelete,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
) -> int:
"""Mehrere Chargen als Korrektur entfernen – nicht als Verbrauch.
Je Charge eine Korrektur-Bewegung wie beim einzelnen Löschen, damit der
Bestandsverlauf stimmt. Unbekannte IDs werden still übergangen, wie bei
``bulk-location``.
"""
if not payload.lot_ids:
return 0
lots = db.query(Lot).filter(Lot.id.in_(payload.lot_ids)).all()
for lot in lots:
db.add(
Movement(
product_id=lot.product_id,
lot_id=None,
user_id=user.id,
type=MovementType.adjust,
quantity=-lot.quantity,
unit_used="base",
note="Charge gelöscht",
)
)
db.delete(lot)
db.commit()
return len(lots)
@router.post("/lots/{lot_id}/split", response_model=LotOut)
def split_lot(
lot_id: int,