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:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import secrets
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from sqlalchemy import (
|
||||
@@ -136,14 +137,28 @@ class Group(Base):
|
||||
)
|
||||
|
||||
|
||||
# Lagerorte tragen einen zufälligen 10-Zeichen-Code als ID statt einer
|
||||
# fortlaufenden Zahl. So kollidieren Sicherung/Import zwischen zwei Instanzen
|
||||
# praktisch nie, und der Code IST zugleich der Inhalt des QR /l/<code>.
|
||||
# Alphabet ohne 0/O/1/I/L – wie bei den Einzelstück-UIDs gut ablesbar.
|
||||
_LOCATION_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
|
||||
|
||||
|
||||
def generate_location_code() -> str:
|
||||
"""Zufälliger 10-Zeichen-Code für einen Lagerort (Kollision vernachlässigbar)."""
|
||||
return "".join(secrets.choice(_LOCATION_CODE_ALPHABET) for _ in range(10))
|
||||
|
||||
|
||||
class Location(Base):
|
||||
__tablename__ = "locations"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(10), primary_key=True, default=generate_location_code
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
# Sub-locations (Regal/Fach) are a Schritt-3 feature; parent_id kept for forward-compat.
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
parent_id: Mapped[str | None] = mapped_column(
|
||||
String(10), ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
@@ -312,8 +327,8 @@ class Lot(Base):
|
||||
String(8), nullable=False, default=DatePrecision.day.value,
|
||||
server_default=DatePrecision.day.value,
|
||||
)
|
||||
location_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
location_id: Mapped[str | None] = mapped_column(
|
||||
String(10), ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
@@ -339,8 +354,8 @@ class Movement(Base):
|
||||
note: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
# Bei Gegenstands-Buchungen der betroffene Lagerort (Lebensmittel führen den
|
||||
# Ort an der Charge/Lot). Nullable, damit bestehende Bewegungen gültig bleiben.
|
||||
location_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
location_id: Mapped[str | None] = mapped_column(
|
||||
String(10), ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
# Grund einer Entnahme (lost/broken/…), nur bei type=out aus dem Entfernen-Dialog.
|
||||
# Als kurzer String, damit per ADD COLUMN nachziehbar (kein Postgres-Enumtyp).
|
||||
@@ -527,8 +542,8 @@ class Item(Base):
|
||||
product_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("products.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
location_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
location_id: Mapped[str | None] = mapped_column(
|
||||
String(10), ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
shop_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("shops.id", ondelete="SET NULL"), nullable=True
|
||||
@@ -583,8 +598,8 @@ class ProductLocationMinStock(Base):
|
||||
product_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("products.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
location_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("locations.id", ondelete="CASCADE"), nullable=False
|
||||
location_id: Mapped[str] = mapped_column(
|
||||
String(10), ForeignKey("locations.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
# In Artikeleinheiten (Packungen/Stueck), wie in der Produktliste gezaehlt.
|
||||
min_stock: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
@@ -603,8 +618,8 @@ class GroupLocationMinStock(Base):
|
||||
group_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("groups.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
location_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("locations.id", ondelete="CASCADE"), nullable=False
|
||||
location_id: Mapped[str] = mapped_column(
|
||||
String(10), ForeignKey("locations.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
min_stock: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user