209 lines
6.7 KiB
Python
209 lines
6.7 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Fix Procurement Data Structure and Add Realistic Price Trends
|
||
|
|
|
||
|
|
Issues to fix:
|
||
|
|
1. Remove nested 'items' arrays from purchase_orders (wrong structure)
|
||
|
|
2. Use existing purchase_order_items table structure at root level
|
||
|
|
3. Add price trends to existing PO items
|
||
|
|
4. Align PO items with actual inventory stock conditions
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
import random
|
||
|
|
|
||
|
|
# Set seed for reproducibility
|
||
|
|
random.seed(42)
|
||
|
|
|
||
|
|
# Price trend data (realistic price movements over time)
|
||
|
|
# These match the 6 ingredients we track in inventory
|
||
|
|
PRICE_TRENDS = {
|
||
|
|
"10000000-0000-0000-0000-000000000001": { # Harina T55
|
||
|
|
"name": "Harina de Trigo T55",
|
||
|
|
"base_price": 0.85,
|
||
|
|
"current_price": 0.92, # +8% over 90 days
|
||
|
|
"trend": 0.08
|
||
|
|
},
|
||
|
|
"10000000-0000-0000-0000-000000000002": { # Harina T65
|
||
|
|
"name": "Harina de Trigo T65",
|
||
|
|
"base_price": 0.95,
|
||
|
|
"current_price": 1.01, # +6%
|
||
|
|
"trend": 0.06
|
||
|
|
},
|
||
|
|
"10000000-0000-0000-0000-000000000011": { # Mantequilla
|
||
|
|
"name": "Mantequilla sin Sal",
|
||
|
|
"base_price": 6.50,
|
||
|
|
"current_price": 7.28, # +12% (highest increase)
|
||
|
|
"trend": 0.12
|
||
|
|
},
|
||
|
|
"10000000-0000-0000-0000-000000000012": { # Leche
|
||
|
|
"name": "Leche Entera Fresca",
|
||
|
|
"base_price": 0.95,
|
||
|
|
"current_price": 0.92, # -3% (seasonal surplus)
|
||
|
|
"trend": -0.03
|
||
|
|
},
|
||
|
|
"10000000-0000-0000-0000-000000000021": { # Levadura
|
||
|
|
"name": "Levadura Fresca",
|
||
|
|
"base_price": 4.20,
|
||
|
|
"current_price": 4.37, # +4%
|
||
|
|
"trend": 0.04
|
||
|
|
},
|
||
|
|
"10000000-0000-0000-0000-000000000032": { # Azúcar
|
||
|
|
"name": "Azúcar Blanco",
|
||
|
|
"base_price": 1.10,
|
||
|
|
"current_price": 1.12, # +2% (stable)
|
||
|
|
"trend": 0.02
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def calculate_price_for_date(ingredient_id: str, days_ago: int) -> float:
|
||
|
|
"""Calculate historical price based on trend"""
|
||
|
|
if ingredient_id not in PRICE_TRENDS:
|
||
|
|
return None
|
||
|
|
|
||
|
|
trend_data = PRICE_TRENDS[ingredient_id]
|
||
|
|
base = trend_data["base_price"]
|
||
|
|
total_trend = trend_data["trend"]
|
||
|
|
|
||
|
|
# Apply trend proportionally
|
||
|
|
# If 90 days trend is +8%, then 45 days ago had +4% from base
|
||
|
|
trend_factor = 1 + (total_trend * (90 - days_ago) / 90)
|
||
|
|
|
||
|
|
# Add small variability (±2%)
|
||
|
|
variability = random.uniform(-0.02, 0.02)
|
||
|
|
|
||
|
|
price = base * trend_factor * (1 + variability)
|
||
|
|
return round(price, 2)
|
||
|
|
|
||
|
|
|
||
|
|
def parse_days_ago(date_str: str) -> int:
|
||
|
|
"""Parse BASE_TS marker to extract days ago"""
|
||
|
|
if not date_str or 'BASE_TS' not in date_str:
|
||
|
|
return 30
|
||
|
|
|
||
|
|
if '- ' in date_str:
|
||
|
|
parts = date_str.split('- ')[1].strip()
|
||
|
|
if 'd' in parts:
|
||
|
|
try:
|
||
|
|
return int(parts.split('d')[0])
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
elif 'h' in parts:
|
||
|
|
return 0 # Same day
|
||
|
|
elif '+ ' in date_str:
|
||
|
|
return 0 # Future order, use current price
|
||
|
|
|
||
|
|
return 0 # BASE_TS alone = today
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
fixture_path = Path(__file__).parent / "07-procurement.json"
|
||
|
|
|
||
|
|
print("🔧 Fixing Procurement Data Structure...")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Load existing data
|
||
|
|
with open(fixture_path, 'r', encoding='utf-8') as f:
|
||
|
|
data = json.load(f)
|
||
|
|
|
||
|
|
purchase_orders = data.get('purchase_orders', [])
|
||
|
|
po_items = data.get('purchase_order_items', [])
|
||
|
|
|
||
|
|
print(f"📦 Found {len(purchase_orders)} purchase orders")
|
||
|
|
print(f"📋 Found {len(po_items)} PO items")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Step 1: Remove nested 'items' arrays from POs (wrong structure)
|
||
|
|
items_removed = 0
|
||
|
|
for po in purchase_orders:
|
||
|
|
if 'items' in po:
|
||
|
|
items_removed += len(po['items'])
|
||
|
|
del po['items']
|
||
|
|
|
||
|
|
if items_removed > 0:
|
||
|
|
print(f"✓ Removed {items_removed} nested items arrays (wrong structure)")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Step 2: Update existing PO items with realistic price trends
|
||
|
|
items_updated = 0
|
||
|
|
|
||
|
|
for item in po_items:
|
||
|
|
ingredient_id = item.get('inventory_product_id')
|
||
|
|
|
||
|
|
if ingredient_id in PRICE_TRENDS:
|
||
|
|
# Find the PO to get order date
|
||
|
|
po_id = item.get('purchase_order_id')
|
||
|
|
po = next((p for p in purchase_orders if p['id'] == po_id), None)
|
||
|
|
|
||
|
|
if po:
|
||
|
|
order_date = po.get('order_date', 'BASE_TS')
|
||
|
|
days_ago = parse_days_ago(order_date)
|
||
|
|
|
||
|
|
# Calculate price for that date
|
||
|
|
historical_price = calculate_price_for_date(ingredient_id, days_ago)
|
||
|
|
|
||
|
|
if historical_price:
|
||
|
|
# Update item with historical price
|
||
|
|
ordered_qty = float(item.get('ordered_quantity', 0))
|
||
|
|
item['unit_price'] = historical_price
|
||
|
|
item['line_total'] = round(ordered_qty * historical_price, 2)
|
||
|
|
|
||
|
|
items_updated += 1
|
||
|
|
|
||
|
|
print(f"✓ Updated {items_updated} PO items with price trends")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Step 3: Recalculate PO totals based on updated items
|
||
|
|
for po in purchase_orders:
|
||
|
|
po_id = po['id']
|
||
|
|
po_items_for_this_po = [item for item in po_items if item.get('purchase_order_id') == po_id]
|
||
|
|
|
||
|
|
if po_items_for_this_po:
|
||
|
|
# Calculate subtotal from items
|
||
|
|
subtotal = sum(float(item.get('line_total', 0)) for item in po_items_for_this_po)
|
||
|
|
tax_rate = 0.21 # 21% IVA in Spain
|
||
|
|
tax = subtotal * tax_rate
|
||
|
|
|
||
|
|
# Keep existing shipping cost or default
|
||
|
|
shipping = float(po.get('shipping_cost', 15.0 if subtotal < 500 else 20.0))
|
||
|
|
discount = float(po.get('discount_amount', 0.0))
|
||
|
|
|
||
|
|
total = subtotal + tax + shipping - discount
|
||
|
|
|
||
|
|
po['subtotal'] = round(subtotal, 2)
|
||
|
|
po['tax_amount'] = round(tax, 2)
|
||
|
|
po['shipping_cost'] = round(shipping, 2)
|
||
|
|
po['discount_amount'] = round(discount, 2)
|
||
|
|
po['total_amount'] = round(total, 2)
|
||
|
|
|
||
|
|
print(f"✓ Recalculated totals for {len(purchase_orders)} purchase orders")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Save fixed data
|
||
|
|
with open(fixture_path, 'w', encoding='utf-8') as f:
|
||
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
|
|
|
||
|
|
print("=" * 60)
|
||
|
|
print("✅ PROCUREMENT STRUCTURE FIXED")
|
||
|
|
print("=" * 60)
|
||
|
|
print()
|
||
|
|
print("🎯 Changes Applied:")
|
||
|
|
print(f" • Removed {items_removed} incorrectly nested items")
|
||
|
|
print(f" • Updated {items_updated} PO items with price trends")
|
||
|
|
print(f" • Recalculated {len(purchase_orders)} PO totals")
|
||
|
|
print()
|
||
|
|
print("📊 Price Trends Applied:")
|
||
|
|
for ing_id, data in PRICE_TRENDS.items():
|
||
|
|
direction = "↑" if data["trend"] > 0 else "↓"
|
||
|
|
print(f" {direction} {data['name']}: {data['trend']*100:+.1f}%")
|
||
|
|
print()
|
||
|
|
print("✅ Data structure now matches PurchaseOrderItem model")
|
||
|
|
print("✅ Price trends enable procurement AI insights")
|
||
|
|
print()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|