Lagerorte per 10-Zeichen-Code statt fortlaufender ID; iOS-Einlagern ohne Kamerazwang

Lagerort-IDs sind jetzt ein zufaelliger 10-Zeichen-Code (wie die Einzelstueck-UIDs) statt einer fortlaufenden Zahl - so kollidiert die Stammdaten-Sicherung zwischen zwei Instanzen praktisch nie mehr, und der Code ist zugleich der Inhalt des QR /l/<code>. Alle Fremdschluessel (lots, movements, items, Mindestbestaende, parent_id) ziehen mit; die Umstellung laeuft einmalig und transaktional beim Serverstart (_migrate_locations_to_code) und rollt bei Fehlern komplett zurueck. Vor dem Deploy ein DB-Backup machen.

iOS-Einlagern oeffnet nicht mehr sofort die Kamera, sondern ein Formular mit Artikelsuche; die Kamera kommt erst per Button. Im Formular laesst sich der Lagerort zusaetzlich per /l/-QR scannen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 12:15:25 +02:00
parent 6e48cd0a7a
commit d518b4aa23
23 changed files with 507 additions and 220 deletions

View File

@@ -34,7 +34,7 @@ class StockError(ValueError):
# oben (check_in/check_out) bleibt davon unberührt.
# ---------------------------------------------------------------------------
def _object_lot(db: Session, product_id: int, location_id: int | None) -> Lot | None:
def _object_lot(db: Session, product_id: int, location_id: str | None) -> Lot | None:
"""Die (einzige) Bestandszeile eines Gegenstands an einem Lagerort."""
query = db.query(Lot).filter(
Lot.product_id == product_id, Lot.best_before.is_(None)
@@ -50,7 +50,7 @@ def object_add(
db: Session,
product: Product,
quantity: float,
location_id: int | None,
location_id: str | None,
user: User | None,
note: str | None = None,
) -> Lot:
@@ -86,7 +86,7 @@ def object_remove(
db: Session,
product: Product,
quantity: float,
location_id: int | None,
location_id: str | None,
reason: str,
user: User | None,
note: str | None = None,
@@ -121,8 +121,8 @@ def object_relocate(
db: Session,
product: Product,
quantity: float,
from_location_id: int | None,
to_location_id: int | None,
from_location_id: str | None,
to_location_id: str | None,
user: User | None,
note: str | None = None,
) -> None:
@@ -215,7 +215,7 @@ def current_stock(db: Session, product_id: int) -> float:
return float(sum(q for (q,) in total))
def location_stock_base(db: Session, product: Product, location_id: int) -> float:
def location_stock_base(db: Session, product: Product, location_id: str) -> float:
"""Bestand eines Produkts an EINEM Lagerort (in Basiseinheiten).
Einzelstücke zählen die Items an diesem Ort, sonst werden die Lot-Mengen des
@@ -235,9 +235,9 @@ def location_stock_base(db: Session, product: Product, location_id: int) -> floa
return float(sum(q for (q,) in total))
def descendant_location_ids(db: Session, location_id: int) -> set[int]:
def descendant_location_ids(db: Session, location_id: str) -> set[str]:
"""Alle Unter-Lagerorte (rekursiv) eines Lagerorts."""
result: set[int] = set()
result: set[str] = set()
stack = [location_id]
while stack:
cur = stack.pop()
@@ -248,7 +248,7 @@ def descendant_location_ids(db: Session, location_id: int) -> set[int]:
return result
def location_subtree_stock_base(db: Session, product: Product, location_id: int) -> float:
def location_subtree_stock_base(db: Session, product: Product, location_id: str) -> float:
"""Bestand an einem Lagerort INKL. aller Unter-Lagerorte (Basiseinheiten).
So gilt ein Mindestbestand auf „Hedingen" als gedeckt, wenn der Vorrat
@@ -264,7 +264,7 @@ def check_in(
quantity: float,
unit: str,
best_before: date | None,
location_id: int | None,
location_id: str | None,
user: User | None,
note: str | None = None,
best_before_precision: str | None = DatePrecision.day.value,