Merge pull request #24 from ualsweb/claude/debug-persistent-issue-016REMFcUHKD3YWc2n3TK8wf
Fix template variable interpolation by creating params copy
This commit is contained in:
188
scripts/clean_old_dashboard_data.py
Normal file
188
scripts/clean_old_dashboard_data.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Utility script to clean old purchase orders and production batches with malformed reasoning_data.
|
||||||
|
|
||||||
|
This script deletes pending purchase orders and production batches that were created before
|
||||||
|
the fix for template variable interpolation. After running this script, trigger a new
|
||||||
|
orchestration run to create fresh data with properly interpolated variables.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/clean_old_dashboard_data.py --tenant-id <tenant_id>
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Add services to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../services/procurement/app'))
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../services/production/app'))
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../shared'))
|
||||||
|
|
||||||
|
|
||||||
|
async def clean_old_purchase_orders(tenant_id: str, dry_run: bool = True):
|
||||||
|
"""Clean old pending purchase orders"""
|
||||||
|
try:
|
||||||
|
from app.core.database import AsyncSessionLocal
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
# Get count of pending POs
|
||||||
|
count_result = await session.execute(
|
||||||
|
text("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM purchase_orders
|
||||||
|
WHERE tenant_id = :tenant_id
|
||||||
|
AND status = 'pending_approval'
|
||||||
|
"""),
|
||||||
|
{"tenant_id": tenant_id}
|
||||||
|
)
|
||||||
|
count = count_result.scalar()
|
||||||
|
|
||||||
|
print(f"Found {count} pending purchase orders for tenant {tenant_id}")
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
print("No purchase orders to clean.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f"DRY RUN: Would delete {count} pending purchase orders")
|
||||||
|
return count
|
||||||
|
|
||||||
|
# Delete pending POs
|
||||||
|
result = await session.execute(
|
||||||
|
text("""
|
||||||
|
DELETE FROM purchase_orders
|
||||||
|
WHERE tenant_id = :tenant_id
|
||||||
|
AND status = 'pending_approval'
|
||||||
|
"""),
|
||||||
|
{"tenant_id": tenant_id}
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
deleted = result.rowcount
|
||||||
|
print(f"✓ Deleted {deleted} pending purchase orders")
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning purchase orders: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
async def clean_old_production_batches(tenant_id: str, dry_run: bool = True):
|
||||||
|
"""Clean old pending production batches"""
|
||||||
|
try:
|
||||||
|
# Import production service dependencies
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../services/production/app'))
|
||||||
|
from app.core.database import AsyncSessionLocal as ProductionSession
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
async with ProductionSession() as session:
|
||||||
|
# Get count of pending/scheduled batches
|
||||||
|
count_result = await session.execute(
|
||||||
|
text("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM production_batches
|
||||||
|
WHERE tenant_id = :tenant_id
|
||||||
|
AND status IN ('pending', 'scheduled')
|
||||||
|
"""),
|
||||||
|
{"tenant_id": tenant_id}
|
||||||
|
)
|
||||||
|
count = count_result.scalar()
|
||||||
|
|
||||||
|
print(f"Found {count} pending/scheduled production batches for tenant {tenant_id}")
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
print("No production batches to clean.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f"DRY RUN: Would delete {count} pending/scheduled batches")
|
||||||
|
return count
|
||||||
|
|
||||||
|
# Delete pending/scheduled batches
|
||||||
|
result = await session.execute(
|
||||||
|
text("""
|
||||||
|
DELETE FROM production_batches
|
||||||
|
WHERE tenant_id = :tenant_id
|
||||||
|
AND status IN ('pending', 'scheduled')
|
||||||
|
"""),
|
||||||
|
{"tenant_id": tenant_id}
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
deleted = result.rowcount
|
||||||
|
print(f"✓ Deleted {deleted} pending/scheduled production batches")
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning production batches: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Clean old dashboard data (POs and batches) with malformed reasoning_data'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--tenant-id',
|
||||||
|
required=True,
|
||||||
|
help='Tenant ID to clean data for'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--execute',
|
||||||
|
action='store_true',
|
||||||
|
help='Actually delete data (default is dry run)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
dry_run = not args.execute
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Dashboard Data Cleanup Utility")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Tenant ID: {args.tenant_id}")
|
||||||
|
print(f"Mode: {'DRY RUN (no changes will be made)' if dry_run else 'EXECUTE (will delete data)'}")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
print("⚠️ WARNING: This will permanently delete pending purchase orders and production batches!")
|
||||||
|
print(" After deletion, you should trigger a new orchestration run to create fresh data.")
|
||||||
|
response = input(" Are you sure you want to continue? (yes/no): ")
|
||||||
|
if response.lower() != 'yes':
|
||||||
|
print("Aborted.")
|
||||||
|
return
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Clean purchase orders
|
||||||
|
print("1. Cleaning Purchase Orders...")
|
||||||
|
po_count = await clean_old_purchase_orders(args.tenant_id, dry_run)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Clean production batches
|
||||||
|
print("2. Cleaning Production Batches...")
|
||||||
|
batch_count = await clean_old_production_batches(args.tenant_id, dry_run)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Summary:")
|
||||||
|
print(f" Purchase Orders: {po_count} {'would be' if dry_run else ''} deleted")
|
||||||
|
print(f" Production Batches: {batch_count} {'would be' if dry_run else ''} deleted")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print("\nTo actually delete the data, run with --execute flag:")
|
||||||
|
print(f" python scripts/clean_old_dashboard_data.py --tenant-id {args.tenant_id} --execute")
|
||||||
|
else:
|
||||||
|
print("\n✓ Data cleaned successfully!")
|
||||||
|
print("\nNext steps:")
|
||||||
|
print(" 1. Restart the orchestrator service")
|
||||||
|
print(" 2. Trigger a new orchestration run from the dashboard")
|
||||||
|
print(" 3. The new POs and batches will have properly interpolated variables")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
@@ -404,8 +404,8 @@ class DashboardService:
|
|||||||
reasoning_type = reasoning_data.get('type', 'inventory_replenishment')
|
reasoning_type = reasoning_data.get('type', 'inventory_replenishment')
|
||||||
reasoning_type_i18n_key = self._get_reasoning_type_i18n_key(reasoning_type, context="purchaseOrder")
|
reasoning_type_i18n_key = self._get_reasoning_type_i18n_key(reasoning_type, context="purchaseOrder")
|
||||||
|
|
||||||
# Preprocess parameters for i18n
|
# Preprocess parameters for i18n - MUST create a copy to avoid modifying immutable database objects
|
||||||
params = reasoning_data.get('parameters', {})
|
params = dict(reasoning_data.get('parameters', {}))
|
||||||
# Convert product_names array to product_names_joined string
|
# Convert product_names array to product_names_joined string
|
||||||
if 'product_names' in params and isinstance(params['product_names'], list):
|
if 'product_names' in params and isinstance(params['product_names'], list):
|
||||||
params['product_names_joined'] = ', '.join(params['product_names'])
|
params['product_names_joined'] = ', '.join(params['product_names'])
|
||||||
@@ -629,7 +629,7 @@ class DashboardService:
|
|||||||
"reasoning_data": reasoning_data, # Structured data for i18n
|
"reasoning_data": reasoning_data, # Structured data for i18n
|
||||||
"reasoning_i18n": {
|
"reasoning_i18n": {
|
||||||
"key": reasoning_type_i18n_key,
|
"key": reasoning_type_i18n_key,
|
||||||
"params": reasoning_data.get('parameters', {})
|
"params": dict(reasoning_data.get('parameters', {})) # Create a copy to avoid immutable object issues
|
||||||
},
|
},
|
||||||
"status_i18n": status_i18n # i18n for status text
|
"status_i18n": status_i18n # i18n for status text
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user