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:
co-authored by
Claude Opus 5
parent
a182037153
commit
c66bd0b09e
@@ -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,
|
||||
|
||||
@@ -636,6 +636,25 @@ class BulkLotLocation(BaseModel):
|
||||
location_id: str | None = None
|
||||
|
||||
|
||||
class BulkLotCheckout(BaseModel):
|
||||
"""Mehrere Chargen auf einmal ausbuchen.
|
||||
|
||||
``quantity`` leer = jede Charge komplett. Mit Menge wird je Charge
|
||||
dieselbe Menge abgebucht – das ergibt nur Sinn, wenn alle zum selben
|
||||
Artikel gehören, sonst wäre die Einheit mehrdeutig (der Server lehnt es
|
||||
dann ab).
|
||||
"""
|
||||
lot_ids: list[int]
|
||||
quantity: float | None = Field(default=None, gt=0)
|
||||
unit: str | None = None
|
||||
note: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class BulkLotDelete(BaseModel):
|
||||
"""Mehrere Chargen als Korrektur entfernen (ohne sie als Verbrauch zu buchen)."""
|
||||
lot_ids: list[int]
|
||||
|
||||
|
||||
class LotSplit(BaseModel):
|
||||
"""Eine Charge aufteilen: `quantity` (in Basiseinheiten) wird abgezweigt und
|
||||
als neue Charge mit gleichem MHD an `location_id` gelegt; der Rest bleibt."""
|
||||
|
||||
Reference in New Issue
Block a user