Verwaltbare Einheiten mit Umrechnung + Chargen bearbeiten + gezieltes Auslagern
Einheiten (neu):
- Tabelle units (name, kind=count|weight|volume, factor, is_builtin); eingebaut
Stueck/Gramm/Kilogramm/Milliliter/Liter, Admin kann eigene anlegen (z.B. Pfund=500g).
- Bestaende bleiben intern in kanonischer Basis (Stueck/Gramm/Milliliter);
Product.display_unit_id und Group.min_stock_unit_id als nullable FKs.
- Neuer Umrechnungs-Service (services/conversion.py) ersetzt die feste Einheitenlogik;
Ein-/Auslagern und Gruppen-Mindestbestand rechnen ueber den Faktor.
- Gruppen-Mindestbestand mit Einheit; Gruppenbestand summiert nur Produkte
passender Art. Neue Verwaltungsseite "Einheiten" (Admin).
- Schonende Migration beim Start: ADD COLUMN IF NOT EXISTS (Postgres), damit
bestehende Installationen ihre Daten behalten.
Chargen:
- PATCH /lots/{id} und DELETE /lots/{id}: Menge/MHD korrigieren, Charge loeschen
(wird als Korrektur-Bewegung protokolliert). Bearbeitung im Produktdetail.
Auslagern:
- Optionales lot_id: gezielt aus einer bestimmten Charge/MHD abbuchen statt FEFO;
Auswahl-Dropdown in der Auslagern-Seite.
Sonstiges: Roadmap aktualisiert, Tests fuer die Umrechnung.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import enum
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Date,
|
||||
DateTime,
|
||||
Enum,
|
||||
@@ -40,6 +41,28 @@ class MovementType(str, enum.Enum):
|
||||
adjust = "adjust"
|
||||
|
||||
|
||||
class UnitKind(str, enum.Enum):
|
||||
"""Art einer Einheit. Bestimmt die kanonische Basiseinheit für die Speicherung."""
|
||||
count = "count" # Basis: Stück
|
||||
weight = "weight" # Basis: Gramm
|
||||
volume = "volume" # Basis: Milliliter
|
||||
|
||||
|
||||
class Unit(Base):
|
||||
"""Vom Admin verwaltbare Einheit mit Umrechnungsfaktor zur kanonischen Basis.
|
||||
|
||||
factor = wie viele Basiseinheiten 1 dieser Einheit entsprechen
|
||||
(z.B. Kilogramm: kind=weight, factor=1000; Liter: kind=volume, factor=1000).
|
||||
"""
|
||||
__tablename__ = "units"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
||||
kind: Mapped[UnitKind] = mapped_column(Enum(UnitKind), nullable=False)
|
||||
factor: Mapped[float] = mapped_column(Float, nullable=False, default=1.0)
|
||||
is_builtin: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
@@ -55,10 +78,14 @@ class Group(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
|
||||
# Group-level minimum stock is a Schritt-3 feature; column kept for forward-compat.
|
||||
min_stock: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
# Einheit des Gruppen-Mindestbestands (z.B. Kilogramm). NULL = zählt in Basiseinheiten.
|
||||
min_stock_unit_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("units.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
products: Mapped[list[Product]] = relationship(back_populates="group")
|
||||
min_stock_unit: Mapped[Unit | None] = relationship()
|
||||
|
||||
|
||||
class Location(Base):
|
||||
@@ -82,9 +109,14 @@ class Product(Base):
|
||||
brand: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
image_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
|
||||
# Kanonische Basiseinheit (piece/gram/milliliter): bestimmt Speicherung + Art (kind).
|
||||
base_unit: Mapped[BaseUnit] = mapped_column(
|
||||
Enum(BaseUnit), nullable=False, default=BaseUnit.piece
|
||||
)
|
||||
# Bevorzugte Anzeige-/Eingabeeinheit (z.B. Kilogramm). NULL = Basiseinheit selbst.
|
||||
display_unit_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("units.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
# Number of base units contained in one package (e.g. 500 g per package).
|
||||
package_size: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
|
||||
@@ -98,6 +130,7 @@ class Product(Base):
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
group: Mapped[Group | None] = relationship(back_populates="products")
|
||||
display_unit: Mapped[Unit | None] = relationship()
|
||||
lots: Mapped[list[Lot]] = relationship(
|
||||
back_populates="product", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user