From 392bfb186f3fe1edb37caeff5afae05e9ca2cca7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Nov 2025 17:35:38 +0000 Subject: [PATCH] refactor: Unify database migrations into single initial schemas Consolidated incremental migrations into single unified initial schema files for both procurement and production services. This simplifies database setup and eliminates migration chain complexity. Changes: - Procurement: Merged 3 migrations into 001_unified_initial_schema.py - Initial schema (20251015_1229) - Add supplier_price_list_id (20251030_0737) - Add JTBD reasoning fields (20251107) - Production: Merged 3 migrations into 001_unified_initial_schema.py - Initial schema (20251015_1231) - Add waste tracking fields (20251023_0900) - Add JTBD reasoning fields (20251107) All new fields (reasoning, consequence, reasoning_data, waste_defect_type, is_ai_assisted, supplier_price_list_id) are now included in the initial schemas from the start. Updated model files to use deferred() for reasoning fields to prevent breaking queries when running against existing databases. --- .../procurement/app/models/purchase_order.py | 9 +- ...chema.py => 001_unified_initial_schema.py} | 24 +- ...add_supplier_price_list_id_to_purchase_.py | 42 --- .../versions/20251107_add_reasoning_fields.py | 30 -- .../scripts/demo/seed_demo_purchase_orders.py | 38 ++ services/production/app/models/production.py | 6 +- .../versions/001_unified_initial_schema.py | 336 ++++++++++++++++++ ...a9c1fd8fec_initial_schema_20251015_1231.py | 304 ---------------- ...20251023_0900_add_waste_tracking_fields.py | 51 --- .../versions/20251107_add_reasoning_fields.py | 27 -- 10 files changed, 398 insertions(+), 469 deletions(-) rename services/procurement/migrations/versions/{20251015_1229_initial_schema.py => 001_unified_initial_schema.py} (98%) delete mode 100644 services/procurement/migrations/versions/20251030_0737_9450f58f3623_add_supplier_price_list_id_to_purchase_.py delete mode 100644 services/procurement/migrations/versions/20251107_add_reasoning_fields.py create mode 100644 services/production/migrations/versions/001_unified_initial_schema.py delete mode 100644 services/production/migrations/versions/20251015_1231_42a9c1fd8fec_initial_schema_20251015_1231.py delete mode 100644 services/production/migrations/versions/20251023_0900_add_waste_tracking_fields.py delete mode 100644 services/production/migrations/versions/20251107_add_reasoning_fields.py diff --git a/services/procurement/app/models/purchase_order.py b/services/procurement/app/models/purchase_order.py index 6c192464..dcba2a31 100644 --- a/services/procurement/app/models/purchase_order.py +++ b/services/procurement/app/models/purchase_order.py @@ -12,7 +12,7 @@ from datetime import datetime, timezone from decimal import Decimal from sqlalchemy import Column, String, DateTime, Float, Integer, Text, Index, Boolean, Numeric, ForeignKey, Enum as SQLEnum from sqlalchemy.dialects.postgresql import UUID, JSONB -from sqlalchemy.orm import relationship +from sqlalchemy.orm import relationship, deferred from sqlalchemy.sql import func from shared.database.base import Base @@ -120,9 +120,10 @@ class PurchaseOrder(Base): terms_and_conditions = Column(Text, nullable=True) # JTBD Dashboard: Reasoning and consequences for user transparency - reasoning = Column(Text, nullable=True) # Why this PO was created (e.g., "Low flour stock (2 days left)") - consequence = Column(Text, nullable=True) # What happens if not approved (e.g., "Stock out risk in 48 hours") - reasoning_data = Column(JSONB, nullable=True) # Structured reasoning data + # Deferred loading to prevent breaking queries when columns don't exist yet + reasoning = deferred(Column(Text, nullable=True)) # Why this PO was created (e.g., "Low flour stock (2 days left)") + consequence = deferred(Column(Text, nullable=True)) # What happens if not approved (e.g., "Stock out risk in 48 hours") + reasoning_data = deferred(Column(JSONB, nullable=True)) # Structured reasoning data # reasoning_data structure: { # "trigger": "low_stock" | "forecast_demand" | "manual", # "ingredients_affected": [{"id": "uuid", "name": "Flour", "current_stock": 10, "days_remaining": 2}], diff --git a/services/procurement/migrations/versions/20251015_1229_initial_schema.py b/services/procurement/migrations/versions/001_unified_initial_schema.py similarity index 98% rename from services/procurement/migrations/versions/20251015_1229_initial_schema.py rename to services/procurement/migrations/versions/001_unified_initial_schema.py index 674de29f..5a60e6ab 100644 --- a/services/procurement/migrations/versions/20251015_1229_initial_schema.py +++ b/services/procurement/migrations/versions/001_unified_initial_schema.py @@ -1,12 +1,12 @@ -"""initial procurement schema +"""unified initial procurement schema -Revision ID: 20251015_1229 +Revision ID: 001_unified_initial_schema Revises: -Create Date: 2025-10-15 12:29:00.00000+02:00 +Create Date: 2025-11-07 Complete procurement service schema including: - Procurement plans and requirements -- Purchase orders and items +- Purchase orders and items (with reasoning fields for JTBD dashboard) - Deliveries and delivery items - Supplier invoices - Replenishment planning @@ -21,7 +21,7 @@ import sqlalchemy as sa from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision: str = '20251015_1229' +revision: str = '001_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 @@ -31,7 +31,7 @@ def upgrade() -> None: # Create PostgreSQL enum types first # PurchaseOrderStatus enum purchaseorderstatus_enum = postgresql.ENUM( - 'draft', 'pending_approval', 'approved', 'sent_to_supplier', + 'draft', 'pending_approval', 'approved', 'sent_to_supplier', 'confirmed', 'partially_received', 'completed', 'cancelled', 'disputed', name='purchaseorderstatus', create_type=False @@ -207,7 +207,7 @@ def upgrade() -> None: # PURCHASE ORDER TABLES # ======================================================================== - # Create purchase_orders table + # Create purchase_orders table (with JTBD dashboard reasoning fields) op.create_table('purchase_orders', sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False), @@ -242,6 +242,10 @@ def upgrade() -> None: sa.Column('notes', sa.Text(), nullable=True), sa.Column('internal_notes', sa.Text(), nullable=True), sa.Column('terms_and_conditions', sa.Text(), nullable=True), + # JTBD Dashboard fields + sa.Column('reasoning', sa.Text(), nullable=True), + sa.Column('consequence', sa.Text(), nullable=True), + sa.Column('reasoning_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), onupdate=sa.text('now()'), nullable=False), sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=False), @@ -260,13 +264,14 @@ def upgrade() -> None: op.create_index('ix_purchase_orders_order_date', 'purchase_orders', ['order_date'], unique=False) op.create_index('ix_purchase_orders_delivery_date', 'purchase_orders', ['required_delivery_date'], unique=False) - # Create purchase_order_items table + # Create purchase_order_items table (with supplier_price_list_id) op.create_table('purchase_order_items', sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('purchase_order_id', postgresql.UUID(as_uuid=True), nullable=False), sa.Column('procurement_requirement_id', postgresql.UUID(as_uuid=True), nullable=True), sa.Column('inventory_product_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('supplier_price_list_id', postgresql.UUID(as_uuid=True), nullable=True), sa.Column('product_code', sa.String(length=100), nullable=True), sa.Column('product_name', sa.String(length=200), nullable=False), sa.Column('ordered_quantity', sa.Numeric(precision=12, scale=3), nullable=False), @@ -286,6 +291,7 @@ def upgrade() -> None: op.create_index(op.f('ix_purchase_order_items_inventory_product_id'), 'purchase_order_items', ['inventory_product_id'], unique=False) op.create_index(op.f('ix_purchase_order_items_procurement_requirement_id'), 'purchase_order_items', ['procurement_requirement_id'], unique=False) op.create_index(op.f('ix_purchase_order_items_purchase_order_id'), 'purchase_order_items', ['purchase_order_id'], unique=False) + op.create_index(op.f('ix_purchase_order_items_supplier_price_list_id'), 'purchase_order_items', ['supplier_price_list_id'], unique=False) op.create_index(op.f('ix_purchase_order_items_tenant_id'), 'purchase_order_items', ['tenant_id'], unique=False) op.create_index('ix_po_items_tenant_po', 'purchase_order_items', ['tenant_id', 'purchase_order_id'], unique=False) op.create_index('ix_po_items_inventory_product', 'purchase_order_items', ['inventory_product_id'], unique=False) @@ -594,7 +600,7 @@ def downgrade() -> None: op.drop_table('purchase_orders') op.drop_table('procurement_requirements') op.drop_table('procurement_plans') - + # Drop enum types op.execute("DROP TYPE IF EXISTS purchaseorderstatus") op.execute("DROP TYPE IF EXISTS deliverystatus") diff --git a/services/procurement/migrations/versions/20251030_0737_9450f58f3623_add_supplier_price_list_id_to_purchase_.py b/services/procurement/migrations/versions/20251030_0737_9450f58f3623_add_supplier_price_list_id_to_purchase_.py deleted file mode 100644 index d606ab68..00000000 --- a/services/procurement/migrations/versions/20251030_0737_9450f58f3623_add_supplier_price_list_id_to_purchase_.py +++ /dev/null @@ -1,42 +0,0 @@ -"""add_supplier_price_list_id_to_purchase_order_items - -Revision ID: 9450f58f3623 -Revises: 20251015_1229 -Create Date: 2025-10-30 07:37:07.477603 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - - -# revision identifiers, used by Alembic. -revision: str = '9450f58f3623' -down_revision: Union[str, None] = '20251015_1229' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Add supplier_price_list_id column to purchase_order_items table - op.add_column('purchase_order_items', - sa.Column('supplier_price_list_id', postgresql.UUID(as_uuid=True), nullable=True) - ) - - # Create index on supplier_price_list_id - op.create_index( - 'ix_purchase_order_items_supplier_price_list_id', - 'purchase_order_items', - ['supplier_price_list_id'], - unique=False - ) - - -def downgrade() -> None: - # Drop index first - op.drop_index('ix_purchase_order_items_supplier_price_list_id', table_name='purchase_order_items') - - # Drop column - op.drop_column('purchase_order_items', 'supplier_price_list_id') diff --git a/services/procurement/migrations/versions/20251107_add_reasoning_fields.py b/services/procurement/migrations/versions/20251107_add_reasoning_fields.py deleted file mode 100644 index 78d4649b..00000000 --- a/services/procurement/migrations/versions/20251107_add_reasoning_fields.py +++ /dev/null @@ -1,30 +0,0 @@ -"""add reasoning fields to purchase orders - -Revision ID: 20251107_add_reasoning_fields -Revises: 20251030_0737_9450f58f3623 -Create Date: 2025-11-07 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = '20251107_add_reasoning_fields' -down_revision = '20251030_0737_9450f58f3623' -branch_labels = None -depends_on = None - - -def upgrade(): - # Add reasoning fields to purchase_orders table - op.add_column('purchase_orders', sa.Column('reasoning', sa.Text(), nullable=True)) - op.add_column('purchase_orders', sa.Column('consequence', sa.Text(), nullable=True)) - op.add_column('purchase_orders', sa.Column('reasoning_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True)) - - -def downgrade(): - # Remove reasoning fields from purchase_orders table - op.drop_column('purchase_orders', 'reasoning_data') - op.drop_column('purchase_orders', 'consequence') - op.drop_column('purchase_orders', 'reasoning') diff --git a/services/procurement/scripts/demo/seed_demo_purchase_orders.py b/services/procurement/scripts/demo/seed_demo_purchase_orders.py index ad63ba7e..46ca5334 100644 --- a/services/procurement/scripts/demo/seed_demo_purchase_orders.py +++ b/services/procurement/scripts/demo/seed_demo_purchase_orders.py @@ -114,6 +114,35 @@ async def create_purchase_order( shipping_cost = Decimal(str(random.uniform(0, 20))) total = subtotal + tax_amount + shipping_cost + # Generate reasoning for JTBD dashboard (if columns exist after migration) + days_until_delivery = (required_delivery - created_at).days + reasoning_text = None + reasoning_json = None + consequence_text = None + + try: + # Try to set reasoning fields (will work after migration) + if status == PurchaseOrderStatus.pending_approval: + reasoning_text = f"Low stock detected for {supplier.name} items. Current inventory projected to run out in {days_until_delivery + 2} days." + consequence_text = f"Stock-out risk in {days_until_delivery + 2} days if not approved. Production may be impacted." + reasoning_json = { + "trigger": "low_stock", + "urgency_score": 75 if days_until_delivery < 5 else 50, + "days_remaining": days_until_delivery + 2, + "supplier_trust_score": supplier.trust_score + } + elif auto_approved: + reasoning_text = f"Auto-approved based on supplier trust score ({supplier.trust_score:.0%}) and amount within threshold (€{subtotal:.2f})." + reasoning_json = { + "trigger": "auto_approval", + "trust_score": supplier.trust_score, + "amount": float(subtotal), + "threshold": 500.0 + } + except Exception: + # Columns don't exist yet, that's ok + pass + # Create PO po = PurchaseOrder( id=uuid.uuid4(), @@ -136,6 +165,15 @@ async def create_purchase_order( updated_by=SYSTEM_USER_ID ) + # Set reasoning fields if they exist (after migration) + if reasoning_text: + try: + po.reasoning = reasoning_text + po.consequence = consequence_text + po.reasoning_data = reasoning_json + except Exception: + pass # Columns don't exist yet + # Set approval data if approved if status in [PurchaseOrderStatus.approved, PurchaseOrderStatus.sent_to_supplier, PurchaseOrderStatus.confirmed, PurchaseOrderStatus.completed]: diff --git a/services/production/app/models/production.py b/services/production/app/models/production.py index c381699c..485ada4d 100644 --- a/services/production/app/models/production.py +++ b/services/production/app/models/production.py @@ -7,6 +7,7 @@ Production models for the production service from sqlalchemy import Column, String, Integer, Float, DateTime, Boolean, Text, JSON, Enum as SQLEnum from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import deferred from sqlalchemy.sql import func from datetime import datetime, timezone from typing import Dict, Any, Optional @@ -133,8 +134,9 @@ class ProductionBatch(Base): cancellation_reason = Column(String(255), nullable=True) # JTBD Dashboard: Reasoning and context for user transparency - reasoning = Column(Text, nullable=True) # Why this batch was scheduled (e.g., "Based on wedding order #1234") - reasoning_data = Column(JSON, nullable=True) # Structured reasoning data + # Deferred loading to prevent breaking queries when columns don't exist yet + reasoning = deferred(Column(Text, nullable=True)) # Why this batch was scheduled (e.g., "Based on wedding order #1234") + reasoning_data = deferred(Column(JSON, nullable=True)) # Structured reasoning data # reasoning_data structure: { # "trigger": "forecast" | "order" | "inventory" | "manual", # "forecast_id": "uuid", diff --git a/services/production/migrations/versions/001_unified_initial_schema.py b/services/production/migrations/versions/001_unified_initial_schema.py new file mode 100644 index 00000000..a9ec5f17 --- /dev/null +++ b/services/production/migrations/versions/001_unified_initial_schema.py @@ -0,0 +1,336 @@ +"""unified initial production schema + +Revision ID: 001_unified_initial_schema +Revises: +Create Date: 2025-11-07 + +Complete production service schema including: +- Production batches (with reasoning fields for JTBD dashboard and waste tracking) +- Production schedules +- Production capacity +- Equipment +- Quality checks and templates +- Audit logs +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '001_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: + # Create audit_logs table + op.create_table('audit_logs', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('tenant_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('action', sa.String(length=100), nullable=False), + sa.Column('resource_type', sa.String(length=100), nullable=False), + sa.Column('resource_id', sa.String(length=255), nullable=True), + sa.Column('severity', sa.String(length=20), nullable=False), + sa.Column('service_name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('changes', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('audit_metadata', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('ip_address', sa.String(length=45), nullable=True), + sa.Column('user_agent', sa.Text(), nullable=True), + sa.Column('endpoint', sa.String(length=255), nullable=True), + sa.Column('method', sa.String(length=10), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_audit_resource_type_action', 'audit_logs', ['resource_type', 'action'], unique=False) + op.create_index('idx_audit_service_created', 'audit_logs', ['service_name', 'created_at'], unique=False) + op.create_index('idx_audit_severity_created', 'audit_logs', ['severity', 'created_at'], unique=False) + op.create_index('idx_audit_tenant_created', 'audit_logs', ['tenant_id', 'created_at'], unique=False) + op.create_index('idx_audit_user_created', 'audit_logs', ['user_id', 'created_at'], unique=False) + op.create_index(op.f('ix_audit_logs_action'), 'audit_logs', ['action'], unique=False) + op.create_index(op.f('ix_audit_logs_created_at'), 'audit_logs', ['created_at'], unique=False) + op.create_index(op.f('ix_audit_logs_resource_id'), 'audit_logs', ['resource_id'], unique=False) + op.create_index(op.f('ix_audit_logs_resource_type'), 'audit_logs', ['resource_type'], unique=False) + op.create_index(op.f('ix_audit_logs_service_name'), 'audit_logs', ['service_name'], unique=False) + 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 equipment table + op.create_table('equipment', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('tenant_id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('type', sa.Enum('OVEN', 'MIXER', 'PROOFER', 'FREEZER', 'PACKAGING', 'OTHER', name='equipmenttype'), nullable=False), + sa.Column('model', sa.String(length=100), nullable=True), + sa.Column('serial_number', sa.String(length=100), nullable=True), + sa.Column('location', sa.String(length=255), nullable=True), + sa.Column('status', sa.Enum('OPERATIONAL', 'MAINTENANCE', 'DOWN', 'WARNING', name='equipmentstatus'), nullable=False), + sa.Column('install_date', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_maintenance_date', sa.DateTime(timezone=True), nullable=True), + sa.Column('next_maintenance_date', sa.DateTime(timezone=True), nullable=True), + sa.Column('maintenance_interval_days', sa.Integer(), nullable=True), + sa.Column('efficiency_percentage', sa.Float(), nullable=True), + sa.Column('uptime_percentage', sa.Float(), nullable=True), + sa.Column('energy_usage_kwh', sa.Float(), nullable=True), + sa.Column('power_kw', sa.Float(), nullable=True), + sa.Column('capacity', sa.Float(), nullable=True), + sa.Column('weight_kg', sa.Float(), nullable=True), + sa.Column('current_temperature', sa.Float(), nullable=True), + sa.Column('target_temperature', sa.Float(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_equipment_tenant_id'), 'equipment', ['tenant_id'], unique=False) + + # Create production_batches table (with all fields including reasoning and waste tracking) + op.create_table('production_batches', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('tenant_id', sa.UUID(), nullable=False), + sa.Column('batch_number', sa.String(length=50), nullable=False), + sa.Column('product_id', sa.UUID(), nullable=False), + sa.Column('product_name', sa.String(length=255), nullable=False), + sa.Column('recipe_id', sa.UUID(), nullable=True), + sa.Column('planned_start_time', sa.DateTime(timezone=True), nullable=False), + sa.Column('planned_end_time', sa.DateTime(timezone=True), nullable=False), + sa.Column('planned_quantity', sa.Float(), nullable=False), + sa.Column('planned_duration_minutes', sa.Integer(), nullable=False), + sa.Column('actual_start_time', sa.DateTime(timezone=True), nullable=True), + sa.Column('actual_end_time', sa.DateTime(timezone=True), nullable=True), + sa.Column('actual_quantity', sa.Float(), nullable=True), + sa.Column('actual_duration_minutes', sa.Integer(), nullable=True), + sa.Column('status', sa.Enum('PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'ON_HOLD', 'QUALITY_CHECK', 'FAILED', name='productionstatus'), nullable=False), + sa.Column('priority', sa.Enum('LOW', 'MEDIUM', 'HIGH', 'URGENT', name='productionpriority'), nullable=False), + sa.Column('current_process_stage', sa.Enum('MIXING', 'PROOFING', 'SHAPING', 'BAKING', 'COOLING', 'PACKAGING', 'FINISHING', name='processstage'), nullable=True), + sa.Column('process_stage_history', sa.JSON(), nullable=True), + sa.Column('pending_quality_checks', sa.JSON(), nullable=True), + sa.Column('completed_quality_checks', sa.JSON(), nullable=True), + sa.Column('estimated_cost', sa.Float(), nullable=True), + sa.Column('actual_cost', sa.Float(), nullable=True), + sa.Column('labor_cost', sa.Float(), nullable=True), + sa.Column('material_cost', sa.Float(), nullable=True), + sa.Column('overhead_cost', sa.Float(), nullable=True), + sa.Column('yield_percentage', sa.Float(), nullable=True), + sa.Column('quality_score', sa.Float(), nullable=True), + sa.Column('waste_quantity', sa.Float(), nullable=True), + sa.Column('defect_quantity', sa.Float(), nullable=True), + # Waste tracking fields (from 20251023_0900 migration) + sa.Column('waste_defect_type', sa.String(length=100), nullable=True), + sa.Column('is_ai_assisted', sa.Boolean(), nullable=False, server_default='false'), + sa.Column('equipment_used', sa.JSON(), nullable=True), + sa.Column('staff_assigned', sa.JSON(), nullable=True), + sa.Column('station_id', sa.String(length=50), nullable=True), + sa.Column('order_id', sa.UUID(), nullable=True), + sa.Column('forecast_id', sa.UUID(), nullable=True), + sa.Column('is_rush_order', sa.Boolean(), nullable=True), + sa.Column('is_special_recipe', sa.Boolean(), nullable=True), + sa.Column('production_notes', sa.Text(), nullable=True), + sa.Column('quality_notes', sa.Text(), nullable=True), + sa.Column('delay_reason', sa.String(length=255), nullable=True), + sa.Column('cancellation_reason', sa.String(length=255), nullable=True), + # JTBD Dashboard fields (from 20251107 migration) + sa.Column('reasoning', sa.Text(), nullable=True), + sa.Column('reasoning_data', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_production_batches_batch_number'), 'production_batches', ['batch_number'], unique=True) + op.create_index(op.f('ix_production_batches_current_process_stage'), 'production_batches', ['current_process_stage'], unique=False) + op.create_index(op.f('ix_production_batches_product_id'), 'production_batches', ['product_id'], unique=False) + op.create_index(op.f('ix_production_batches_status'), 'production_batches', ['status'], unique=False) + op.create_index(op.f('ix_production_batches_tenant_id'), 'production_batches', ['tenant_id'], unique=False) + op.create_index('ix_production_batches_is_ai_assisted', 'production_batches', ['is_ai_assisted'], unique=False) + + # Create production_capacity table + op.create_table('production_capacity', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('tenant_id', sa.UUID(), nullable=False), + sa.Column('resource_type', sa.String(length=50), nullable=False), + sa.Column('resource_id', sa.String(length=100), nullable=False), + sa.Column('resource_name', sa.String(length=255), nullable=False), + sa.Column('date', sa.DateTime(timezone=True), nullable=False), + sa.Column('start_time', sa.DateTime(timezone=True), nullable=False), + sa.Column('end_time', sa.DateTime(timezone=True), nullable=False), + sa.Column('total_capacity_units', sa.Float(), nullable=False), + sa.Column('allocated_capacity_units', sa.Float(), nullable=False), + sa.Column('remaining_capacity_units', sa.Float(), nullable=False), + sa.Column('is_available', sa.Boolean(), nullable=True), + sa.Column('is_maintenance', sa.Boolean(), nullable=True), + sa.Column('is_reserved', sa.Boolean(), nullable=True), + sa.Column('equipment_type', sa.String(length=100), nullable=True), + sa.Column('max_batch_size', sa.Float(), nullable=True), + sa.Column('min_batch_size', sa.Float(), nullable=True), + sa.Column('setup_time_minutes', sa.Integer(), nullable=True), + sa.Column('cleanup_time_minutes', sa.Integer(), nullable=True), + sa.Column('efficiency_rating', sa.Float(), nullable=True), + sa.Column('maintenance_status', sa.String(length=50), nullable=True), + sa.Column('last_maintenance_date', sa.DateTime(timezone=True), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('restrictions', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_production_capacity_date'), 'production_capacity', ['date'], unique=False) + op.create_index(op.f('ix_production_capacity_tenant_id'), 'production_capacity', ['tenant_id'], unique=False) + + # Create production_schedules table + op.create_table('production_schedules', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('tenant_id', sa.UUID(), nullable=False), + sa.Column('schedule_date', sa.DateTime(timezone=True), nullable=False), + sa.Column('shift_start', sa.DateTime(timezone=True), nullable=False), + sa.Column('shift_end', sa.DateTime(timezone=True), nullable=False), + sa.Column('total_capacity_hours', sa.Float(), nullable=False), + sa.Column('planned_capacity_hours', sa.Float(), nullable=False), + sa.Column('actual_capacity_hours', sa.Float(), nullable=True), + sa.Column('overtime_hours', sa.Float(), nullable=True), + sa.Column('staff_count', sa.Integer(), nullable=False), + sa.Column('equipment_capacity', sa.JSON(), nullable=True), + sa.Column('station_assignments', sa.JSON(), nullable=True), + sa.Column('total_batches_planned', sa.Integer(), nullable=False), + sa.Column('total_batches_completed', sa.Integer(), nullable=True), + sa.Column('total_quantity_planned', sa.Float(), nullable=False), + sa.Column('total_quantity_produced', sa.Float(), nullable=True), + sa.Column('is_finalized', sa.Boolean(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('efficiency_percentage', sa.Float(), nullable=True), + sa.Column('utilization_percentage', sa.Float(), nullable=True), + sa.Column('on_time_completion_rate', sa.Float(), nullable=True), + sa.Column('schedule_notes', sa.Text(), nullable=True), + sa.Column('schedule_adjustments', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('finalized_at', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_production_schedules_schedule_date'), 'production_schedules', ['schedule_date'], unique=False) + op.create_index(op.f('ix_production_schedules_tenant_id'), 'production_schedules', ['tenant_id'], unique=False) + + # Create quality_check_templates table + op.create_table('quality_check_templates', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('tenant_id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('template_code', sa.String(length=100), nullable=True), + sa.Column('check_type', sa.String(length=50), nullable=False), + sa.Column('category', sa.String(length=100), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('instructions', sa.Text(), nullable=True), + sa.Column('parameters', sa.JSON(), nullable=True), + sa.Column('thresholds', sa.JSON(), nullable=True), + sa.Column('scoring_criteria', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_required', sa.Boolean(), nullable=True), + sa.Column('is_critical', sa.Boolean(), nullable=True), + sa.Column('weight', sa.Float(), nullable=True), + sa.Column('min_value', sa.Float(), nullable=True), + sa.Column('max_value', sa.Float(), nullable=True), + sa.Column('target_value', sa.Float(), nullable=True), + sa.Column('unit', sa.String(length=20), nullable=True), + sa.Column('tolerance_percentage', sa.Float(), nullable=True), + sa.Column('applicable_stages', sa.JSON(), nullable=True), + sa.Column('created_by', sa.UUID(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_quality_check_templates_template_code'), 'quality_check_templates', ['template_code'], unique=False) + op.create_index(op.f('ix_quality_check_templates_tenant_id'), 'quality_check_templates', ['tenant_id'], unique=False) + + # Create quality_checks table + op.create_table('quality_checks', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('tenant_id', sa.UUID(), nullable=False), + sa.Column('batch_id', sa.UUID(), nullable=False), + sa.Column('template_id', sa.UUID(), nullable=True), + sa.Column('check_type', sa.String(length=50), nullable=False), + sa.Column('process_stage', sa.Enum('MIXING', 'PROOFING', 'SHAPING', 'BAKING', 'COOLING', 'PACKAGING', 'FINISHING', name='processstage'), nullable=True), + sa.Column('check_time', sa.DateTime(timezone=True), nullable=False), + sa.Column('checker_id', sa.String(length=100), nullable=True), + sa.Column('quality_score', sa.Float(), nullable=False), + sa.Column('pass_fail', sa.Boolean(), nullable=False), + sa.Column('defect_count', sa.Integer(), nullable=False), + sa.Column('defect_types', sa.JSON(), nullable=True), + sa.Column('measured_weight', sa.Float(), nullable=True), + sa.Column('measured_temperature', sa.Float(), nullable=True), + sa.Column('measured_moisture', sa.Float(), nullable=True), + sa.Column('measured_dimensions', sa.JSON(), nullable=True), + sa.Column('stage_specific_data', sa.JSON(), nullable=True), + sa.Column('target_weight', sa.Float(), nullable=True), + sa.Column('target_temperature', sa.Float(), nullable=True), + sa.Column('target_moisture', sa.Float(), nullable=True), + sa.Column('tolerance_percentage', sa.Float(), nullable=True), + sa.Column('within_tolerance', sa.Boolean(), nullable=True), + sa.Column('corrective_action_needed', sa.Boolean(), nullable=True), + sa.Column('corrective_actions', sa.JSON(), nullable=True), + sa.Column('template_results', sa.JSON(), nullable=True), + sa.Column('criteria_scores', sa.JSON(), nullable=True), + sa.Column('check_notes', sa.Text(), nullable=True), + sa.Column('photos_urls', sa.JSON(), nullable=True), + sa.Column('certificate_url', sa.String(length=500), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_quality_checks_batch_id'), 'quality_checks', ['batch_id'], unique=False) + op.create_index(op.f('ix_quality_checks_process_stage'), 'quality_checks', ['process_stage'], unique=False) + op.create_index(op.f('ix_quality_checks_template_id'), 'quality_checks', ['template_id'], unique=False) + op.create_index(op.f('ix_quality_checks_tenant_id'), 'quality_checks', ['tenant_id'], unique=False) + + +def downgrade() -> None: + # Drop tables in reverse order of creation + op.drop_index(op.f('ix_quality_checks_tenant_id'), table_name='quality_checks') + op.drop_index(op.f('ix_quality_checks_template_id'), table_name='quality_checks') + op.drop_index(op.f('ix_quality_checks_process_stage'), table_name='quality_checks') + op.drop_index(op.f('ix_quality_checks_batch_id'), table_name='quality_checks') + op.drop_table('quality_checks') + op.drop_index(op.f('ix_quality_check_templates_tenant_id'), table_name='quality_check_templates') + op.drop_index(op.f('ix_quality_check_templates_template_code'), table_name='quality_check_templates') + op.drop_table('quality_check_templates') + op.drop_index(op.f('ix_production_schedules_tenant_id'), table_name='production_schedules') + op.drop_index(op.f('ix_production_schedules_schedule_date'), table_name='production_schedules') + op.drop_table('production_schedules') + op.drop_index(op.f('ix_production_capacity_tenant_id'), table_name='production_capacity') + op.drop_index(op.f('ix_production_capacity_date'), table_name='production_capacity') + op.drop_table('production_capacity') + op.drop_index('ix_production_batches_is_ai_assisted', table_name='production_batches') + op.drop_index(op.f('ix_production_batches_tenant_id'), table_name='production_batches') + op.drop_index(op.f('ix_production_batches_status'), table_name='production_batches') + op.drop_index(op.f('ix_production_batches_product_id'), table_name='production_batches') + op.drop_index(op.f('ix_production_batches_current_process_stage'), table_name='production_batches') + op.drop_index(op.f('ix_production_batches_batch_number'), table_name='production_batches') + op.drop_table('production_batches') + op.drop_index(op.f('ix_equipment_tenant_id'), table_name='equipment') + op.drop_table('equipment') + 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') + op.drop_index(op.f('ix_audit_logs_service_name'), table_name='audit_logs') + op.drop_index(op.f('ix_audit_logs_resource_type'), table_name='audit_logs') + op.drop_index(op.f('ix_audit_logs_resource_id'), table_name='audit_logs') + op.create_index(op.f('ix_audit_logs_created_at'), table_name='audit_logs') + op.drop_index(op.f('ix_audit_logs_action'), table_name='audit_logs') + op.drop_index('idx_audit_user_created', table_name='audit_logs') + op.drop_index('idx_audit_tenant_created', table_name='audit_logs') + op.drop_index('idx_audit_severity_created', table_name='audit_logs') + 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') + + # Drop enum types + op.execute("DROP TYPE IF EXISTS equipmenttype") + op.execute("DROP TYPE IF EXISTS equipmentstatus") + op.execute("DROP TYPE IF EXISTS productionstatus") + op.execute("DROP TYPE IF EXISTS productionpriority") + op.execute("DROP TYPE IF EXISTS processstage") diff --git a/services/production/migrations/versions/20251015_1231_42a9c1fd8fec_initial_schema_20251015_1231.py b/services/production/migrations/versions/20251015_1231_42a9c1fd8fec_initial_schema_20251015_1231.py deleted file mode 100644 index 3b5dc31b..00000000 --- a/services/production/migrations/versions/20251015_1231_42a9c1fd8fec_initial_schema_20251015_1231.py +++ /dev/null @@ -1,304 +0,0 @@ -"""initial_schema_20251015_1231 - -Revision ID: 42a9c1fd8fec -Revises: -Create Date: 2025-10-15 12:31:07.740405+02:00 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = '42a9c1fd8fec' -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! ### - op.create_table('audit_logs', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('tenant_id', sa.UUID(), nullable=False), - sa.Column('user_id', sa.UUID(), nullable=False), - sa.Column('action', sa.String(length=100), nullable=False), - sa.Column('resource_type', sa.String(length=100), nullable=False), - sa.Column('resource_id', sa.String(length=255), nullable=True), - sa.Column('severity', sa.String(length=20), nullable=False), - sa.Column('service_name', sa.String(length=100), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('changes', postgresql.JSON(astext_type=sa.Text()), nullable=True), - sa.Column('audit_metadata', postgresql.JSON(astext_type=sa.Text()), nullable=True), - sa.Column('ip_address', sa.String(length=45), nullable=True), - sa.Column('user_agent', sa.Text(), nullable=True), - sa.Column('endpoint', sa.String(length=255), nullable=True), - sa.Column('method', sa.String(length=10), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('idx_audit_resource_type_action', 'audit_logs', ['resource_type', 'action'], unique=False) - op.create_index('idx_audit_service_created', 'audit_logs', ['service_name', 'created_at'], unique=False) - op.create_index('idx_audit_severity_created', 'audit_logs', ['severity', 'created_at'], unique=False) - op.create_index('idx_audit_tenant_created', 'audit_logs', ['tenant_id', 'created_at'], unique=False) - op.create_index('idx_audit_user_created', 'audit_logs', ['user_id', 'created_at'], unique=False) - op.create_index(op.f('ix_audit_logs_action'), 'audit_logs', ['action'], unique=False) - op.create_index(op.f('ix_audit_logs_created_at'), 'audit_logs', ['created_at'], unique=False) - op.create_index(op.f('ix_audit_logs_resource_id'), 'audit_logs', ['resource_id'], unique=False) - op.create_index(op.f('ix_audit_logs_resource_type'), 'audit_logs', ['resource_type'], unique=False) - op.create_index(op.f('ix_audit_logs_service_name'), 'audit_logs', ['service_name'], unique=False) - 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) - op.create_table('equipment', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('tenant_id', sa.UUID(), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('type', sa.Enum('OVEN', 'MIXER', 'PROOFER', 'FREEZER', 'PACKAGING', 'OTHER', name='equipmenttype'), nullable=False), - sa.Column('model', sa.String(length=100), nullable=True), - sa.Column('serial_number', sa.String(length=100), nullable=True), - sa.Column('location', sa.String(length=255), nullable=True), - sa.Column('status', sa.Enum('OPERATIONAL', 'MAINTENANCE', 'DOWN', 'WARNING', name='equipmentstatus'), nullable=False), - sa.Column('install_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('last_maintenance_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('next_maintenance_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('maintenance_interval_days', sa.Integer(), nullable=True), - sa.Column('efficiency_percentage', sa.Float(), nullable=True), - sa.Column('uptime_percentage', sa.Float(), nullable=True), - sa.Column('energy_usage_kwh', sa.Float(), nullable=True), - sa.Column('power_kw', sa.Float(), nullable=True), - sa.Column('capacity', sa.Float(), nullable=True), - sa.Column('weight_kg', sa.Float(), nullable=True), - sa.Column('current_temperature', sa.Float(), nullable=True), - sa.Column('target_temperature', sa.Float(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_equipment_tenant_id'), 'equipment', ['tenant_id'], unique=False) - op.create_table('production_batches', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('tenant_id', sa.UUID(), nullable=False), - sa.Column('batch_number', sa.String(length=50), nullable=False), - sa.Column('product_id', sa.UUID(), nullable=False), - sa.Column('product_name', sa.String(length=255), nullable=False), - sa.Column('recipe_id', sa.UUID(), nullable=True), - sa.Column('planned_start_time', sa.DateTime(timezone=True), nullable=False), - sa.Column('planned_end_time', sa.DateTime(timezone=True), nullable=False), - sa.Column('planned_quantity', sa.Float(), nullable=False), - sa.Column('planned_duration_minutes', sa.Integer(), nullable=False), - sa.Column('actual_start_time', sa.DateTime(timezone=True), nullable=True), - sa.Column('actual_end_time', sa.DateTime(timezone=True), nullable=True), - sa.Column('actual_quantity', sa.Float(), nullable=True), - sa.Column('actual_duration_minutes', sa.Integer(), nullable=True), - sa.Column('status', sa.Enum('PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'ON_HOLD', 'QUALITY_CHECK', 'FAILED', name='productionstatus'), nullable=False), - sa.Column('priority', sa.Enum('LOW', 'MEDIUM', 'HIGH', 'URGENT', name='productionpriority'), nullable=False), - sa.Column('current_process_stage', sa.Enum('MIXING', 'PROOFING', 'SHAPING', 'BAKING', 'COOLING', 'PACKAGING', 'FINISHING', name='processstage'), nullable=True), - sa.Column('process_stage_history', sa.JSON(), nullable=True), - sa.Column('pending_quality_checks', sa.JSON(), nullable=True), - sa.Column('completed_quality_checks', sa.JSON(), nullable=True), - sa.Column('estimated_cost', sa.Float(), nullable=True), - sa.Column('actual_cost', sa.Float(), nullable=True), - sa.Column('labor_cost', sa.Float(), nullable=True), - sa.Column('material_cost', sa.Float(), nullable=True), - sa.Column('overhead_cost', sa.Float(), nullable=True), - sa.Column('yield_percentage', sa.Float(), nullable=True), - sa.Column('quality_score', sa.Float(), nullable=True), - sa.Column('waste_quantity', sa.Float(), nullable=True), - sa.Column('defect_quantity', sa.Float(), nullable=True), - sa.Column('equipment_used', sa.JSON(), nullable=True), - sa.Column('staff_assigned', sa.JSON(), nullable=True), - sa.Column('station_id', sa.String(length=50), nullable=True), - sa.Column('order_id', sa.UUID(), nullable=True), - sa.Column('forecast_id', sa.UUID(), nullable=True), - sa.Column('is_rush_order', sa.Boolean(), nullable=True), - sa.Column('is_special_recipe', sa.Boolean(), nullable=True), - sa.Column('production_notes', sa.Text(), nullable=True), - sa.Column('quality_notes', sa.Text(), nullable=True), - sa.Column('delay_reason', sa.String(length=255), nullable=True), - sa.Column('cancellation_reason', sa.String(length=255), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_production_batches_batch_number'), 'production_batches', ['batch_number'], unique=True) - op.create_index(op.f('ix_production_batches_current_process_stage'), 'production_batches', ['current_process_stage'], unique=False) - op.create_index(op.f('ix_production_batches_product_id'), 'production_batches', ['product_id'], unique=False) - op.create_index(op.f('ix_production_batches_status'), 'production_batches', ['status'], unique=False) - op.create_index(op.f('ix_production_batches_tenant_id'), 'production_batches', ['tenant_id'], unique=False) - op.create_table('production_capacity', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('tenant_id', sa.UUID(), nullable=False), - sa.Column('resource_type', sa.String(length=50), nullable=False), - sa.Column('resource_id', sa.String(length=100), nullable=False), - sa.Column('resource_name', sa.String(length=255), nullable=False), - sa.Column('date', sa.DateTime(timezone=True), nullable=False), - sa.Column('start_time', sa.DateTime(timezone=True), nullable=False), - sa.Column('end_time', sa.DateTime(timezone=True), nullable=False), - sa.Column('total_capacity_units', sa.Float(), nullable=False), - sa.Column('allocated_capacity_units', sa.Float(), nullable=False), - sa.Column('remaining_capacity_units', sa.Float(), nullable=False), - sa.Column('is_available', sa.Boolean(), nullable=True), - sa.Column('is_maintenance', sa.Boolean(), nullable=True), - sa.Column('is_reserved', sa.Boolean(), nullable=True), - sa.Column('equipment_type', sa.String(length=100), nullable=True), - sa.Column('max_batch_size', sa.Float(), nullable=True), - sa.Column('min_batch_size', sa.Float(), nullable=True), - sa.Column('setup_time_minutes', sa.Integer(), nullable=True), - sa.Column('cleanup_time_minutes', sa.Integer(), nullable=True), - sa.Column('efficiency_rating', sa.Float(), nullable=True), - sa.Column('maintenance_status', sa.String(length=50), nullable=True), - sa.Column('last_maintenance_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('restrictions', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_production_capacity_date'), 'production_capacity', ['date'], unique=False) - op.create_index(op.f('ix_production_capacity_tenant_id'), 'production_capacity', ['tenant_id'], unique=False) - op.create_table('production_schedules', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('tenant_id', sa.UUID(), nullable=False), - sa.Column('schedule_date', sa.DateTime(timezone=True), nullable=False), - sa.Column('shift_start', sa.DateTime(timezone=True), nullable=False), - sa.Column('shift_end', sa.DateTime(timezone=True), nullable=False), - sa.Column('total_capacity_hours', sa.Float(), nullable=False), - sa.Column('planned_capacity_hours', sa.Float(), nullable=False), - sa.Column('actual_capacity_hours', sa.Float(), nullable=True), - sa.Column('overtime_hours', sa.Float(), nullable=True), - sa.Column('staff_count', sa.Integer(), nullable=False), - sa.Column('equipment_capacity', sa.JSON(), nullable=True), - sa.Column('station_assignments', sa.JSON(), nullable=True), - sa.Column('total_batches_planned', sa.Integer(), nullable=False), - sa.Column('total_batches_completed', sa.Integer(), nullable=True), - sa.Column('total_quantity_planned', sa.Float(), nullable=False), - sa.Column('total_quantity_produced', sa.Float(), nullable=True), - sa.Column('is_finalized', sa.Boolean(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('efficiency_percentage', sa.Float(), nullable=True), - sa.Column('utilization_percentage', sa.Float(), nullable=True), - sa.Column('on_time_completion_rate', sa.Float(), nullable=True), - sa.Column('schedule_notes', sa.Text(), nullable=True), - sa.Column('schedule_adjustments', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('finalized_at', sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_production_schedules_schedule_date'), 'production_schedules', ['schedule_date'], unique=False) - op.create_index(op.f('ix_production_schedules_tenant_id'), 'production_schedules', ['tenant_id'], unique=False) - op.create_table('quality_check_templates', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('tenant_id', sa.UUID(), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('template_code', sa.String(length=100), nullable=True), - sa.Column('check_type', sa.String(length=50), nullable=False), - sa.Column('category', sa.String(length=100), nullable=True), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('instructions', sa.Text(), nullable=True), - sa.Column('parameters', sa.JSON(), nullable=True), - sa.Column('thresholds', sa.JSON(), nullable=True), - sa.Column('scoring_criteria', sa.JSON(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('is_required', sa.Boolean(), nullable=True), - sa.Column('is_critical', sa.Boolean(), nullable=True), - sa.Column('weight', sa.Float(), nullable=True), - sa.Column('min_value', sa.Float(), nullable=True), - sa.Column('max_value', sa.Float(), nullable=True), - sa.Column('target_value', sa.Float(), nullable=True), - sa.Column('unit', sa.String(length=20), nullable=True), - sa.Column('tolerance_percentage', sa.Float(), nullable=True), - sa.Column('applicable_stages', sa.JSON(), nullable=True), - sa.Column('created_by', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_quality_check_templates_template_code'), 'quality_check_templates', ['template_code'], unique=False) - op.create_index(op.f('ix_quality_check_templates_tenant_id'), 'quality_check_templates', ['tenant_id'], unique=False) - op.create_table('quality_checks', - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('tenant_id', sa.UUID(), nullable=False), - sa.Column('batch_id', sa.UUID(), nullable=False), - sa.Column('template_id', sa.UUID(), nullable=True), - sa.Column('check_type', sa.String(length=50), nullable=False), - sa.Column('process_stage', sa.Enum('MIXING', 'PROOFING', 'SHAPING', 'BAKING', 'COOLING', 'PACKAGING', 'FINISHING', name='processstage'), nullable=True), - sa.Column('check_time', sa.DateTime(timezone=True), nullable=False), - sa.Column('checker_id', sa.String(length=100), nullable=True), - sa.Column('quality_score', sa.Float(), nullable=False), - sa.Column('pass_fail', sa.Boolean(), nullable=False), - sa.Column('defect_count', sa.Integer(), nullable=False), - sa.Column('defect_types', sa.JSON(), nullable=True), - sa.Column('measured_weight', sa.Float(), nullable=True), - sa.Column('measured_temperature', sa.Float(), nullable=True), - sa.Column('measured_moisture', sa.Float(), nullable=True), - sa.Column('measured_dimensions', sa.JSON(), nullable=True), - sa.Column('stage_specific_data', sa.JSON(), nullable=True), - sa.Column('target_weight', sa.Float(), nullable=True), - sa.Column('target_temperature', sa.Float(), nullable=True), - sa.Column('target_moisture', sa.Float(), nullable=True), - sa.Column('tolerance_percentage', sa.Float(), nullable=True), - sa.Column('within_tolerance', sa.Boolean(), nullable=True), - sa.Column('corrective_action_needed', sa.Boolean(), nullable=True), - sa.Column('corrective_actions', sa.JSON(), nullable=True), - sa.Column('template_results', sa.JSON(), nullable=True), - sa.Column('criteria_scores', sa.JSON(), nullable=True), - sa.Column('check_notes', sa.Text(), nullable=True), - sa.Column('photos_urls', sa.JSON(), nullable=True), - sa.Column('certificate_url', sa.String(length=500), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_quality_checks_batch_id'), 'quality_checks', ['batch_id'], unique=False) - op.create_index(op.f('ix_quality_checks_process_stage'), 'quality_checks', ['process_stage'], unique=False) - op.create_index(op.f('ix_quality_checks_template_id'), 'quality_checks', ['template_id'], unique=False) - op.create_index(op.f('ix_quality_checks_tenant_id'), 'quality_checks', ['tenant_id'], unique=False) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_quality_checks_tenant_id'), table_name='quality_checks') - op.drop_index(op.f('ix_quality_checks_template_id'), table_name='quality_checks') - op.drop_index(op.f('ix_quality_checks_process_stage'), table_name='quality_checks') - op.drop_index(op.f('ix_quality_checks_batch_id'), table_name='quality_checks') - op.drop_table('quality_checks') - op.drop_index(op.f('ix_quality_check_templates_tenant_id'), table_name='quality_check_templates') - op.drop_index(op.f('ix_quality_check_templates_template_code'), table_name='quality_check_templates') - op.drop_table('quality_check_templates') - op.drop_index(op.f('ix_production_schedules_tenant_id'), table_name='production_schedules') - op.drop_index(op.f('ix_production_schedules_schedule_date'), table_name='production_schedules') - op.drop_table('production_schedules') - op.drop_index(op.f('ix_production_capacity_tenant_id'), table_name='production_capacity') - op.drop_index(op.f('ix_production_capacity_date'), table_name='production_capacity') - op.drop_table('production_capacity') - op.drop_index(op.f('ix_production_batches_tenant_id'), table_name='production_batches') - op.drop_index(op.f('ix_production_batches_status'), table_name='production_batches') - op.drop_index(op.f('ix_production_batches_product_id'), table_name='production_batches') - op.drop_index(op.f('ix_production_batches_current_process_stage'), table_name='production_batches') - op.drop_index(op.f('ix_production_batches_batch_number'), table_name='production_batches') - op.drop_table('production_batches') - op.drop_index(op.f('ix_equipment_tenant_id'), table_name='equipment') - op.drop_table('equipment') - 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') - op.drop_index(op.f('ix_audit_logs_service_name'), table_name='audit_logs') - op.drop_index(op.f('ix_audit_logs_resource_type'), table_name='audit_logs') - op.drop_index(op.f('ix_audit_logs_resource_id'), table_name='audit_logs') - op.drop_index(op.f('ix_audit_logs_created_at'), table_name='audit_logs') - op.drop_index(op.f('ix_audit_logs_action'), table_name='audit_logs') - op.drop_index('idx_audit_user_created', table_name='audit_logs') - op.drop_index('idx_audit_tenant_created', table_name='audit_logs') - op.drop_index('idx_audit_severity_created', table_name='audit_logs') - 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 ### diff --git a/services/production/migrations/versions/20251023_0900_add_waste_tracking_fields.py b/services/production/migrations/versions/20251023_0900_add_waste_tracking_fields.py deleted file mode 100644 index 8cc42471..00000000 --- a/services/production/migrations/versions/20251023_0900_add_waste_tracking_fields.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Add waste_defect_type and is_ai_assisted to production_batches - -Revision ID: 7f8e9d2a1b3c -Revises: 42a9c1fd8fec -Create Date: 2025-10-23 09:00:00.000000 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '7f8e9d2a1b3c' -down_revision = '42a9c1fd8fec' -branch_labels = None -depends_on = None - - -def upgrade() -> None: - """Add waste_defect_type and is_ai_assisted columns to production_batches table""" - - # Add waste_defect_type column - op.add_column( - 'production_batches', - sa.Column('waste_defect_type', sa.String(length=100), nullable=True) - ) - - # Add is_ai_assisted column with default False - op.add_column( - 'production_batches', - sa.Column('is_ai_assisted', sa.Boolean(), nullable=False, server_default='false') - ) - - # Add index on is_ai_assisted for faster queries on AI-assisted batch filtering - op.create_index( - 'ix_production_batches_is_ai_assisted', - 'production_batches', - ['is_ai_assisted'], - unique=False - ) - - -def downgrade() -> None: - """Remove waste_defect_type and is_ai_assisted columns from production_batches table""" - - # Drop index first - op.drop_index('ix_production_batches_is_ai_assisted', table_name='production_batches') - - # Drop columns - op.drop_column('production_batches', 'is_ai_assisted') - op.drop_column('production_batches', 'waste_defect_type') diff --git a/services/production/migrations/versions/20251107_add_reasoning_fields.py b/services/production/migrations/versions/20251107_add_reasoning_fields.py deleted file mode 100644 index 01ff9bfb..00000000 --- a/services/production/migrations/versions/20251107_add_reasoning_fields.py +++ /dev/null @@ -1,27 +0,0 @@ -"""add reasoning fields to production batches - -Revision ID: 20251107_add_reasoning_fields -Revises: 20251023_0900_add_waste_tracking_fields -Create Date: 2025-11-07 - -""" -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision = '20251107_add_reasoning_fields' -down_revision = '20251023_0900_add_waste_tracking_fields' -branch_labels = None -depends_on = None - - -def upgrade(): - # Add reasoning fields to production_batches table - op.add_column('production_batches', sa.Column('reasoning', sa.Text(), nullable=True)) - op.add_column('production_batches', sa.Column('reasoning_data', sa.JSON(), nullable=True)) - - -def downgrade(): - # Remove reasoning fields from production_batches table - op.drop_column('production_batches', 'reasoning_data') - op.drop_column('production_batches', 'reasoning')