""" Seed Suppliers and Offers for African cocoa belt. Creates offers via Temporal workflow so they sync to the graph. """ import random import uuid from decimal import Decimal import requests from django.core.management.base import BaseCommand from django.db import transaction from offers.models import Offer from offers.services import OfferService, OfferData from suppliers.models import SupplierProfile # African cocoa belt countries AFRICAN_COUNTRIES = [ ("Côte d'Ivoire", "CI", 6.8276, -5.2893), # Abidjan ("Ghana", "GH", 5.6037, -0.1870), # Accra ("Nigeria", "NG", 6.5244, 3.3792), # Lagos ("Cameroon", "CM", 4.0511, 9.7679), # Douala ("Togo", "TG", 6.1725, 1.2314), # Lomé ] # Products will be fetched from Odoo at runtime # Format: (name, category, uuid) - populated by _fetch_products_from_odoo() class Command(BaseCommand): help = "Seed Suppliers and Offers for African cocoa belt with workflow sync" def add_arguments(self, parser): parser.add_argument( "--suppliers", type=int, default=10, help="How many suppliers to create (default: 10)", ) parser.add_argument( "--offers", type=int, default=50, help="How many offers to create (default: 50)", ) parser.add_argument( "--clear", action="store_true", help="Delete all existing suppliers and offers before seeding", ) parser.add_argument( "--no-workflow", action="store_true", help="Create offers directly in DB without workflow (no graph sync)", ) parser.add_argument( "--geo-url", type=str, default="http://geo:8000/graphql/public/", help="Geo service GraphQL URL (default: http://geo:8000/graphql/public/)", ) parser.add_argument( "--odoo-url", type=str, default="http://odoo:8069", help="Odoo URL (default: http://odoo:8069)", ) parser.add_argument( "--product", type=str, default=None, help="Filter offers by product name (e.g., 'Cocoa Beans')", ) def handle(self, *args, **options): if options["clear"]: with transaction.atomic(): offers_deleted, _ = Offer.objects.all().delete() suppliers_deleted, _ = SupplierProfile.objects.all().delete() self.stdout.write(self.style.WARNING( f"Deleted {suppliers_deleted} supplier profiles and {offers_deleted} offers" )) suppliers_count = max(0, options["suppliers"]) offers_count = max(0, options["offers"]) use_workflow = not options["no_workflow"] geo_url = options["geo_url"] odoo_url = options["odoo_url"] product_filter = options["product"] # Fetch products from Odoo self.stdout.write("Fetching products from Odoo...") products = self._fetch_products_from_odoo(odoo_url) if not products: self.stdout.write(self.style.ERROR("No products found in Odoo. Cannot create offers.")) return self.stdout.write(f"Found {len(products)} products") # Filter by product name if specified if product_filter: products = [p for p in products if product_filter.lower() in p[0].lower()] if not products: self.stdout.write(self.style.ERROR(f"No products matching '{product_filter}' found.")) return self.stdout.write(f"Filtered to {len(products)} products matching '{product_filter}'") # Fetch African hubs from geo service self.stdout.write("Fetching African hubs from geo service...") hubs = self._fetch_african_hubs(geo_url) if not hubs: self.stdout.write(self.style.WARNING( "No African hubs found. Using default locations." )) hubs = self._default_african_hubs() self.stdout.write(f"Found {len(hubs)} African hubs") # Create suppliers self.stdout.write(f"Creating {suppliers_count} suppliers...") new_suppliers = self._create_suppliers(suppliers_count, hubs) self.stdout.write(self.style.SUCCESS(f"Created {len(new_suppliers)} suppliers")) # Create offers self.stdout.write(f"Creating {offers_count} offers (workflow={use_workflow})...") if use_workflow: created_offers = self._create_offers_via_workflow(offers_count, hubs, products) else: created_offers = self._create_offers_direct(offers_count, hubs, products) self.stdout.write(self.style.SUCCESS(f"Created {len(created_offers)} offers")) def _fetch_products_from_odoo(self, odoo_url: str) -> list: """Fetch products from Odoo via JSON-RPC""" products = [] try: # Search for products response = requests.post( f"{odoo_url}/jsonrpc", json={ "jsonrpc": "2.0", "method": "call", "params": { "service": "object", "method": "execute_kw", "args": [ "odoo", # database 2, # uid (admin) "admin", # password "products.product", # model "search_read", [[]], # domain (all products) {"fields": ["uuid", "name", "category_id"]}, ], }, "id": 1, }, timeout=10, ) if response.status_code == 200: data = response.json() result = data.get("result", []) for p in result: category_name = p.get("category_id", [None, "Agriculture"])[1] if p.get("category_id") else "Agriculture" products.append((p["name"], category_name, p["uuid"])) except Exception as e: self.stdout.write(self.style.WARNING(f"Failed to fetch products from Odoo: {e}")) return products def _fetch_african_hubs(self, geo_url: str) -> list: """Fetch African hubs from geo service via GraphQL. Gets all nodes and filters by African countries in Python since the GraphQL schema doesn't support country filter. """ african_countries = { "Côte d'Ivoire", "Ivory Coast", "Ghana", "Nigeria", "Cameroon", "Togo", "Senegal", "Mali", "Burkina Faso", "Guinea", "Benin", "Niger", "Sierra Leone", "Liberia", } query = """ query GetNodes($limit: Int) { nodes(limit: $limit) { uuid name country countryCode latitude longitude } } """ try: response = requests.post( geo_url, json={"query": query, "variables": {"limit": 5000}}, timeout=30, ) if response.status_code == 200: data = response.json() if "errors" in data: self.stdout.write(self.style.WARNING(f"GraphQL errors: {data['errors']}")) return [] nodes = data.get("data", {}).get("nodes", []) # Filter by African countries african_hubs = [ n for n in nodes if n.get("country") in african_countries ] return african_hubs except Exception as e: self.stdout.write(self.style.WARNING(f"Failed to fetch hubs: {e}")) return [] def _default_african_hubs(self) -> list: """Default African hubs if geo service is unavailable""" return [ { "uuid": str(uuid.uuid4()), "name": "Port of Abidjan", "country": "Côte d'Ivoire", "countryCode": "CI", "latitude": 5.3167, "longitude": -4.0167, }, { "uuid": str(uuid.uuid4()), "name": "Port of San Pedro", "country": "Côte d'Ivoire", "countryCode": "CI", "latitude": 4.7500, "longitude": -6.6333, }, { "uuid": str(uuid.uuid4()), "name": "Port of Tema", "country": "Ghana", "countryCode": "GH", "latitude": 5.6333, "longitude": -0.0167, }, { "uuid": str(uuid.uuid4()), "name": "Port of Takoradi", "country": "Ghana", "countryCode": "GH", "latitude": 4.8833, "longitude": -1.7500, }, { "uuid": str(uuid.uuid4()), "name": "Port of Lagos", "country": "Nigeria", "countryCode": "NG", "latitude": 6.4531, "longitude": 3.3958, }, { "uuid": str(uuid.uuid4()), "name": "Port of Douala", "country": "Cameroon", "countryCode": "CM", "latitude": 4.0483, "longitude": 9.7043, }, { "uuid": str(uuid.uuid4()), "name": "Port of Lomé", "country": "Togo", "countryCode": "TG", "latitude": 6.1375, "longitude": 1.2125, }, ] def _create_suppliers(self, count: int, hubs: list) -> list: """Create supplier profiles in African countries""" created = [] for idx in range(count): hub = random.choice(hubs) if hubs else None country, country_code = self._get_random_african_country() # Use hub coordinates if available, otherwise use country defaults if hub: lat = hub["latitude"] + random.uniform(-0.5, 0.5) lng = hub["longitude"] + random.uniform(-0.5, 0.5) else: lat, lng = self._get_country_coords(country) lat += random.uniform(-0.5, 0.5) lng += random.uniform(-0.5, 0.5) profile = SupplierProfile.objects.create( uuid=str(uuid.uuid4()), team_uuid=str(uuid.uuid4()), name=f"Cocoa Supplier {idx + 1} ({country})", description=f"Premium cocoa supplier from {country}. Specializing in high-quality cocoa beans.", country=country, country_code=country_code, logo_url="", latitude=lat, longitude=lng, is_verified=random.choice([True, True, False]), # 66% verified is_active=True, ) created.append(profile) return created def _create_offers_via_workflow(self, count: int, hubs: list, products: list) -> list: """Create offers via Temporal workflow (syncs to graph)""" created = [] suppliers = list(SupplierProfile.objects.all()) if not suppliers: self.stdout.write(self.style.ERROR("No suppliers found. Create suppliers first.")) return created for idx in range(count): supplier = random.choice(suppliers) hub = random.choice(hubs) product_name, category_name, product_uuid = random.choice(products) data = OfferData( team_uuid=supplier.team_uuid, product_uuid=product_uuid, product_name=product_name, category_name=category_name, location_uuid=hub["uuid"], location_name=hub["name"], location_country=hub["country"], location_country_code=hub.get("countryCode", ""), location_latitude=hub["latitude"], location_longitude=hub["longitude"], quantity=self._rand_decimal(10, 500, 2), unit="ton", price_per_unit=self._rand_decimal(2000, 4000, 2), # Cocoa ~$2000-4000/ton currency="USD", description=f"High quality {product_name} from {hub['country']}", ) try: offer_uuid, workflow_id, _ = OfferService.create_offer_via_workflow(data) self.stdout.write(f" [{idx+1}/{count}] Created offer {offer_uuid[:8]}... workflow: {workflow_id}") created.append(offer_uuid) except Exception as e: self.stdout.write(self.style.ERROR(f" [{idx+1}/{count}] Failed: {e}")) return created def _create_offers_direct(self, count: int, hubs: list, products: list) -> list: """Create offers directly in DB (no workflow, no graph sync)""" created = [] suppliers = list(SupplierProfile.objects.all()) if not suppliers: self.stdout.write(self.style.ERROR("No suppliers found. Create suppliers first.")) return created for idx in range(count): supplier = random.choice(suppliers) hub = random.choice(hubs) product_name, category_name, product_uuid = random.choice(products) offer = Offer.objects.create( uuid=str(uuid.uuid4()), team_uuid=supplier.team_uuid, status="active", workflow_status="pending", location_uuid=hub["uuid"], location_name=hub["name"], location_country=hub["country"], location_country_code=hub.get("countryCode", ""), location_latitude=hub["latitude"], location_longitude=hub["longitude"], product_uuid=product_uuid, product_name=product_name, category_name=category_name, quantity=self._rand_decimal(10, 500, 2), unit="ton", price_per_unit=self._rand_decimal(2000, 4000, 2), currency="USD", description=f"High quality {product_name} from {hub['country']}", ) created.append(offer) return created def _get_random_african_country(self) -> tuple: """Get random African country name and code""" country, code, _, _ = random.choice(AFRICAN_COUNTRIES) return country, code def _get_country_coords(self, country: str) -> tuple: """Get default coordinates for a country""" for name, code, lat, lng in AFRICAN_COUNTRIES: if name == country: return lat, lng return 6.0, 0.0 # Default: Gulf of Guinea def _rand_decimal(self, low: int, high: int, places: int) -> Decimal: value = random.uniform(low, high) quantize_str = "1." + "0" * places return Decimal(str(value)).quantize(Decimal(quantize_str))