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.
This commit is contained in:
@@ -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}],
|
||||
|
||||
@@ -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")
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user