feat(inventory): add spare parts management

This commit is contained in:
Schubert Ferenc 2026-07-05 11:32:06 +02:00
parent abc6e308dd
commit fb5c2cc26a
43 changed files with 2950 additions and 2 deletions

View file

@ -0,0 +1,214 @@
"""create inventory
Revision ID: ef8a2d5c9017
Revises: b4e8f1c7a205
Create Date: 2026-07-05 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "ef8a2d5c9017"
down_revision: Union[str, Sequence[str], None] = "b4e8f1c7a205"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
INVENTORY_PERMISSIONS = [
("inventory.read", "Lager lesen", "Ersatzteile und Lagerdaten anzeigen", "inventory"),
("inventory.create", "Lagerartikel erstellen", "Ersatzteile anlegen", "inventory"),
("inventory.update", "Lagerartikel bearbeiten", "Ersatzteile aktualisieren", "inventory"),
("inventory.delete", "Lagerartikel deaktivieren", "Ersatzteile entfernen oder deaktivieren", "inventory"),
("inventory.stock.adjust", "Bestand anpassen", "Lagerbestand korrigieren", "inventory"),
("inventory.stock.reserve", "Bestand reservieren", "Lagerbestand reservieren oder freigeben", "inventory"),
("inventory.stock.consume", "Bestand verbrauchen", "Lagerbestand verbuchen", "inventory"),
("inventory.manage.masterdata", "Lagerstammdaten verwalten", "Kategorien, Lagerorte und Lieferanten verwalten", "inventory"),
]
def upgrade() -> None:
op.create_table(
"inventory_categories",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=160), nullable=False),
sa.Column("slug", sa.String(length=180), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
sa.UniqueConstraint("slug"),
)
op.create_index(op.f("ix_inventory_categories_name"), "inventory_categories", ["name"], unique=True)
op.create_index(op.f("ix_inventory_categories_slug"), "inventory_categories", ["slug"], unique=True)
op.create_table(
"inventory_locations",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=160), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_index(op.f("ix_inventory_locations_name"), "inventory_locations", ["name"], unique=True)
op.create_table(
"inventory_suppliers",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=180), nullable=False),
sa.Column("contact_name", sa.String(length=180), nullable=True),
sa.Column("email", sa.String(length=255), nullable=True),
sa.Column("phone", sa.String(length=80), nullable=True),
sa.Column("website", sa.String(length=255), nullable=True),
sa.Column("customer_number", sa.String(length=120), nullable=True),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_index(op.f("ix_inventory_suppliers_name"), "inventory_suppliers", ["name"], unique=True)
op.create_table(
"inventory_items",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("sku", sa.String(length=40), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("category_id", sa.Integer(), nullable=True),
sa.Column("manufacturer", sa.String(length=180), nullable=True),
sa.Column("manufacturer_part_number", sa.String(length=180), nullable=True),
sa.Column("supplier_id", sa.Integer(), nullable=True),
sa.Column("supplier_part_number", sa.String(length=180), nullable=True),
sa.Column("compatible_devices", sa.Text(), nullable=True),
sa.Column("location_id", sa.Integer(), nullable=True),
sa.Column("quantity_on_hand", sa.Integer(), server_default="0", nullable=False),
sa.Column("quantity_reserved", sa.Integer(), server_default="0", nullable=False),
sa.Column("quantity_available", sa.Integer(), server_default="0", nullable=False),
sa.Column("reorder_level", sa.Integer(), server_default="0", nullable=False),
sa.Column("unit", sa.String(length=40), server_default="Stk.", nullable=False),
sa.Column("purchase_price_cents", sa.Integer(), nullable=True),
sa.Column("selling_price_cents", sa.Integer(), nullable=True),
sa.Column("currency", sa.String(length=3), server_default="EUR", nullable=False),
sa.Column("tax_rate_percent", sa.Numeric(5, 2), server_default="19.00", nullable=False),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("is_active", sa.Boolean(), server_default="true", nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["category_id"], ["inventory_categories.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["location_id"], ["inventory_locations.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["supplier_id"], ["inventory_suppliers.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("sku", name="uq_inventory_items_sku"),
)
op.create_index(op.f("ix_inventory_items_category_id"), "inventory_items", ["category_id"], unique=False)
op.create_index(op.f("ix_inventory_items_is_active"), "inventory_items", ["is_active"], unique=False)
op.create_index(op.f("ix_inventory_items_location_id"), "inventory_items", ["location_id"], unique=False)
op.create_index(op.f("ix_inventory_items_manufacturer"), "inventory_items", ["manufacturer"], unique=False)
op.create_index(op.f("ix_inventory_items_manufacturer_part_number"), "inventory_items", ["manufacturer_part_number"], unique=False)
op.create_index(op.f("ix_inventory_items_name"), "inventory_items", ["name"], unique=False)
op.create_index(op.f("ix_inventory_items_quantity_available"), "inventory_items", ["quantity_available"], unique=False)
op.create_index(op.f("ix_inventory_items_sku"), "inventory_items", ["sku"], unique=True)
op.create_index(op.f("ix_inventory_items_supplier_id"), "inventory_items", ["supplier_id"], unique=False)
op.create_index(op.f("ix_inventory_items_supplier_part_number"), "inventory_items", ["supplier_part_number"], unique=False)
op.create_table(
"inventory_stock_movements",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("item_id", sa.Integer(), nullable=False),
sa.Column("movement_type", sa.String(length=40), nullable=False),
sa.Column("quantity", sa.Integer(), nullable=False),
sa.Column("reason", sa.String(length=255), server_default="", nullable=False),
sa.Column("reference_type", sa.String(length=80), nullable=True),
sa.Column("reference_id", sa.Integer(), nullable=True),
sa.Column("note", sa.Text(), nullable=True),
sa.Column("actor_user_id", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["actor_user_id"], ["users.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["item_id"], ["inventory_items.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_inventory_stock_movements_actor_user_id"), "inventory_stock_movements", ["actor_user_id"], unique=False)
op.create_index(op.f("ix_inventory_stock_movements_item_id"), "inventory_stock_movements", ["item_id"], unique=False)
op.create_index(op.f("ix_inventory_stock_movements_movement_type"), "inventory_stock_movements", ["movement_type"], unique=False)
op.create_index(op.f("ix_inventory_stock_movements_reference_id"), "inventory_stock_movements", ["reference_id"], unique=False)
op.create_index(op.f("ix_inventory_stock_movements_reference_type"), "inventory_stock_movements", ["reference_type"], unique=False)
for name, display_name, description, module in INVENTORY_PERMISSIONS:
op.execute(
sa.text(
"""
INSERT INTO permissions (name, display_name, description, module)
VALUES (:name, :display_name, :description, :module)
ON CONFLICT (name) DO UPDATE SET
display_name = excluded.display_name,
description = excluded.description,
module = excluded.module
"""
).bindparams(name=name, display_name=display_name, description=description, module=module)
)
role_permissions = {
"administrator": [item[0] for item in INVENTORY_PERMISSIONS],
"management": ["inventory.read", "inventory.create", "inventory.update", "inventory.stock.adjust", "inventory.stock.reserve", "inventory.stock.consume", "inventory.manage.masterdata"],
"technician": ["inventory.read", "inventory.stock.reserve", "inventory.stock.consume"],
"support": ["inventory.read"],
"warehouse": ["inventory.read", "inventory.create", "inventory.update", "inventory.stock.adjust", "inventory.stock.reserve", "inventory.stock.consume", "inventory.manage.masterdata"],
}
for role_name, permission_names in role_permissions.items():
for permission_name in permission_names:
op.execute(
sa.text(
"""
INSERT INTO role_permissions (role_id, permission_id)
SELECT roles.id, permissions.id
FROM roles, permissions
WHERE roles.name = :role_name
AND permissions.name = :permission_name
ON CONFLICT DO NOTHING
"""
).bindparams(role_name=role_name, permission_name=permission_name)
)
def downgrade() -> None:
op.execute(
sa.text(
"""
DELETE FROM role_permissions
WHERE permission_id IN (
SELECT id FROM permissions WHERE module = 'inventory'
)
"""
)
)
op.execute("DELETE FROM permissions WHERE module = 'inventory'")
op.drop_index(op.f("ix_inventory_stock_movements_reference_type"), table_name="inventory_stock_movements")
op.drop_index(op.f("ix_inventory_stock_movements_reference_id"), table_name="inventory_stock_movements")
op.drop_index(op.f("ix_inventory_stock_movements_movement_type"), table_name="inventory_stock_movements")
op.drop_index(op.f("ix_inventory_stock_movements_item_id"), table_name="inventory_stock_movements")
op.drop_index(op.f("ix_inventory_stock_movements_actor_user_id"), table_name="inventory_stock_movements")
op.drop_table("inventory_stock_movements")
op.drop_index(op.f("ix_inventory_items_supplier_part_number"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_supplier_id"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_sku"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_quantity_available"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_name"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_manufacturer_part_number"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_manufacturer"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_location_id"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_is_active"), table_name="inventory_items")
op.drop_index(op.f("ix_inventory_items_category_id"), table_name="inventory_items")
op.drop_table("inventory_items")
op.drop_index(op.f("ix_inventory_suppliers_name"), table_name="inventory_suppliers")
op.drop_table("inventory_suppliers")
op.drop_index(op.f("ix_inventory_locations_name"), table_name="inventory_locations")
op.drop_table("inventory_locations")
op.drop_index(op.f("ix_inventory_categories_slug"), table_name="inventory_categories")
op.drop_index(op.f("ix_inventory_categories_name"), table_name="inventory_categories")
op.drop_table("inventory_categories")