refactor: Remove TEXT fields and use only reasoning_data for i18n
Completed the migration to structured reasoning_data for multilingual
dashboard support. Removed hardcoded TEXT fields (reasoning, consequence)
and updated all related code to use JSONB reasoning_data.
Changes:
1. Models Updated (removed TEXT fields):
- PurchaseOrder: Removed reasoning, consequence TEXT columns
- ProductionBatch: Removed reasoning TEXT column
- Both now use only reasoning_data (JSONB/JSON)
2. Dashboard Service Updated:
- Changed to return reasoning_data instead of TEXT fields
- Creates default reasoning_data if missing
- PO actions: reasoning_data with type and parameters
- Production timeline: reasoning_data for each batch
3. Unified Schemas Updated (no separate migration):
- services/procurement/migrations/001_unified_initial_schema.py
- services/production/migrations/001_unified_initial_schema.py
- Removed reasoning/consequence columns from table definitions
- Updated comments to reflect i18n approach
Database Schema:
- purchase_orders: Only reasoning_data (JSONB)
- production_batches: Only reasoning_data (JSON)
Backend now generates:
{
"type": "low_stock_detection",
"parameters": {
"supplier_name": "Harinas del Norte",
"days_until_stockout": 3,
...
},
"consequence": {
"type": "stockout_risk",
"severity": "high"
}
}
Next Steps:
- Frontend: Create i18n translation keys
- Frontend: Update components to translate reasoning_data
- Test multilingual support (ES, EN, CA)
This commit is contained in:
@@ -367,14 +367,23 @@ class DashboardService:
|
|||||||
# Calculate urgency based on required delivery date
|
# Calculate urgency based on required delivery date
|
||||||
urgency = self._calculate_po_urgency(po)
|
urgency = self._calculate_po_urgency(po)
|
||||||
|
|
||||||
|
# Get reasoning_data or create default
|
||||||
|
reasoning_data = po.get("reasoning_data") or {
|
||||||
|
"type": "low_stock_detection",
|
||||||
|
"parameters": {
|
||||||
|
"supplier_name": po.get('supplier_name', 'Unknown'),
|
||||||
|
"product_names": ["Items"],
|
||||||
|
"days_until_stockout": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actions.append({
|
actions.append({
|
||||||
"id": po["id"],
|
"id": po["id"],
|
||||||
"type": ActionType.APPROVE_PO,
|
"type": ActionType.APPROVE_PO,
|
||||||
"urgency": urgency,
|
"urgency": urgency,
|
||||||
"title": f"Purchase Order {po.get('po_number', 'N/A')}",
|
"title": f"Purchase Order {po.get('po_number', 'N/A')}",
|
||||||
"subtitle": f"Supplier: {po.get('supplier_name', 'Unknown')}",
|
"subtitle": f"Supplier: {po.get('supplier_name', 'Unknown')}",
|
||||||
"reasoning": po.get("reasoning") or "Low stock levels detected",
|
"reasoning_data": reasoning_data, # NEW: Structured data for i18n
|
||||||
"consequence": po.get("consequence") or "Order needed to maintain inventory levels",
|
|
||||||
"amount": po.get("total_amount", 0),
|
"amount": po.get("total_amount", 0),
|
||||||
"currency": po.get("currency", "EUR"),
|
"currency": po.get("currency", "EUR"),
|
||||||
"actions": [
|
"actions": [
|
||||||
@@ -490,6 +499,16 @@ class DashboardService:
|
|||||||
status_icon = "⏰"
|
status_icon = "⏰"
|
||||||
status_text = "PENDING"
|
status_text = "PENDING"
|
||||||
|
|
||||||
|
# Get reasoning_data or create default
|
||||||
|
reasoning_data = batch.get("reasoning_data") or {
|
||||||
|
"type": "forecast_demand",
|
||||||
|
"parameters": {
|
||||||
|
"product_name": batch.get("product_name", "Product"),
|
||||||
|
"predicted_demand": batch.get("planned_quantity", 0),
|
||||||
|
"confidence_score": 85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timeline.append({
|
timeline.append({
|
||||||
"id": batch["id"],
|
"id": batch["id"],
|
||||||
"batchNumber": batch.get("batch_number"),
|
"batchNumber": batch.get("batch_number"),
|
||||||
@@ -505,7 +524,7 @@ class DashboardService:
|
|||||||
"progress": progress,
|
"progress": progress,
|
||||||
"readyBy": planned_end.isoformat() if planned_end else None,
|
"readyBy": planned_end.isoformat() if planned_end else None,
|
||||||
"priority": batch.get("priority", "MEDIUM"),
|
"priority": batch.get("priority", "MEDIUM"),
|
||||||
"reasoning": batch.get("reasoning") or "Based on demand forecast"
|
"reasoning_data": reasoning_data # NEW: Structured data for i18n
|
||||||
})
|
})
|
||||||
|
|
||||||
# Sort by planned start time
|
# Sort by planned start time
|
||||||
|
|||||||
@@ -119,17 +119,30 @@ class PurchaseOrder(Base):
|
|||||||
internal_notes = Column(Text, nullable=True) # Not shared with supplier
|
internal_notes = Column(Text, nullable=True) # Not shared with supplier
|
||||||
terms_and_conditions = Column(Text, nullable=True)
|
terms_and_conditions = Column(Text, nullable=True)
|
||||||
|
|
||||||
# JTBD Dashboard: Reasoning and consequences for user transparency
|
# JTBD Dashboard: Structured reasoning data for i18n support
|
||||||
# Deferred loading to prevent breaking queries when columns don't exist yet
|
# Backend stores structured data, frontend translates using i18n
|
||||||
reasoning = deferred(Column(Text, nullable=True)) # Why this PO was created (e.g., "Low flour stock (2 days left)")
|
reasoning_data = Column(JSONB, nullable=True) # Structured reasoning data for multilingual support
|
||||||
consequence = deferred(Column(Text, nullable=True)) # What happens if not approved (e.g., "Stock out risk in 48 hours")
|
# reasoning_data structure (see shared/schemas/reasoning_types.py):
|
||||||
reasoning_data = deferred(Column(JSONB, nullable=True)) # Structured reasoning data
|
# {
|
||||||
# reasoning_data structure: {
|
# "type": "low_stock_detection" | "forecast_demand" | "safety_stock_replenishment" | etc.,
|
||||||
# "trigger": "low_stock" | "forecast_demand" | "manual",
|
# "parameters": {
|
||||||
# "ingredients_affected": [{"id": "uuid", "name": "Flour", "current_stock": 10, "days_remaining": 2}],
|
# "supplier_name": "Harinas del Norte",
|
||||||
# "orders_impacted": [{"id": "uuid", "product": "Baguette", "quantity": 100}],
|
# "product_names": ["Flour Type 55", "Flour Type 45"],
|
||||||
# "urgency_score": 0-100,
|
# "days_until_stockout": 3,
|
||||||
# "estimated_stock_out_date": "2025-11-10T00:00:00Z"
|
# "current_stock": 45.5,
|
||||||
|
# "required_stock": 200
|
||||||
|
# },
|
||||||
|
# "consequence": {
|
||||||
|
# "type": "stockout_risk",
|
||||||
|
# "severity": "high",
|
||||||
|
# "impact_days": 3,
|
||||||
|
# "affected_products": ["Baguette", "Croissant"]
|
||||||
|
# },
|
||||||
|
# "metadata": {
|
||||||
|
# "trigger_source": "orchestrator_auto",
|
||||||
|
# "forecast_confidence": 0.85,
|
||||||
|
# "ai_assisted": true
|
||||||
|
# }
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# Audit fields
|
# Audit fields
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Create Date: 2025-11-07
|
|||||||
|
|
||||||
Complete procurement service schema including:
|
Complete procurement service schema including:
|
||||||
- Procurement plans and requirements
|
- Procurement plans and requirements
|
||||||
- Purchase orders and items (with reasoning fields for JTBD dashboard)
|
- Purchase orders and items (with reasoning_data for i18n JTBD dashboard)
|
||||||
- Deliveries and delivery items
|
- Deliveries and delivery items
|
||||||
- Supplier invoices
|
- Supplier invoices
|
||||||
- Replenishment planning
|
- Replenishment planning
|
||||||
@@ -207,7 +207,7 @@ def upgrade() -> None:
|
|||||||
# PURCHASE ORDER TABLES
|
# PURCHASE ORDER TABLES
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
|
|
||||||
# Create purchase_orders table (with JTBD dashboard reasoning fields)
|
# Create purchase_orders table (with reasoning_data for i18n)
|
||||||
op.create_table('purchase_orders',
|
op.create_table('purchase_orders',
|
||||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False),
|
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
@@ -242,9 +242,7 @@ def upgrade() -> None:
|
|||||||
sa.Column('notes', sa.Text(), nullable=True),
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
sa.Column('internal_notes', sa.Text(), nullable=True),
|
sa.Column('internal_notes', sa.Text(), nullable=True),
|
||||||
sa.Column('terms_and_conditions', sa.Text(), nullable=True),
|
sa.Column('terms_and_conditions', sa.Text(), nullable=True),
|
||||||
# JTBD Dashboard fields
|
# JTBD Dashboard: Structured reasoning for i18n support
|
||||||
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('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('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('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), onupdate=sa.text('now()'), nullable=False),
|
||||||
|
|||||||
@@ -133,16 +133,29 @@ class ProductionBatch(Base):
|
|||||||
delay_reason = Column(String(255), nullable=True)
|
delay_reason = Column(String(255), nullable=True)
|
||||||
cancellation_reason = Column(String(255), nullable=True)
|
cancellation_reason = Column(String(255), nullable=True)
|
||||||
|
|
||||||
# JTBD Dashboard: Reasoning and context for user transparency
|
# JTBD Dashboard: Structured reasoning data for i18n support
|
||||||
# Deferred loading to prevent breaking queries when columns don't exist yet
|
# Backend stores structured data, frontend translates using i18n
|
||||||
reasoning = deferred(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 for multilingual support
|
||||||
reasoning_data = deferred(Column(JSON, nullable=True)) # Structured reasoning data
|
# reasoning_data structure (see shared/schemas/reasoning_types.py):
|
||||||
# reasoning_data structure: {
|
# {
|
||||||
# "trigger": "forecast" | "order" | "inventory" | "manual",
|
# "type": "forecast_demand" | "customer_order" | "stock_replenishment" | etc.,
|
||||||
# "forecast_id": "uuid",
|
# "parameters": {
|
||||||
# "orders_fulfilled": [{"id": "uuid", "customer": "Maria's Bakery", "quantity": 100}],
|
# "product_name": "Croissant",
|
||||||
# "demand_score": 0-100,
|
# "predicted_demand": 500,
|
||||||
# "scheduling_priority_reason": "High demand + VIP customer"
|
# "current_stock": 120,
|
||||||
|
# "production_needed": 380,
|
||||||
|
# "confidence_score": 87
|
||||||
|
# },
|
||||||
|
# "urgency": {
|
||||||
|
# "level": "normal",
|
||||||
|
# "ready_by_time": "08:00",
|
||||||
|
# "customer_commitment": false
|
||||||
|
# },
|
||||||
|
# "metadata": {
|
||||||
|
# "trigger_source": "orchestrator_auto",
|
||||||
|
# "forecast_id": "uuid-here",
|
||||||
|
# "ai_assisted": true
|
||||||
|
# }
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# Timestamps
|
# Timestamps
|
||||||
@@ -191,7 +204,6 @@ class ProductionBatch(Base):
|
|||||||
"quality_notes": self.quality_notes,
|
"quality_notes": self.quality_notes,
|
||||||
"delay_reason": self.delay_reason,
|
"delay_reason": self.delay_reason,
|
||||||
"cancellation_reason": self.cancellation_reason,
|
"cancellation_reason": self.cancellation_reason,
|
||||||
"reasoning": self.reasoning,
|
|
||||||
"reasoning_data": self.reasoning_data,
|
"reasoning_data": self.reasoning_data,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Revises:
|
|||||||
Create Date: 2025-11-07
|
Create Date: 2025-11-07
|
||||||
|
|
||||||
Complete production service schema including:
|
Complete production service schema including:
|
||||||
- Production batches (with reasoning fields for JTBD dashboard and waste tracking)
|
- Production batches (with reasoning_data for i18n JTBD dashboard and waste tracking)
|
||||||
- Production schedules
|
- Production schedules
|
||||||
- Production capacity
|
- Production capacity
|
||||||
- Equipment
|
- Equipment
|
||||||
@@ -90,7 +90,7 @@ def upgrade() -> None:
|
|||||||
)
|
)
|
||||||
op.create_index(op.f('ix_equipment_tenant_id'), 'equipment', ['tenant_id'], unique=False)
|
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)
|
# Create production_batches table (with reasoning_data for i18n and waste tracking)
|
||||||
op.create_table('production_batches',
|
op.create_table('production_batches',
|
||||||
sa.Column('id', sa.UUID(), nullable=False),
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
sa.Column('tenant_id', sa.UUID(), nullable=False),
|
sa.Column('tenant_id', sa.UUID(), nullable=False),
|
||||||
@@ -135,8 +135,7 @@ def upgrade() -> None:
|
|||||||
sa.Column('quality_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('delay_reason', sa.String(length=255), nullable=True),
|
||||||
sa.Column('cancellation_reason', sa.String(length=255), nullable=True),
|
sa.Column('cancellation_reason', sa.String(length=255), nullable=True),
|
||||||
# JTBD Dashboard fields (from 20251107 migration)
|
# JTBD Dashboard: Structured reasoning for i18n support
|
||||||
sa.Column('reasoning', sa.Text(), nullable=True),
|
|
||||||
sa.Column('reasoning_data', sa.JSON(), 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('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('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
|||||||
Reference in New Issue
Block a user