New alert system and panel de control page

This commit is contained in:
Urtzi Alfaro
2025-11-27 15:52:40 +01:00
parent 1a2f4602f3
commit e902419b6e
178 changed files with 20982 additions and 6944 deletions

View File

@@ -1,77 +0,0 @@
"""add_local_production_support
Revision ID: add_local_production_support
Revises: e7fcea67bf4e
Create Date: 2025-10-29 14:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'add_local_production_support'
down_revision = 'e7fcea67bf4e'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Add local production support columns to ingredients table"""
# Add produced_locally column
op.add_column('ingredients', sa.Column(
'produced_locally',
sa.Boolean(),
nullable=False,
server_default='false',
comment='If true, ingredient is produced in-house and requires BOM explosion'
))
# Add recipe_id column for BOM explosion
op.add_column('ingredients', sa.Column(
'recipe_id',
postgresql.UUID(as_uuid=True),
nullable=True,
comment='Links to recipe for BOM explosion when ingredient is produced locally'
))
# Create index for efficient querying of locally-produced ingredients
op.create_index(
'ix_ingredients_produced_locally',
'ingredients',
['produced_locally'],
unique=False
)
# Create index for recipe_id lookups
op.create_index(
'ix_ingredients_recipe_id',
'ingredients',
['recipe_id'],
unique=False
)
# Add check constraint: if produced_locally is true, recipe_id should be set
# Note: This is a soft constraint - we allow NULL recipe_id even if produced_locally=true
# to support gradual data migration and edge cases
# op.create_check_constraint(
# 'ck_ingredients_local_production',
# 'ingredients',
# 'produced_locally = false OR recipe_id IS NOT NULL'
# )
def downgrade() -> None:
"""Remove local production support columns from ingredients table"""
# Drop check constraint
# op.drop_constraint('ck_ingredients_local_production', 'ingredients', type_='check')
# Drop indexes
op.drop_index('ix_ingredients_recipe_id', table_name='ingredients')
op.drop_index('ix_ingredients_produced_locally', table_name='ingredients')
# Drop columns
op.drop_column('ingredients', 'recipe_id')
op.drop_column('ingredients', 'produced_locally')

View File

@@ -1,84 +0,0 @@
"""make_stock_management_fields_nullable
Revision ID: make_stock_fields_nullable
Revises: add_local_production_support
Create Date: 2025-11-08 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'make_stock_fields_nullable'
down_revision = 'add_local_production_support'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Make stock management fields nullable to simplify onboarding
These fields (low_stock_threshold, reorder_point, reorder_quantity) are now optional
during onboarding and can be configured later based on actual usage patterns.
"""
# Make low_stock_threshold nullable
op.alter_column('ingredients', 'low_stock_threshold',
existing_type=sa.Float(),
nullable=True,
existing_nullable=False)
# Make reorder_point nullable
op.alter_column('ingredients', 'reorder_point',
existing_type=sa.Float(),
nullable=True,
existing_nullable=False)
# Make reorder_quantity nullable
op.alter_column('ingredients', 'reorder_quantity',
existing_type=sa.Float(),
nullable=True,
existing_nullable=False)
def downgrade() -> None:
"""Revert stock management fields to NOT NULL
WARNING: This will fail if any records have NULL values in these fields.
You must set default values before running this downgrade.
"""
# Set default values for any NULL records before making fields NOT NULL
op.execute("""
UPDATE ingredients
SET low_stock_threshold = 10.0
WHERE low_stock_threshold IS NULL
""")
op.execute("""
UPDATE ingredients
SET reorder_point = 20.0
WHERE reorder_point IS NULL
""")
op.execute("""
UPDATE ingredients
SET reorder_quantity = 50.0
WHERE reorder_quantity IS NULL
""")
# Make fields NOT NULL again
op.alter_column('ingredients', 'low_stock_threshold',
existing_type=sa.Float(),
nullable=False,
existing_nullable=True)
op.alter_column('ingredients', 'reorder_point',
existing_type=sa.Float(),
nullable=False,
existing_nullable=True)
op.alter_column('ingredients', 'reorder_quantity',
existing_type=sa.Float(),
nullable=False,
existing_nullable=True)

View File

@@ -1,9 +1,14 @@
"""initial_schema_20251015_1229
"""Unified initial schema for inventory service
Revision ID: e7fcea67bf4e
Revises:
Create Date: 2025-10-15 12:29:40.991849+02:00
Revision ID: 20251123_unified_initial_schema
Revises:
Create Date: 2025-11-23
This unified migration combines all previous inventory migrations:
- Initial schema (e7fcea67bf4e)
- Local production support (add_local_production_support)
- Stock management fields nullable (20251108_1200_make_stock_fields_nullable)
- Stock receipts (20251123_add_stock_receipts)
"""
from typing import Sequence, Union
@@ -12,14 +17,14 @@ import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'e7fcea67bf4e'
revision: str = '20251123_unified_initial_schema'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Create audit_logs table
op.create_table('audit_logs',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -52,6 +57,8 @@ def upgrade() -> None:
op.create_index(op.f('ix_audit_logs_severity'), 'audit_logs', ['severity'], unique=False)
op.create_index(op.f('ix_audit_logs_tenant_id'), 'audit_logs', ['tenant_id'], unique=False)
op.create_index(op.f('ix_audit_logs_user_id'), 'audit_logs', ['user_id'], unique=False)
# Create ingredients table with all fields including local production support and nullable stock fields
op.create_table('ingredients',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -69,9 +76,10 @@ def upgrade() -> None:
sa.Column('average_cost', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('last_purchase_price', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('standard_cost', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('low_stock_threshold', sa.Float(), nullable=False),
sa.Column('reorder_point', sa.Float(), nullable=False),
sa.Column('reorder_quantity', sa.Float(), nullable=False),
# Stock management fields - made nullable for simplified onboarding
sa.Column('low_stock_threshold', sa.Float(), nullable=True),
sa.Column('reorder_point', sa.Float(), nullable=True),
sa.Column('reorder_quantity', sa.Float(), nullable=True),
sa.Column('max_stock_level', sa.Float(), nullable=True),
sa.Column('shelf_life_days', sa.Integer(), nullable=True),
sa.Column('display_life_hours', sa.Integer(), nullable=True),
@@ -85,6 +93,10 @@ def upgrade() -> None:
sa.Column('is_perishable', sa.Boolean(), nullable=True),
sa.Column('allergen_info', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('nutritional_info', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
# Local production support fields
sa.Column('produced_locally', sa.Boolean(), nullable=False, server_default='false'),
sa.Column('recipe_id', postgresql.UUID(as_uuid=True), nullable=True),
# Audit fields
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', sa.UUID(), nullable=True),
@@ -104,6 +116,10 @@ def upgrade() -> None:
op.create_index(op.f('ix_ingredients_product_type'), 'ingredients', ['product_type'], unique=False)
op.create_index(op.f('ix_ingredients_sku'), 'ingredients', ['sku'], unique=False)
op.create_index(op.f('ix_ingredients_tenant_id'), 'ingredients', ['tenant_id'], unique=False)
op.create_index('ix_ingredients_produced_locally', 'ingredients', ['produced_locally'], unique=False)
op.create_index('ix_ingredients_recipe_id', 'ingredients', ['recipe_id'], unique=False)
# Create temperature_logs table
op.create_table('temperature_logs',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -128,6 +144,8 @@ def upgrade() -> None:
op.create_index(op.f('ix_temperature_logs_recorded_at'), 'temperature_logs', ['recorded_at'], unique=False)
op.create_index(op.f('ix_temperature_logs_storage_location'), 'temperature_logs', ['storage_location'], unique=False)
op.create_index(op.f('ix_temperature_logs_tenant_id'), 'temperature_logs', ['tenant_id'], unique=False)
# Create food_safety_compliance table
op.create_table('food_safety_compliance',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -163,6 +181,8 @@ def upgrade() -> None:
op.create_index(op.f('ix_food_safety_compliance_next_audit_date'), 'food_safety_compliance', ['next_audit_date'], unique=False)
op.create_index(op.f('ix_food_safety_compliance_standard'), 'food_safety_compliance', ['standard'], unique=False)
op.create_index(op.f('ix_food_safety_compliance_tenant_id'), 'food_safety_compliance', ['tenant_id'], unique=False)
# Create product_transformations table
op.create_table('product_transformations',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -196,6 +216,8 @@ def upgrade() -> None:
op.create_index('idx_transformations_tenant_date', 'product_transformations', ['tenant_id', 'transformation_date'], unique=False)
op.create_index(op.f('ix_product_transformations_tenant_id'), 'product_transformations', ['tenant_id'], unique=False)
op.create_index(op.f('ix_product_transformations_transformation_reference'), 'product_transformations', ['transformation_reference'], unique=False)
# Create stock table
op.create_table('stock',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -252,6 +274,81 @@ def upgrade() -> None:
op.create_index(op.f('ix_stock_supplier_id'), 'stock', ['supplier_id'], unique=False)
op.create_index(op.f('ix_stock_tenant_id'), 'stock', ['tenant_id'], unique=False)
op.create_index(op.f('ix_stock_transformation_reference'), 'stock', ['transformation_reference'], unique=False)
# Create stock_receipts table (lot-level tracking)
# Note: The Enum will automatically create the type on first use
op.create_table(
'stock_receipts',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('po_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('po_number', sa.String(100), nullable=True),
sa.Column('received_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('received_by_user_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('status', sa.Enum('draft', 'confirmed', 'cancelled', name='receiptstatus', create_type=True), nullable=False, server_default='draft'),
sa.Column('supplier_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('supplier_name', sa.String(255), nullable=True),
sa.Column('notes', sa.Text, nullable=True),
sa.Column('has_discrepancies', sa.Boolean, nullable=False, server_default='false'),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()')),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('confirmed_at', sa.DateTime(timezone=True), nullable=True),
)
op.create_index('idx_stock_receipts_tenant_status', 'stock_receipts', ['tenant_id', 'status'])
op.create_index('idx_stock_receipts_po', 'stock_receipts', ['po_id'])
op.create_index('idx_stock_receipts_received_at', 'stock_receipts', ['tenant_id', 'received_at'])
op.create_index('idx_stock_receipts_supplier', 'stock_receipts', ['supplier_id', 'received_at'])
# Create stock_receipt_line_items table
op.create_table(
'stock_receipt_line_items',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('receipt_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('ingredient_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('ingredient_name', sa.String(255), nullable=True),
sa.Column('po_line_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('expected_quantity', sa.Numeric(10, 2), nullable=False),
sa.Column('actual_quantity', sa.Numeric(10, 2), nullable=False),
sa.Column('unit_of_measure', sa.String(20), nullable=False),
sa.Column('has_discrepancy', sa.Boolean, nullable=False, server_default='false'),
sa.Column('discrepancy_reason', sa.Text, nullable=True),
sa.Column('unit_cost', sa.Numeric(10, 2), nullable=True),
sa.Column('total_cost', sa.Numeric(10, 2), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()')),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['receipt_id'], ['stock_receipts.id'], ondelete='CASCADE'),
)
op.create_index('idx_line_items_receipt', 'stock_receipt_line_items', ['receipt_id'])
op.create_index('idx_line_items_ingredient', 'stock_receipt_line_items', ['ingredient_id'])
op.create_index('idx_line_items_discrepancy', 'stock_receipt_line_items', ['tenant_id', 'has_discrepancy'])
# Create stock_lots table
op.create_table(
'stock_lots',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('line_item_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('stock_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('lot_number', sa.String(100), nullable=True),
sa.Column('supplier_lot_number', sa.String(100), nullable=True),
sa.Column('quantity', sa.Numeric(10, 2), nullable=False),
sa.Column('unit_of_measure', sa.String(20), nullable=False),
sa.Column('expiration_date', sa.Date, nullable=False),
sa.Column('best_before_date', sa.Date, nullable=True),
sa.Column('warehouse_location', sa.String(100), nullable=True),
sa.Column('storage_zone', sa.String(50), nullable=True),
sa.Column('quality_notes', sa.Text, nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()')),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['line_item_id'], ['stock_receipt_line_items.id'], ondelete='CASCADE'),
)
op.create_index('idx_lots_line_item', 'stock_lots', ['line_item_id'])
op.create_index('idx_lots_stock', 'stock_lots', ['stock_id'])
op.create_index('idx_lots_expiration', 'stock_lots', ['tenant_id', 'expiration_date'])
op.create_index('idx_lots_lot_number', 'stock_lots', ['tenant_id', 'lot_number'])
# Create food_safety_alerts table
op.create_table('food_safety_alerts',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -329,6 +426,8 @@ def upgrade() -> None:
op.create_index(op.f('ix_food_safety_alerts_status'), 'food_safety_alerts', ['status'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_stock_id'), 'food_safety_alerts', ['stock_id'], unique=False)
op.create_index(op.f('ix_food_safety_alerts_tenant_id'), 'food_safety_alerts', ['tenant_id'], unique=False)
# Create stock_alerts table
op.create_table('stock_alerts',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -363,6 +462,8 @@ def upgrade() -> None:
op.create_index(op.f('ix_stock_alerts_ingredient_id'), 'stock_alerts', ['ingredient_id'], unique=False)
op.create_index(op.f('ix_stock_alerts_stock_id'), 'stock_alerts', ['stock_id'], unique=False)
op.create_index(op.f('ix_stock_alerts_tenant_id'), 'stock_alerts', ['tenant_id'], unique=False)
# Create stock_movements table
op.create_table('stock_movements',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False),
@@ -397,11 +498,10 @@ def upgrade() -> None:
op.create_index(op.f('ix_stock_movements_stock_id'), 'stock_movements', ['stock_id'], unique=False)
op.create_index(op.f('ix_stock_movements_supplier_id'), 'stock_movements', ['supplier_id'], unique=False)
op.create_index(op.f('ix_stock_movements_tenant_id'), 'stock_movements', ['tenant_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Drop all tables in reverse order
op.drop_index(op.f('ix_stock_movements_tenant_id'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_supplier_id'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_stock_id'), table_name='stock_movements')
@@ -415,6 +515,7 @@ def downgrade() -> None:
op.drop_index('idx_movements_supplier', table_name='stock_movements')
op.drop_index('idx_movements_reference', table_name='stock_movements')
op.drop_table('stock_movements')
op.drop_index(op.f('ix_stock_alerts_tenant_id'), table_name='stock_alerts')
op.drop_index(op.f('ix_stock_alerts_stock_id'), table_name='stock_alerts')
op.drop_index(op.f('ix_stock_alerts_ingredient_id'), table_name='stock_alerts')
@@ -424,6 +525,7 @@ def downgrade() -> None:
op.drop_index('idx_alerts_tenant_active', table_name='stock_alerts')
op.drop_index('idx_alerts_ingredient', table_name='stock_alerts')
op.drop_table('stock_alerts')
op.drop_index(op.f('ix_food_safety_alerts_tenant_id'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_stock_id'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_status'), table_name='food_safety_alerts')
@@ -434,6 +536,27 @@ def downgrade() -> None:
op.drop_index(op.f('ix_food_safety_alerts_alert_type'), table_name='food_safety_alerts')
op.drop_index(op.f('ix_food_safety_alerts_alert_code'), table_name='food_safety_alerts')
op.drop_table('food_safety_alerts')
# Drop stock receipts tables
op.drop_index('idx_lots_lot_number', table_name='stock_lots')
op.drop_index('idx_lots_expiration', table_name='stock_lots')
op.drop_index('idx_lots_stock', table_name='stock_lots')
op.drop_index('idx_lots_line_item', table_name='stock_lots')
op.drop_table('stock_lots')
op.drop_index('idx_line_items_discrepancy', table_name='stock_receipt_line_items')
op.drop_index('idx_line_items_ingredient', table_name='stock_receipt_line_items')
op.drop_index('idx_line_items_receipt', table_name='stock_receipt_line_items')
op.drop_table('stock_receipt_line_items')
op.drop_index('idx_stock_receipts_supplier', table_name='stock_receipts')
op.drop_index('idx_stock_receipts_received_at', table_name='stock_receipts')
op.drop_index('idx_stock_receipts_po', table_name='stock_receipts')
op.drop_index('idx_stock_receipts_tenant_status', table_name='stock_receipts')
op.drop_table('stock_receipts')
op.execute('DROP TYPE receiptstatus')
# Continue with other tables
op.drop_index(op.f('ix_stock_transformation_reference'), table_name='stock')
op.drop_index(op.f('ix_stock_tenant_id'), table_name='stock')
op.drop_index(op.f('ix_stock_supplier_id'), table_name='stock')
@@ -452,6 +575,7 @@ def downgrade() -> None:
op.drop_index('idx_stock_expiration', table_name='stock')
op.drop_index('idx_stock_batch', table_name='stock')
op.drop_table('stock')
op.drop_index(op.f('ix_product_transformations_transformation_reference'), table_name='product_transformations')
op.drop_index(op.f('ix_product_transformations_tenant_id'), table_name='product_transformations')
op.drop_index('idx_transformations_tenant_date', table_name='product_transformations')
@@ -460,16 +584,21 @@ def downgrade() -> None:
op.drop_index('idx_transformations_source', table_name='product_transformations')
op.drop_index('idx_transformations_reference', table_name='product_transformations')
op.drop_table('product_transformations')
op.drop_index(op.f('ix_food_safety_compliance_tenant_id'), table_name='food_safety_compliance')
op.drop_index(op.f('ix_food_safety_compliance_standard'), table_name='food_safety_compliance')
op.drop_index(op.f('ix_food_safety_compliance_next_audit_date'), table_name='food_safety_compliance')
op.drop_index(op.f('ix_food_safety_compliance_ingredient_id'), table_name='food_safety_compliance')
op.drop_index(op.f('ix_food_safety_compliance_expiration_date'), table_name='food_safety_compliance')
op.drop_table('food_safety_compliance')
op.drop_index(op.f('ix_temperature_logs_tenant_id'), table_name='temperature_logs')
op.drop_index(op.f('ix_temperature_logs_storage_location'), table_name='temperature_logs')
op.drop_index(op.f('ix_temperature_logs_recorded_at'), table_name='temperature_logs')
op.drop_table('temperature_logs')
op.drop_index('ix_ingredients_recipe_id', table_name='ingredients')
op.drop_index('ix_ingredients_produced_locally', table_name='ingredients')
op.drop_index(op.f('ix_ingredients_tenant_id'), table_name='ingredients')
op.drop_index(op.f('ix_ingredients_sku'), table_name='ingredients')
op.drop_index(op.f('ix_ingredients_product_type'), table_name='ingredients')
@@ -485,6 +614,7 @@ def downgrade() -> None:
op.drop_index('idx_ingredients_ingredient_category', table_name='ingredients')
op.drop_index('idx_ingredients_barcode', table_name='ingredients')
op.drop_table('ingredients')
op.drop_index(op.f('ix_audit_logs_user_id'), table_name='audit_logs')
op.drop_index(op.f('ix_audit_logs_tenant_id'), table_name='audit_logs')
op.drop_index(op.f('ix_audit_logs_severity'), table_name='audit_logs')
@@ -499,4 +629,3 @@ def downgrade() -> None:
op.drop_index('idx_audit_service_created', table_name='audit_logs')
op.drop_index('idx_audit_resource_type_action', table_name='audit_logs')
op.drop_table('audit_logs')
# ### end Alembic commands ###