Backend: gleiche Chargen (Artikel+MHD+Ort) werden zusammengefasst

Nach dem Umlagern/Aufteilen entstanden Dubletten: zwei Chargen desselben Artikels
mit gleichem MHD am gleichen Lagerort, obwohl das fuer den Nutzer eine Charge ist.
Neu: consolidate_lot_group fasst solche Chargen zusammen (Mengen addieren,
Bewegungen auf die aeltere Charge umhaengen, Rest loeschen) - aufgerufen nach
split, bulk-location und update_lot. Bestand und Historie bleiben unveraendert.
Beim Start raeumt consolidate_duplicate_lots einmalig bestehende Dubletten auf.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-30 08:53:12 +02:00
parent 231d86895f
commit 05b54dc7ac
3 changed files with 108 additions and 5 deletions

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import date
from sqlalchemy import asc
from sqlalchemy import asc, func
from sqlalchemy.orm import Session
from ..models import (
@@ -302,6 +302,83 @@ def check_in(
return lot
def consolidate_lot_group(
db: Session,
product_id: int,
best_before: date | None,
best_before_precision: str,
location_id: str | None,
) -> Lot | None:
"""Chargen mit gleichem Artikel, MHD, Genauigkeit und Lagerort zu EINER
zusammenfassen: Mengen addieren, Bewegungen auf die verbleibende (älteste)
Charge umhängen, die übrigen löschen. Gibt die verbleibende Charge zurück.
Wird nach jedem Umlagern/Aufteilen aufgerufen zwei gleich gelagerte Chargen
mit demselben MHD sind für den Nutzer dieselbe Charge und sollen als eine
erscheinen. Der Bestand bleibt unverändert, die Historie (Bewegungen) auch.
"""
query = db.query(Lot).filter(
Lot.product_id == product_id,
Lot.best_before_precision == best_before_precision,
)
query = (
query.filter(Lot.best_before.is_(None))
if best_before is None
else query.filter(Lot.best_before == best_before)
)
query = (
query.filter(Lot.location_id.is_(None))
if location_id is None
else query.filter(Lot.location_id == location_id)
)
lots = query.order_by(asc(Lot.created_at), asc(Lot.id)).all()
if len(lots) <= 1:
return lots[0] if lots else None
survivor = lots[0]
for extra in lots[1:]:
survivor.quantity += extra.quantity
db.query(Movement).filter(Movement.lot_id == extra.id).update(
{Movement.lot_id: survivor.id}, synchronize_session=False
)
db.delete(extra)
db.flush()
return survivor
def consolidate_duplicate_lots(db: Session) -> int:
"""Alle bereits vorhandenen Dubletten einmalig zusammenfassen (beim Start).
Idempotent: Nach dem ersten Lauf gibt es keine Gruppen mit mehr als einer
Charge mehr. Gibt die Anzahl entfernter Chargen zurück."""
gruppen = (
db.query(
Lot.product_id, Lot.best_before, Lot.best_before_precision, Lot.location_id
)
.group_by(
Lot.product_id, Lot.best_before, Lot.best_before_precision, Lot.location_id
)
.having(func.count(Lot.id) > 1)
.all()
)
entfernt = 0
for product_id, best_before, precision, location_id in gruppen:
vorher = (
db.query(func.count(Lot.id))
.filter(
Lot.product_id == product_id,
Lot.best_before_precision == precision,
Lot.best_before.is_(None) if best_before is None else Lot.best_before == best_before,
Lot.location_id.is_(None) if location_id is None else Lot.location_id == location_id,
)
.scalar()
)
consolidate_lot_group(db, product_id, best_before, precision, location_id)
entfernt += max(0, (vorher or 0) - 1)
if entfernt:
db.commit()
return entfernt
def check_out_lot(
db: Session,
product: Product,