Improve demo seed
This commit is contained in:
@@ -1,229 +1,281 @@
|
||||
{
|
||||
"clientes": [
|
||||
{
|
||||
"customer_name": "Cafetería El Rincón",
|
||||
"customer_type": "retail",
|
||||
"business_name": "El Rincón Cafetería S.L.",
|
||||
"contact_person": "Ana Rodríguez García",
|
||||
"email": "pedidos@cafeteriaelrincon.es",
|
||||
"phone": "+34 963 456 789",
|
||||
"address": "Calle Mayor, 78, 46001 Valencia",
|
||||
"payment_terms": "net_7",
|
||||
"discount_percentage": 15.0,
|
||||
"id": "20000000-0000-0000-0000-000000000001",
|
||||
"customer_code": "CLI-001",
|
||||
"name": "Hotel Plaza Mayor",
|
||||
"business_name": "Hotel Plaza Mayor S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "compras@hotelplazamayor.es",
|
||||
"phone": "+34 91 234 5601",
|
||||
"address_line1": "Plaza Mayor 15",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28012",
|
||||
"country": "España",
|
||||
"customer_segment": "wholesale",
|
||||
"priority_level": "high",
|
||||
"payment_terms": "net_30",
|
||||
"credit_limit": 5000.00,
|
||||
"discount_percentage": 10.00,
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Entrega antes de las 6:00 AM. Llamar al llegar."
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000002",
|
||||
"customer_code": "CLI-002",
|
||||
"name": "Restaurante El Mesón",
|
||||
"business_name": "Restaurante El Mesón S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "pedidos@elmeson.es",
|
||||
"phone": "+34 91 345 6702",
|
||||
"address_line1": "Calle Mayor 45",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28013",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"priority_level": "normal",
|
||||
"payment_terms": "net_15",
|
||||
"credit_limit": 2000.00,
|
||||
"is_active": true,
|
||||
"notes": "Cliente diario. Entrega preferente 6:00-7:00 AM.",
|
||||
"tags": ["hosteleria", "cafeteria", "diario"]
|
||||
"discount_percentage": 5.00,
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Dejar pedido en la puerta de servicio."
|
||||
},
|
||||
{
|
||||
"customer_name": "Supermercado La Bodega",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Supermercados La Bodega S.L.",
|
||||
"contact_person": "Carlos Jiménez Moreno",
|
||||
"email": "compras@superlabodega.com",
|
||||
"phone": "+34 965 789 012",
|
||||
"address": "Avenida del Mediterráneo, 156, 03500 Benidorm, Alicante",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 20.0,
|
||||
"credit_limit": 5000.00,
|
||||
"is_active": true,
|
||||
"notes": "Entrega 3 veces/semana: Lunes, Miércoles, Viernes. Horario: 5:00-6:00 AM.",
|
||||
"tags": ["retail", "supermercado", "mayorista"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Restaurante Casa Pepe",
|
||||
"customer_type": "retail",
|
||||
"business_name": "Casa Pepe Restauración S.C.",
|
||||
"contact_person": "José Luis Pérez",
|
||||
"email": "pedidos@casapepe.es",
|
||||
"phone": "+34 961 234 567",
|
||||
"address": "Plaza del Mercado, 12, 46003 Valencia",
|
||||
"payment_terms": "net_15",
|
||||
"discount_percentage": 12.0,
|
||||
"credit_limit": 1500.00,
|
||||
"is_active": true,
|
||||
"notes": "Especializado en cocina mediterránea. Requiere panes especiales.",
|
||||
"tags": ["hosteleria", "restaurante"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Hotel Playa Sol",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Hoteles Costa Blanca S.A.",
|
||||
"contact_person": "María Carmen López",
|
||||
"email": "compras@hotelplayasol.com",
|
||||
"phone": "+34 965 123 456",
|
||||
"address": "Paseo Marítimo, 234, 03501 Benidorm, Alicante",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 18.0,
|
||||
"credit_limit": 8000.00,
|
||||
"is_active": true,
|
||||
"notes": "Hotel 4 estrellas. Pedidos grandes para desayuno buffet. Volumen estable todo el año.",
|
||||
"tags": ["hosteleria", "hotel", "mayorista", "alto_volumen"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Bar Los Naranjos",
|
||||
"customer_type": "retail",
|
||||
"business_name": "Los Naranjos C.B.",
|
||||
"contact_person": "Francisco Martínez",
|
||||
"email": "losnaranjos@gmail.com",
|
||||
"phone": "+34 963 789 012",
|
||||
"address": "Calle de la Paz, 45, 46002 Valencia",
|
||||
"payment_terms": "net_7",
|
||||
"discount_percentage": 10.0,
|
||||
"credit_limit": 800.00,
|
||||
"is_active": true,
|
||||
"notes": "Bar de barrio. Pedidos pequeños diarios.",
|
||||
"tags": ["hosteleria", "bar", "pequeño"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Panadería La Tahona",
|
||||
"customer_type": "retail",
|
||||
"business_name": "Panadería La Tahona",
|
||||
"contact_person": "Isabel García Ruiz",
|
||||
"email": "latahona@hotmail.com",
|
||||
"phone": "+34 962 345 678",
|
||||
"address": "Avenida de los Naranjos, 89, 46470 Albal, Valencia",
|
||||
"payment_terms": "net_15",
|
||||
"discount_percentage": 25.0,
|
||||
"credit_limit": 3000.00,
|
||||
"is_active": true,
|
||||
"notes": "Panadería que no tiene obrador propio. Compra productos semipreparados.",
|
||||
"tags": ["panaderia", "b2b"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Catering García e Hijos",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "García Catering S.L.",
|
||||
"contact_person": "Miguel García Sánchez",
|
||||
"email": "pedidos@cateringgarcia.es",
|
||||
"phone": "+34 963 567 890",
|
||||
"address": "Polígono Industrial Vara de Quart, Nave 34, 46014 Valencia",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 22.0,
|
||||
"credit_limit": 6000.00,
|
||||
"is_active": true,
|
||||
"notes": "Catering para eventos. Pedidos variables según calendario de eventos.",
|
||||
"tags": ["catering", "eventos", "variable"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Residencia Tercera Edad San Antonio",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Residencia San Antonio",
|
||||
"contact_person": "Lucía Fernández",
|
||||
"email": "compras@residenciasanantonio.es",
|
||||
"phone": "+34 961 890 123",
|
||||
"address": "Calle San Antonio, 156, 46013 Valencia",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 15.0,
|
||||
"credit_limit": 4000.00,
|
||||
"is_active": true,
|
||||
"notes": "Residencia con 120 plazas. Pedidos regulares y previsibles.",
|
||||
"tags": ["institucional", "residencia", "estable"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Colegio Santa Teresa",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Cooperativa Colegio Santa Teresa",
|
||||
"contact_person": "Carmen Navarro",
|
||||
"email": "cocina@colegiosantateresa.es",
|
||||
"phone": "+34 963 012 345",
|
||||
"address": "Avenida de la Constitución, 234, 46008 Valencia",
|
||||
"payment_terms": "net_45",
|
||||
"discount_percentage": 18.0,
|
||||
"credit_limit": 5000.00,
|
||||
"is_active": true,
|
||||
"notes": "Colegio con 800 alumnos. Pedidos de septiembre a junio (calendario escolar).",
|
||||
"tags": ["institucional", "colegio", "estacional"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Mercado Central - Puesto 23",
|
||||
"customer_type": "retail",
|
||||
"business_name": "Antonio Sánchez - Mercado Central",
|
||||
"contact_person": "Antonio Sánchez",
|
||||
"email": "antoniosanchez.mercado@gmail.com",
|
||||
"phone": "+34 963 456 012",
|
||||
"address": "Mercado Central, Puesto 23, 46001 Valencia",
|
||||
"payment_terms": "net_7",
|
||||
"discount_percentage": 8.0,
|
||||
"id": "20000000-0000-0000-0000-000000000003",
|
||||
"customer_code": "CLI-003",
|
||||
"name": "Cafetería La Esquina",
|
||||
"business_name": "Cafetería La Esquina S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "info@laesquina.es",
|
||||
"phone": "+34 91 456 7803",
|
||||
"address_line1": "Calle Toledo 23",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28005",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"priority_level": "normal",
|
||||
"payment_terms": "immediate",
|
||||
"credit_limit": 1000.00,
|
||||
"is_active": true,
|
||||
"notes": "Puesto de venta en el mercado central. Compra para revender.",
|
||||
"tags": ["mercado", "revendedor", "pequeño"]
|
||||
"discount_percentage": 0.00,
|
||||
"preferred_delivery_method": "delivery"
|
||||
},
|
||||
{
|
||||
"customer_name": "Cafetería Universidad Politécnica",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Servicios Universitarios UPV",
|
||||
"contact_person": "Roberto Martín",
|
||||
"email": "cafeteria@upv.es",
|
||||
"phone": "+34 963 789 456",
|
||||
"address": "Campus de Vera, Edificio 4N, 46022 Valencia",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 20.0,
|
||||
"credit_limit": 7000.00,
|
||||
"is_active": true,
|
||||
"notes": "Cafetería universitaria. Alto volumen durante curso académico. Cierra en verano.",
|
||||
"tags": ["institucional", "universidad", "estacional", "alto_volumen"]
|
||||
"id": "20000000-0000-0000-0000-000000000004",
|
||||
"customer_code": "CLI-004",
|
||||
"name": "María García Ruiz",
|
||||
"customer_type": "individual",
|
||||
"email": "maria.garcia@email.com",
|
||||
"phone": "+34 612 345 678",
|
||||
"address_line1": "Calle Alcalá 100, 3º B",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28009",
|
||||
"country": "España",
|
||||
"customer_segment": "vip",
|
||||
"priority_level": "high",
|
||||
"payment_terms": "immediate",
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Cliente VIP - Tartas de cumpleaños personalizadas"
|
||||
},
|
||||
{
|
||||
"customer_name": "Panadería El Horno de Oro",
|
||||
"customer_type": "retail",
|
||||
"business_name": "El Horno de Oro S.C.",
|
||||
"contact_person": "Manuel Jiménez",
|
||||
"email": "hornodeoro@telefonica.net",
|
||||
"phone": "+34 965 234 567",
|
||||
"address": "Calle del Cid, 67, 03400 Villena, Alicante",
|
||||
"id": "20000000-0000-0000-0000-000000000005",
|
||||
"customer_code": "CLI-005",
|
||||
"name": "Carlos Martínez López",
|
||||
"customer_type": "individual",
|
||||
"email": "carlos.m@email.com",
|
||||
"phone": "+34 623 456 789",
|
||||
"address_line1": "Gran Vía 75, 5º A",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28013",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"priority_level": "normal",
|
||||
"payment_terms": "immediate",
|
||||
"preferred_delivery_method": "pickup"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000006",
|
||||
"customer_code": "CLI-006",
|
||||
"name": "Panadería Central Distribución",
|
||||
"business_name": "Panadería Central S.A.",
|
||||
"customer_type": "central_bakery",
|
||||
"email": "produccion@panaderiacentral.es",
|
||||
"phone": "+34 91 567 8904",
|
||||
"address_line1": "Polígono Industrial Norte, Nave 12",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28050",
|
||||
"country": "España",
|
||||
"customer_segment": "wholesale",
|
||||
"priority_level": "high",
|
||||
"payment_terms": "net_15",
|
||||
"discount_percentage": 25.0,
|
||||
"credit_limit": 2500.00,
|
||||
"is_active": true,
|
||||
"notes": "Panadería tradicional. Compra productos especializados que no produce.",
|
||||
"tags": ["panaderia", "b2b", "especializado"]
|
||||
"credit_limit": 10000.00,
|
||||
"discount_percentage": 15.00,
|
||||
"preferred_delivery_method": "pickup",
|
||||
"special_instructions": "Pedidos grandes - Coordinación con almacén necesaria"
|
||||
},
|
||||
{
|
||||
"customer_name": "Bar Cafetería La Plaza",
|
||||
"customer_type": "retail",
|
||||
"business_name": "La Plaza Hostelería",
|
||||
"contact_person": "Teresa López",
|
||||
"email": "barlaplaza@hotmail.com",
|
||||
"phone": "+34 962 567 890",
|
||||
"address": "Plaza Mayor, 3, 46470 Catarroja, Valencia",
|
||||
"payment_terms": "net_7",
|
||||
"discount_percentage": 12.0,
|
||||
"credit_limit": 1200.00,
|
||||
"is_active": true,
|
||||
"notes": "Bar de pueblo con clientela local. Pedidos regulares de lunes a sábado.",
|
||||
"tags": ["hosteleria", "bar", "regular"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Supermercado Eco Verde",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Eco Verde Distribución S.L.",
|
||||
"contact_person": "Laura Sánchez",
|
||||
"email": "compras@ecoverde.es",
|
||||
"phone": "+34 963 890 123",
|
||||
"address": "Calle Colón, 178, 46004 Valencia",
|
||||
"id": "20000000-0000-0000-0000-000000000007",
|
||||
"customer_code": "CLI-007",
|
||||
"name": "Supermercado El Ahorro",
|
||||
"business_name": "Supermercado El Ahorro S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "compras@elahorro.es",
|
||||
"phone": "+34 91 678 9015",
|
||||
"address_line1": "Avenida de América 200",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28028",
|
||||
"country": "España",
|
||||
"customer_segment": "wholesale",
|
||||
"priority_level": "high",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 18.0,
|
||||
"credit_limit": 4500.00,
|
||||
"is_active": true,
|
||||
"notes": "Supermercado especializado en productos ecológicos. Interesados en panes artesanales.",
|
||||
"tags": ["retail", "supermercado", "ecologico", "premium"]
|
||||
"credit_limit": 8000.00,
|
||||
"discount_percentage": 12.00,
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Entrega en muelle de carga. Horario: 7:00-9:00 AM"
|
||||
},
|
||||
{
|
||||
"customer_name": "Restaurante La Alquería",
|
||||
"customer_type": "retail",
|
||||
"business_name": "La Alquería Grupo Gastronómico",
|
||||
"contact_person": "Javier Moreno",
|
||||
"email": "jefe.cocina@laalqueria.es",
|
||||
"phone": "+34 961 456 789",
|
||||
"address": "Camino de Vera, 45, 46022 Valencia",
|
||||
"id": "20000000-0000-0000-0000-000000000008",
|
||||
"customer_code": "CLI-008",
|
||||
"name": "Ana Rodríguez Fernández",
|
||||
"customer_type": "individual",
|
||||
"email": "ana.rodriguez@email.com",
|
||||
"phone": "+34 634 567 890",
|
||||
"address_line1": "Calle Serrano 50, 2º D",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28001",
|
||||
"country": "España",
|
||||
"customer_segment": "vip",
|
||||
"priority_level": "high",
|
||||
"payment_terms": "immediate",
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Prefiere croissants de mantequilla y pan integral"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000009",
|
||||
"customer_code": "CLI-009",
|
||||
"name": "Colegio San José",
|
||||
"business_name": "Colegio San José - Comedor Escolar",
|
||||
"customer_type": "business",
|
||||
"email": "administracion@colegiosanjose.es",
|
||||
"phone": "+34 91 789 0126",
|
||||
"address_line1": "Calle Bravo Murillo 150",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28020",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"priority_level": "normal",
|
||||
"payment_terms": "net_30",
|
||||
"credit_limit": 3000.00,
|
||||
"discount_percentage": 8.00,
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Entrega diaria a las 7:30 AM. 500 alumnos."
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000010",
|
||||
"customer_code": "CLI-010",
|
||||
"name": "Javier López Sánchez",
|
||||
"customer_type": "individual",
|
||||
"email": "javier.lopez@email.com",
|
||||
"phone": "+34 645 678 901",
|
||||
"address_line1": "Calle Atocha 25, 1º C",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28012",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"priority_level": "normal",
|
||||
"payment_terms": "immediate",
|
||||
"preferred_delivery_method": "pickup"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000011",
|
||||
"customer_code": "CLI-011",
|
||||
"name": "Cafetería Central Station",
|
||||
"business_name": "Central Station Coffee S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "pedidos@centralstation.es",
|
||||
"phone": "+34 91 890 1237",
|
||||
"address_line1": "Estación de Atocha, Local 23",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28045",
|
||||
"country": "España",
|
||||
"customer_segment": "wholesale",
|
||||
"priority_level": "high",
|
||||
"payment_terms": "net_15",
|
||||
"discount_percentage": 15.0,
|
||||
"credit_limit": 3500.00,
|
||||
"is_active": true,
|
||||
"notes": "Restaurante de alta gama. Exigente con la calidad. Panes artesanales especiales.",
|
||||
"tags": ["hosteleria", "restaurante", "premium", "exigente"]
|
||||
"credit_limit": 4000.00,
|
||||
"discount_percentage": 10.00,
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Dos entregas diarias: 5:30 AM y 12:00 PM"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000012",
|
||||
"customer_code": "CLI-012",
|
||||
"name": "Isabel Torres Muñoz",
|
||||
"customer_type": "individual",
|
||||
"email": "isabel.torres@email.com",
|
||||
"phone": "+34 656 789 012",
|
||||
"address_line1": "Calle Goya 88, 4º A",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28001",
|
||||
"country": "España",
|
||||
"customer_segment": "vip",
|
||||
"priority_level": "high",
|
||||
"payment_terms": "immediate",
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Pedidos semanales de tartas especiales"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000013",
|
||||
"customer_code": "CLI-013",
|
||||
"name": "Bar Tapas La Latina",
|
||||
"business_name": "Bar La Latina S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "info@barlalatina.es",
|
||||
"phone": "+34 91 901 2348",
|
||||
"address_line1": "Plaza de la Paja 8",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28005",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"priority_level": "normal",
|
||||
"payment_terms": "net_15",
|
||||
"credit_limit": 1500.00,
|
||||
"discount_percentage": 5.00,
|
||||
"preferred_delivery_method": "pickup"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000014",
|
||||
"customer_code": "CLI-014",
|
||||
"name": "Francisco Gómez Rivera",
|
||||
"customer_type": "individual",
|
||||
"email": "francisco.gomez@email.com",
|
||||
"phone": "+34 667 890 123",
|
||||
"address_line1": "Calle Velázquez 120, 6º B",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28006",
|
||||
"country": "España",
|
||||
"customer_segment": "regular",
|
||||
"priority_level": "normal",
|
||||
"payment_terms": "immediate",
|
||||
"preferred_delivery_method": "pickup"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000015",
|
||||
"customer_code": "CLI-015",
|
||||
"name": "Residencia Tercera Edad Los Olivos",
|
||||
"business_name": "Residencia Los Olivos S.L.",
|
||||
"customer_type": "business",
|
||||
"email": "cocina@residenciaolivos.es",
|
||||
"phone": "+34 91 012 3459",
|
||||
"address_line1": "Calle Arturo Soria 345",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28033",
|
||||
"country": "España",
|
||||
"customer_segment": "wholesale",
|
||||
"priority_level": "high",
|
||||
"payment_terms": "net_30",
|
||||
"credit_limit": 6000.00,
|
||||
"discount_percentage": 10.00,
|
||||
"preferred_delivery_method": "delivery",
|
||||
"special_instructions": "Pan de molde sin corteza para 120 residentes. Entrega 6:00 AM."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
266
services/orders/scripts/demo/compras_config_es.json
Normal file
266
services/orders/scripts/demo/compras_config_es.json
Normal file
@@ -0,0 +1,266 @@
|
||||
{
|
||||
"configuracion_compras": {
|
||||
"planes_por_tenant": 8,
|
||||
"requisitos_por_plan": {
|
||||
"min": 5,
|
||||
"max": 12
|
||||
},
|
||||
"distribucion_temporal": {
|
||||
"completados": {
|
||||
"porcentaje": 0.25,
|
||||
"offset_dias_min": -45,
|
||||
"offset_dias_max": -8,
|
||||
"estados": ["completed"]
|
||||
},
|
||||
"en_ejecucion": {
|
||||
"porcentaje": 0.375,
|
||||
"offset_dias_min": -7,
|
||||
"offset_dias_max": -1,
|
||||
"estados": ["in_execution", "approved"]
|
||||
},
|
||||
"pendiente_aprobacion": {
|
||||
"porcentaje": 0.25,
|
||||
"offset_dias_min": 0,
|
||||
"offset_dias_max": 0,
|
||||
"estados": ["pending_approval"]
|
||||
},
|
||||
"borrador": {
|
||||
"porcentaje": 0.125,
|
||||
"offset_dias_min": 1,
|
||||
"offset_dias_max": 3,
|
||||
"estados": ["draft"]
|
||||
}
|
||||
},
|
||||
"distribucion_estados": {
|
||||
"draft": 0.125,
|
||||
"pending_approval": 0.25,
|
||||
"approved": 0.25,
|
||||
"in_execution": 0.25,
|
||||
"completed": 0.125
|
||||
},
|
||||
"tipos_plan": [
|
||||
{"tipo": "regular", "peso": 0.75},
|
||||
{"tipo": "emergency", "peso": 0.15},
|
||||
{"tipo": "seasonal", "peso": 0.10}
|
||||
],
|
||||
"prioridades": {
|
||||
"low": 0.20,
|
||||
"normal": 0.55,
|
||||
"high": 0.20,
|
||||
"critical": 0.05
|
||||
},
|
||||
"estrategias_compra": [
|
||||
{"estrategia": "just_in_time", "peso": 0.50},
|
||||
{"estrategia": "bulk", "peso": 0.30},
|
||||
{"estrategia": "mixed", "peso": 0.20}
|
||||
],
|
||||
"niveles_riesgo": {
|
||||
"low": 0.50,
|
||||
"medium": 0.30,
|
||||
"high": 0.15,
|
||||
"critical": 0.05
|
||||
},
|
||||
"ingredientes_demo": [
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000001",
|
||||
"nombre": "Harina de Trigo Panadera T-55",
|
||||
"sku": "ING-HAR-001",
|
||||
"categoria": "harinas",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "kg",
|
||||
"costo_unitario": 0.65,
|
||||
"lead_time_dias": 3,
|
||||
"cantidad_minima": 500.0,
|
||||
"vida_util_dias": 180
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000002",
|
||||
"nombre": "Harina de Trigo Integral",
|
||||
"sku": "ING-HAR-002",
|
||||
"categoria": "harinas",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "kg",
|
||||
"costo_unitario": 0.85,
|
||||
"lead_time_dias": 3,
|
||||
"cantidad_minima": 300.0,
|
||||
"vida_util_dias": 120
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000003",
|
||||
"nombre": "Levadura Fresca Prensada",
|
||||
"sku": "ING-LEV-001",
|
||||
"categoria": "levaduras",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "kg",
|
||||
"costo_unitario": 3.50,
|
||||
"lead_time_dias": 2,
|
||||
"cantidad_minima": 25.0,
|
||||
"vida_util_dias": 21
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000004",
|
||||
"nombre": "Sal Marina Refinada",
|
||||
"sku": "ING-SAL-001",
|
||||
"categoria": "ingredientes_basicos",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "kg",
|
||||
"costo_unitario": 0.40,
|
||||
"lead_time_dias": 7,
|
||||
"cantidad_minima": 200.0,
|
||||
"vida_util_dias": 730
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000005",
|
||||
"nombre": "Mantequilla 82% MG",
|
||||
"sku": "ING-MAN-001",
|
||||
"categoria": "lacteos",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "kg",
|
||||
"costo_unitario": 5.80,
|
||||
"lead_time_dias": 2,
|
||||
"cantidad_minima": 50.0,
|
||||
"vida_util_dias": 90
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000006",
|
||||
"nombre": "Azúcar Blanco Refinado",
|
||||
"sku": "ING-AZU-001",
|
||||
"categoria": "azucares",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "kg",
|
||||
"costo_unitario": 0.75,
|
||||
"lead_time_dias": 5,
|
||||
"cantidad_minima": 300.0,
|
||||
"vida_util_dias": 365
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000007",
|
||||
"nombre": "Huevos Categoría A",
|
||||
"sku": "ING-HUE-001",
|
||||
"categoria": "lacteos",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "unidad",
|
||||
"costo_unitario": 0.18,
|
||||
"lead_time_dias": 2,
|
||||
"cantidad_minima": 360.0,
|
||||
"vida_util_dias": 28
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000008",
|
||||
"nombre": "Leche Entera UHT",
|
||||
"sku": "ING-LEC-001",
|
||||
"categoria": "lacteos",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "litro",
|
||||
"costo_unitario": 0.85,
|
||||
"lead_time_dias": 3,
|
||||
"cantidad_minima": 100.0,
|
||||
"vida_util_dias": 90
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000009",
|
||||
"nombre": "Chocolate Cobertura 70%",
|
||||
"sku": "ING-CHO-001",
|
||||
"categoria": "chocolates",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "kg",
|
||||
"costo_unitario": 12.50,
|
||||
"lead_time_dias": 5,
|
||||
"cantidad_minima": 25.0,
|
||||
"vida_util_dias": 365
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000010",
|
||||
"nombre": "Aceite de Oliva Virgen Extra",
|
||||
"sku": "ING-ACE-001",
|
||||
"categoria": "aceites",
|
||||
"tipo": "ingredient",
|
||||
"unidad": "litro",
|
||||
"costo_unitario": 4.20,
|
||||
"lead_time_dias": 4,
|
||||
"cantidad_minima": 50.0,
|
||||
"vida_util_dias": 540
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000011",
|
||||
"nombre": "Bolsas de Papel Kraft",
|
||||
"sku": "PAC-BOL-001",
|
||||
"categoria": "embalaje",
|
||||
"tipo": "packaging",
|
||||
"unidad": "unidad",
|
||||
"costo_unitario": 0.08,
|
||||
"lead_time_dias": 10,
|
||||
"cantidad_minima": 5000.0,
|
||||
"vida_util_dias": 730
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000012",
|
||||
"nombre": "Cajas de Cartón Grande",
|
||||
"sku": "PAC-CAJ-001",
|
||||
"categoria": "embalaje",
|
||||
"tipo": "packaging",
|
||||
"unidad": "unidad",
|
||||
"costo_unitario": 0.45,
|
||||
"lead_time_dias": 7,
|
||||
"cantidad_minima": 500.0,
|
||||
"vida_util_dias": 730
|
||||
}
|
||||
],
|
||||
"rangos_cantidad": {
|
||||
"harinas": {"min": 500.0, "max": 2000.0},
|
||||
"levaduras": {"min": 20.0, "max": 100.0},
|
||||
"ingredientes_basicos": {"min": 100.0, "max": 500.0},
|
||||
"lacteos": {"min": 50.0, "max": 300.0},
|
||||
"azucares": {"min": 200.0, "max": 800.0},
|
||||
"chocolates": {"min": 10.0, "max": 50.0},
|
||||
"aceites": {"min": 30.0, "max": 150.0},
|
||||
"embalaje": {"min": 1000.0, "max": 10000.0}
|
||||
},
|
||||
"buffer_seguridad_porcentaje": {
|
||||
"min": 10.0,
|
||||
"max": 30.0,
|
||||
"tipico": 20.0
|
||||
},
|
||||
"horizonte_planificacion_dias": {
|
||||
"individual_bakery": 14,
|
||||
"central_bakery": 21
|
||||
},
|
||||
"metricas_rendimiento": {
|
||||
"tasa_cumplimiento": {"min": 85.0, "max": 98.0},
|
||||
"entrega_puntual": {"min": 80.0, "max": 95.0},
|
||||
"precision_costo": {"min": 90.0, "max": 99.0},
|
||||
"puntuacion_calidad": {"min": 7.0, "max": 10.0}
|
||||
}
|
||||
},
|
||||
"alertas_compras": {
|
||||
"plan_urgente": {
|
||||
"condicion": "plan_type = emergency AND status IN (draft, pending_approval)",
|
||||
"mensaje": "Plan de compras de emergencia requiere aprobación urgente: {plan_number}",
|
||||
"severidad": "high"
|
||||
},
|
||||
"requisito_critico": {
|
||||
"condicion": "priority = critical AND required_by_date < NOW() + INTERVAL '3 days'",
|
||||
"mensaje": "Requisito crítico con fecha límite próxima: {product_name} para {required_by_date}",
|
||||
"severidad": "high"
|
||||
},
|
||||
"riesgo_suministro": {
|
||||
"condicion": "supply_risk_level IN (high, critical)",
|
||||
"mensaje": "Alto riesgo de suministro detectado en plan {plan_number}",
|
||||
"severidad": "medium"
|
||||
},
|
||||
"fecha_pedido_proxima": {
|
||||
"condicion": "suggested_order_date BETWEEN NOW() AND NOW() + INTERVAL '2 days'",
|
||||
"mensaje": "Fecha sugerida de pedido próxima: {product_name}",
|
||||
"severidad": "medium"
|
||||
}
|
||||
},
|
||||
"notas": {
|
||||
"descripcion": "Configuración para generación de planes de compras demo",
|
||||
"planes_totales": 8,
|
||||
"ingredientes_disponibles": 12,
|
||||
"proveedores": "Usar proveedores de proveedores_es.json",
|
||||
"fechas": "Usar offsets relativos a BASE_REFERENCE_DATE",
|
||||
"moneda": "EUR",
|
||||
"idioma": "español"
|
||||
}
|
||||
}
|
||||
220
services/orders/scripts/demo/pedidos_config_es.json
Normal file
220
services/orders/scripts/demo/pedidos_config_es.json
Normal file
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"configuracion_pedidos": {
|
||||
"total_pedidos_por_tenant": 30,
|
||||
"distribucion_temporal": {
|
||||
"completados_antiguos": {
|
||||
"porcentaje": 0.30,
|
||||
"offset_dias_min": -60,
|
||||
"offset_dias_max": -15,
|
||||
"estados": ["delivered", "completed"]
|
||||
},
|
||||
"completados_recientes": {
|
||||
"porcentaje": 0.25,
|
||||
"offset_dias_min": -14,
|
||||
"offset_dias_max": -1,
|
||||
"estados": ["delivered", "completed"]
|
||||
},
|
||||
"en_proceso": {
|
||||
"porcentaje": 0.25,
|
||||
"offset_dias_min": 0,
|
||||
"offset_dias_max": 0,
|
||||
"estados": ["confirmed", "in_production", "ready"]
|
||||
},
|
||||
"futuros": {
|
||||
"porcentaje": 0.20,
|
||||
"offset_dias_min": 1,
|
||||
"offset_dias_max": 7,
|
||||
"estados": ["pending", "confirmed"]
|
||||
}
|
||||
},
|
||||
"distribucion_estados": {
|
||||
"pending": 0.10,
|
||||
"confirmed": 0.15,
|
||||
"in_production": 0.10,
|
||||
"ready": 0.10,
|
||||
"in_delivery": 0.05,
|
||||
"delivered": 0.35,
|
||||
"completed": 0.10,
|
||||
"cancelled": 0.05
|
||||
},
|
||||
"distribucion_prioridad": {
|
||||
"low": 0.30,
|
||||
"normal": 0.50,
|
||||
"high": 0.15,
|
||||
"urgent": 0.05
|
||||
},
|
||||
"lineas_por_pedido": {
|
||||
"min": 2,
|
||||
"max": 8
|
||||
},
|
||||
"cantidad_por_linea": {
|
||||
"min": 5,
|
||||
"max": 100
|
||||
},
|
||||
"precio_unitario": {
|
||||
"min": 1.50,
|
||||
"max": 15.00
|
||||
},
|
||||
"descuento_porcentaje": {
|
||||
"sin_descuento": 0.70,
|
||||
"con_descuento_5": 0.15,
|
||||
"con_descuento_10": 0.10,
|
||||
"con_descuento_15": 0.05
|
||||
},
|
||||
"metodos_pago": [
|
||||
{"metodo": "bank_transfer", "peso": 0.40},
|
||||
{"metodo": "credit_card", "peso": 0.25},
|
||||
{"metodo": "cash", "peso": 0.20},
|
||||
{"metodo": "check", "peso": 0.10},
|
||||
{"metodo": "account", "peso": 0.05}
|
||||
],
|
||||
"tipos_entrega": [
|
||||
{"tipo": "standard", "peso": 0.60},
|
||||
{"tipo": "delivery", "peso": 0.25},
|
||||
{"tipo": "pickup", "peso": 0.15}
|
||||
],
|
||||
"notas_pedido": [
|
||||
"Entrega en horario de mañana, antes de las 8:00 AM",
|
||||
"Llamar 15 minutos antes de llegar",
|
||||
"Dejar en la entrada de servicio",
|
||||
"Contactar con el encargado al llegar",
|
||||
"Pedido urgente para evento especial",
|
||||
"Embalaje especial para transporte",
|
||||
"Verificar cantidad antes de descargar",
|
||||
"Entrega programada según calendario acordado",
|
||||
"Incluir factura con el pedido",
|
||||
"Pedido recurrente semanal"
|
||||
],
|
||||
"productos_demo": [
|
||||
{
|
||||
"nombre": "Pan de Barra Tradicional",
|
||||
"codigo": "PROD-001",
|
||||
"precio_base": 1.80,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Baguette",
|
||||
"codigo": "PROD-002",
|
||||
"precio_base": 2.00,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Pan Integral",
|
||||
"codigo": "PROD-003",
|
||||
"precio_base": 2.50,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Pan de Centeno",
|
||||
"codigo": "PROD-004",
|
||||
"precio_base": 2.80,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Croissant",
|
||||
"codigo": "PROD-005",
|
||||
"precio_base": 1.50,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Napolitana de Chocolate",
|
||||
"codigo": "PROD-006",
|
||||
"precio_base": 1.80,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Palmera",
|
||||
"codigo": "PROD-007",
|
||||
"precio_base": 1.60,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Ensaimada",
|
||||
"codigo": "PROD-008",
|
||||
"precio_base": 3.50,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Magdalena",
|
||||
"codigo": "PROD-009",
|
||||
"precio_base": 1.20,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Bollo de Leche",
|
||||
"codigo": "PROD-010",
|
||||
"precio_base": 1.00,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Pan de Molde Blanco",
|
||||
"codigo": "PROD-011",
|
||||
"precio_base": 2.20,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Pan de Molde Integral",
|
||||
"codigo": "PROD-012",
|
||||
"precio_base": 2.50,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Panecillo",
|
||||
"codigo": "PROD-013",
|
||||
"precio_base": 0.80,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Rosca de Anís",
|
||||
"codigo": "PROD-014",
|
||||
"precio_base": 3.00,
|
||||
"unidad": "unidad"
|
||||
},
|
||||
{
|
||||
"nombre": "Empanada de Atún",
|
||||
"codigo": "PROD-015",
|
||||
"precio_base": 4.50,
|
||||
"unidad": "unidad"
|
||||
}
|
||||
],
|
||||
"horarios_entrega": [
|
||||
"06:00-08:00",
|
||||
"08:00-10:00",
|
||||
"10:00-12:00",
|
||||
"12:00-14:00",
|
||||
"14:00-16:00",
|
||||
"16:00-18:00"
|
||||
]
|
||||
},
|
||||
"alertas_pedidos": {
|
||||
"pedidos_urgentes": {
|
||||
"condicion": "priority = urgent AND status IN (pending, confirmed)",
|
||||
"mensaje": "Pedido urgente requiere atención inmediata: {order_number}",
|
||||
"severidad": "high"
|
||||
},
|
||||
"pedidos_retrasados": {
|
||||
"condicion": "delivery_date < NOW() AND status NOT IN (delivered, completed, cancelled)",
|
||||
"mensaje": "Pedido retrasado: {order_number} para cliente {customer_name}",
|
||||
"severidad": "high"
|
||||
},
|
||||
"pedidos_proximos": {
|
||||
"condicion": "delivery_date BETWEEN NOW() AND NOW() + INTERVAL '24 hours'",
|
||||
"mensaje": "Entrega programada en las próximas 24 horas: {order_number}",
|
||||
"severidad": "medium"
|
||||
},
|
||||
"pedidos_grandes": {
|
||||
"condicion": "total_amount > 500",
|
||||
"mensaje": "Pedido de alto valor requiere verificación: {order_number} ({total_amount}¬)",
|
||||
"severidad": "medium"
|
||||
}
|
||||
},
|
||||
"notas": {
|
||||
"descripcion": "Configuración para generación automática de pedidos demo",
|
||||
"total_pedidos": 30,
|
||||
"productos_disponibles": 15,
|
||||
"clientes_requeridos": "Usar clientes de clientes_es.json",
|
||||
"fechas": "Usar offsets relativos a BASE_REFERENCE_DATE",
|
||||
"moneda": "EUR",
|
||||
"idioma": "español"
|
||||
}
|
||||
}
|
||||
230
services/orders/scripts/demo/seed_demo_customers.py
Executable file
230
services/orders/scripts/demo/seed_demo_customers.py
Executable file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Customer Seeding Script for Orders Service
|
||||
Creates customers for demo template tenants
|
||||
|
||||
This script runs as a Kubernetes init job inside the orders-service container.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Add app to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
|
||||
from app.models.customer import Customer
|
||||
|
||||
# Configure logging
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
|
||||
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery
|
||||
|
||||
# Base reference date for date calculations
|
||||
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def load_customer_data():
|
||||
"""Load customer data from JSON file"""
|
||||
data_file = Path(__file__).parent / "clientes_es.json"
|
||||
if not data_file.exists():
|
||||
raise FileNotFoundError(f"Customer data file not found: {data_file}")
|
||||
|
||||
with open(data_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def calculate_date_from_offset(offset_days: int) -> datetime:
|
||||
"""Calculate a date based on offset from BASE_REFERENCE_DATE"""
|
||||
return BASE_REFERENCE_DATE + timedelta(days=offset_days)
|
||||
|
||||
|
||||
async def seed_customers_for_tenant(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
tenant_name: str,
|
||||
customer_list: list
|
||||
):
|
||||
"""Seed customers for a specific tenant"""
|
||||
logger.info(f"Seeding customers for: {tenant_name}", tenant_id=str(tenant_id))
|
||||
|
||||
# Check if customers already exist
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.tenant_id == tenant_id).limit(1)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f"Customers already exist for {tenant_name}, skipping seed")
|
||||
return {"tenant_id": str(tenant_id), "customers_created": 0, "skipped": True}
|
||||
|
||||
count = 0
|
||||
for customer_data in customer_list:
|
||||
# Calculate dates from offsets
|
||||
first_order_date = None
|
||||
if "first_order_offset_days" in customer_data:
|
||||
first_order_date = calculate_date_from_offset(customer_data["first_order_offset_days"])
|
||||
|
||||
last_order_date = None
|
||||
if "last_order_offset_days" in customer_data:
|
||||
last_order_date = calculate_date_from_offset(customer_data["last_order_offset_days"])
|
||||
|
||||
# Use strings directly (model doesn't use enums)
|
||||
customer_type = customer_data.get("customer_type", "business")
|
||||
customer_segment = customer_data.get("customer_segment", "regular")
|
||||
is_active = customer_data.get("status", "active") == "active"
|
||||
|
||||
# Create customer (using actual model fields)
|
||||
# For San Pablo, use original IDs. For La Espiga, generate new UUIDs
|
||||
if tenant_id == DEMO_TENANT_SAN_PABLO:
|
||||
customer_id = uuid.UUID(customer_data["id"])
|
||||
else:
|
||||
# Generate deterministic UUID for La Espiga based on original ID
|
||||
base_uuid = uuid.UUID(customer_data["id"])
|
||||
# Add a fixed offset to create a unique but deterministic ID
|
||||
customer_id = uuid.UUID(int=base_uuid.int + 0x10000000000000000000000000000000)
|
||||
|
||||
customer = Customer(
|
||||
id=customer_id,
|
||||
tenant_id=tenant_id,
|
||||
customer_code=customer_data["customer_code"],
|
||||
name=customer_data["name"],
|
||||
business_name=customer_data.get("business_name"),
|
||||
customer_type=customer_type,
|
||||
tax_id=customer_data.get("tax_id"),
|
||||
email=customer_data.get("email"),
|
||||
phone=customer_data.get("phone"),
|
||||
address_line1=customer_data.get("billing_address"),
|
||||
city=customer_data.get("billing_city"),
|
||||
state=customer_data.get("billing_state"),
|
||||
postal_code=customer_data.get("billing_postal_code"),
|
||||
country=customer_data.get("billing_country", "España"),
|
||||
is_active=is_active,
|
||||
preferred_delivery_method=customer_data.get("preferred_delivery_method", "delivery"),
|
||||
payment_terms=customer_data.get("payment_terms", "immediate"),
|
||||
credit_limit=customer_data.get("credit_limit"),
|
||||
discount_percentage=customer_data.get("discount_percentage", 0.0),
|
||||
customer_segment=customer_segment,
|
||||
priority_level=customer_data.get("priority_level", "normal"),
|
||||
special_instructions=customer_data.get("special_instructions"),
|
||||
total_orders=customer_data.get("total_orders", 0),
|
||||
total_spent=customer_data.get("total_revenue", 0.0),
|
||||
average_order_value=customer_data.get("average_order_value", 0.0),
|
||||
last_order_date=last_order_date,
|
||||
created_at=BASE_REFERENCE_DATE,
|
||||
updated_at=BASE_REFERENCE_DATE
|
||||
)
|
||||
|
||||
db.add(customer)
|
||||
count += 1
|
||||
logger.debug(f"Created customer: {customer.name}", customer_id=str(customer.id))
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Successfully created {count} customers for {tenant_name}")
|
||||
|
||||
return {
|
||||
"tenant_id": str(tenant_id),
|
||||
"customers_created": count,
|
||||
"skipped": False
|
||||
}
|
||||
|
||||
|
||||
async def seed_all(db: AsyncSession):
|
||||
"""Seed all demo tenants with customers"""
|
||||
logger.info("Starting demo customer seed process")
|
||||
|
||||
# Load customer data
|
||||
data = load_customer_data()
|
||||
|
||||
results = []
|
||||
|
||||
# Both tenants get the same customer base
|
||||
# (In real scenario, you might want different customer lists)
|
||||
result_san_pablo = await seed_customers_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_SAN_PABLO,
|
||||
"San Pablo - Individual Bakery",
|
||||
data["clientes"]
|
||||
)
|
||||
results.append(result_san_pablo)
|
||||
|
||||
result_la_espiga = await seed_customers_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_LA_ESPIGA,
|
||||
"La Espiga - Central Bakery",
|
||||
data["clientes"]
|
||||
)
|
||||
results.append(result_la_espiga)
|
||||
|
||||
total_created = sum(r["customers_created"] for r in results)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total_customers_created": total_created,
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution function"""
|
||||
# Get database URL from environment
|
||||
database_url = os.getenv("ORDERS_DATABASE_URL")
|
||||
if not database_url:
|
||||
logger.error("ORDERS_DATABASE_URL environment variable must be set")
|
||||
return 1
|
||||
|
||||
# Ensure asyncpg driver
|
||||
if database_url.startswith("postgresql://"):
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
# Create async engine
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await seed_all(session)
|
||||
|
||||
logger.info(
|
||||
"Customer seed completed successfully!",
|
||||
total_customers=result["total_customers_created"],
|
||||
status=result["status"]
|
||||
)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*60)
|
||||
print("DEMO CUSTOMER SEED SUMMARY")
|
||||
print("="*60)
|
||||
for tenant_result in result["results"]:
|
||||
tenant_id = tenant_result["tenant_id"]
|
||||
count = tenant_result["customers_created"]
|
||||
skipped = tenant_result.get("skipped", False)
|
||||
status = "SKIPPED (already exists)" if skipped else f"CREATED {count} customers"
|
||||
print(f"Tenant {tenant_id}: {status}")
|
||||
print(f"\nTotal Customers Created: {result['total_customers_created']}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Customer seed failed: {str(e)}", exc_info=True)
|
||||
return 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
396
services/orders/scripts/demo/seed_demo_orders.py
Executable file
396
services/orders/scripts/demo/seed_demo_orders.py
Executable file
@@ -0,0 +1,396 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Orders Seeding Script for Orders Service
|
||||
Creates realistic orders with order lines for demo template tenants
|
||||
|
||||
This script runs as a Kubernetes init job inside the orders-service container.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
from decimal import Decimal
|
||||
|
||||
# Add app to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
|
||||
from app.models.order import CustomerOrder, OrderItem
|
||||
from app.models.customer import Customer
|
||||
from app.models.enums import OrderStatus, PaymentMethod, PaymentStatus, DeliveryMethod
|
||||
|
||||
# Configure logging
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
|
||||
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery
|
||||
|
||||
# Base reference date for date calculations
|
||||
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def load_orders_config():
|
||||
"""Load orders configuration from JSON file"""
|
||||
config_file = Path(__file__).parent / "pedidos_config_es.json"
|
||||
if not config_file.exists():
|
||||
raise FileNotFoundError(f"Orders config file not found: {config_file}")
|
||||
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_customers_data():
|
||||
"""Load customers data from JSON file"""
|
||||
customers_file = Path(__file__).parent / "clientes_es.json"
|
||||
if not customers_file.exists():
|
||||
raise FileNotFoundError(f"Customers file not found: {customers_file}")
|
||||
|
||||
with open(customers_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data.get("clientes", [])
|
||||
|
||||
|
||||
def calculate_date_from_offset(offset_days: int) -> datetime:
|
||||
"""Calculate a date based on offset from BASE_REFERENCE_DATE"""
|
||||
return BASE_REFERENCE_DATE + timedelta(days=offset_days)
|
||||
|
||||
|
||||
# Model uses simple strings, no need for enum mapping functions
|
||||
# (OrderPriority, DeliveryType don't exist in enums.py)
|
||||
|
||||
|
||||
def weighted_choice(choices: list) -> dict:
|
||||
"""Make a weighted random choice from list of dicts with 'peso' key"""
|
||||
total_weight = sum(c.get("peso", 1.0) for c in choices)
|
||||
r = random.uniform(0, total_weight)
|
||||
|
||||
cumulative = 0
|
||||
for choice in choices:
|
||||
cumulative += choice.get("peso", 1.0)
|
||||
if r <= cumulative:
|
||||
return choice
|
||||
|
||||
return choices[-1]
|
||||
|
||||
|
||||
def generate_order_number(tenant_id: uuid.UUID, index: int) -> str:
|
||||
"""Generate a unique order number"""
|
||||
tenant_prefix = "SP" if tenant_id == DEMO_TENANT_SAN_PABLO else "LE"
|
||||
return f"ORD-{tenant_prefix}-{BASE_REFERENCE_DATE.year}-{index:04d}"
|
||||
|
||||
|
||||
async def generate_orders_for_tenant(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
tenant_name: str,
|
||||
config: dict,
|
||||
customers_data: list
|
||||
):
|
||||
"""Generate orders for a specific tenant"""
|
||||
logger.info(f"Generating orders for: {tenant_name}", tenant_id=str(tenant_id))
|
||||
|
||||
# Check if orders already exist
|
||||
result = await db.execute(
|
||||
select(CustomerOrder).where(CustomerOrder.tenant_id == tenant_id).limit(1)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f"Orders already exist for {tenant_name}, skipping seed")
|
||||
return {"tenant_id": str(tenant_id), "orders_created": 0, "order_lines_created": 0, "skipped": True}
|
||||
|
||||
# Get customers for this tenant
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.tenant_id == tenant_id)
|
||||
)
|
||||
customers = list(result.scalars().all())
|
||||
|
||||
if not customers:
|
||||
logger.warning(f"No customers found for {tenant_name}, cannot generate orders")
|
||||
return {"tenant_id": str(tenant_id), "orders_created": 0, "order_lines_created": 0, "error": "no_customers"}
|
||||
|
||||
orders_config = config["configuracion_pedidos"]
|
||||
total_orders = orders_config["total_pedidos_por_tenant"]
|
||||
|
||||
orders_created = 0
|
||||
lines_created = 0
|
||||
|
||||
for i in range(total_orders):
|
||||
# Select random customer
|
||||
customer = random.choice(customers)
|
||||
|
||||
# Determine temporal distribution
|
||||
rand_temporal = random.random()
|
||||
cumulative = 0
|
||||
temporal_category = None
|
||||
|
||||
for category, details in orders_config["distribucion_temporal"].items():
|
||||
cumulative += details["porcentaje"]
|
||||
if rand_temporal <= cumulative:
|
||||
temporal_category = details
|
||||
break
|
||||
|
||||
if not temporal_category:
|
||||
temporal_category = orders_config["distribucion_temporal"]["completados_antiguos"]
|
||||
|
||||
# Calculate order date
|
||||
offset_days = random.randint(
|
||||
temporal_category["offset_dias_min"],
|
||||
temporal_category["offset_dias_max"]
|
||||
)
|
||||
order_date = calculate_date_from_offset(offset_days)
|
||||
|
||||
# Select status based on temporal category (use strings directly)
|
||||
status = random.choice(temporal_category["estados"])
|
||||
|
||||
# Select priority (use strings directly)
|
||||
priority_rand = random.random()
|
||||
cumulative_priority = 0
|
||||
priority = "normal"
|
||||
for p, weight in orders_config["distribucion_prioridad"].items():
|
||||
cumulative_priority += weight
|
||||
if priority_rand <= cumulative_priority:
|
||||
priority = p
|
||||
break
|
||||
|
||||
# Select payment method (use strings directly)
|
||||
payment_method_choice = weighted_choice(orders_config["metodos_pago"])
|
||||
payment_method = payment_method_choice["metodo"]
|
||||
|
||||
# Select delivery type (use strings directly)
|
||||
delivery_type_choice = weighted_choice(orders_config["tipos_entrega"])
|
||||
delivery_method = delivery_type_choice["tipo"]
|
||||
|
||||
# Calculate delivery date (1-7 days after order date typically)
|
||||
delivery_offset = random.randint(1, 7)
|
||||
delivery_date = order_date + timedelta(days=delivery_offset)
|
||||
|
||||
# Select delivery time
|
||||
delivery_time = random.choice(orders_config["horarios_entrega"])
|
||||
|
||||
# Generate order number
|
||||
order_number = generate_order_number(tenant_id, i + 1)
|
||||
|
||||
# Select notes
|
||||
notes = random.choice(orders_config["notas_pedido"]) if random.random() < 0.6 else None
|
||||
|
||||
# Create order (using only actual model fields)
|
||||
order = CustomerOrder(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant_id,
|
||||
order_number=order_number,
|
||||
customer_id=customer.id,
|
||||
status=status,
|
||||
order_type="standard",
|
||||
priority=priority,
|
||||
order_date=order_date,
|
||||
requested_delivery_date=delivery_date,
|
||||
confirmed_delivery_date=delivery_date if status != "pending" else None,
|
||||
actual_delivery_date=delivery_date if status in ["delivered", "completed"] else None,
|
||||
delivery_method=delivery_method,
|
||||
delivery_address={"address": customer.address_line1, "city": customer.city, "postal_code": customer.postal_code} if customer.address_line1 else None,
|
||||
payment_method=payment_method,
|
||||
payment_status="paid" if status in ["delivered", "completed"] else "pending",
|
||||
payment_terms="immediate",
|
||||
subtotal=Decimal("0.00"), # Will calculate
|
||||
discount_percentage=Decimal("0.00"), # Will set
|
||||
discount_amount=Decimal("0.00"), # Will calculate
|
||||
tax_amount=Decimal("0.00"), # Will calculate
|
||||
delivery_fee=Decimal("0.00"),
|
||||
total_amount=Decimal("0.00"), # Will calculate
|
||||
special_instructions=notes,
|
||||
order_source="manual",
|
||||
sales_channel="direct",
|
||||
created_at=order_date,
|
||||
updated_at=order_date
|
||||
)
|
||||
|
||||
db.add(order)
|
||||
await db.flush() # Get order ID
|
||||
|
||||
# Generate order lines
|
||||
num_lines = random.randint(
|
||||
orders_config["lineas_por_pedido"]["min"],
|
||||
orders_config["lineas_por_pedido"]["max"]
|
||||
)
|
||||
|
||||
# Select random products
|
||||
selected_products = random.sample(
|
||||
orders_config["productos_demo"],
|
||||
min(num_lines, len(orders_config["productos_demo"]))
|
||||
)
|
||||
|
||||
subtotal = Decimal("0.00")
|
||||
|
||||
for line_num, product in enumerate(selected_products, 1):
|
||||
quantity = random.randint(
|
||||
orders_config["cantidad_por_linea"]["min"],
|
||||
orders_config["cantidad_por_linea"]["max"]
|
||||
)
|
||||
|
||||
# Use base price with some variation
|
||||
unit_price = Decimal(str(product["precio_base"])) * Decimal(str(random.uniform(0.95, 1.05)))
|
||||
unit_price = unit_price.quantize(Decimal("0.01"))
|
||||
|
||||
line_total = unit_price * quantity
|
||||
|
||||
order_line = OrderItem(
|
||||
id=uuid.uuid4(),
|
||||
order_id=order.id,
|
||||
product_id=uuid.uuid4(), # Generate placeholder product ID
|
||||
product_name=product["nombre"],
|
||||
product_sku=product["codigo"],
|
||||
quantity=Decimal(str(quantity)),
|
||||
unit_of_measure="each",
|
||||
unit_price=unit_price,
|
||||
line_discount=Decimal("0.00"),
|
||||
line_total=line_total,
|
||||
status="pending"
|
||||
)
|
||||
|
||||
db.add(order_line)
|
||||
subtotal += line_total
|
||||
lines_created += 1
|
||||
|
||||
# Apply order-level discount
|
||||
discount_rand = random.random()
|
||||
if discount_rand < 0.70:
|
||||
discount_percentage = Decimal("0.00")
|
||||
elif discount_rand < 0.85:
|
||||
discount_percentage = Decimal("5.00")
|
||||
elif discount_rand < 0.95:
|
||||
discount_percentage = Decimal("10.00")
|
||||
else:
|
||||
discount_percentage = Decimal("15.00")
|
||||
|
||||
discount_amount = (subtotal * discount_percentage / 100).quantize(Decimal("0.01"))
|
||||
amount_after_discount = subtotal - discount_amount
|
||||
tax_amount = (amount_after_discount * Decimal("0.10")).quantize(Decimal("0.01"))
|
||||
total_amount = amount_after_discount + tax_amount
|
||||
|
||||
# Update order totals
|
||||
order.subtotal = subtotal
|
||||
order.discount_percentage = discount_percentage
|
||||
order.discount_amount = discount_amount
|
||||
order.tax_amount = tax_amount
|
||||
order.total_amount = total_amount
|
||||
|
||||
orders_created += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Successfully created {orders_created} orders with {lines_created} lines for {tenant_name}")
|
||||
|
||||
return {
|
||||
"tenant_id": str(tenant_id),
|
||||
"orders_created": orders_created,
|
||||
"order_lines_created": lines_created,
|
||||
"skipped": False
|
||||
}
|
||||
|
||||
|
||||
async def seed_all(db: AsyncSession):
|
||||
"""Seed all demo tenants with orders"""
|
||||
logger.info("Starting demo orders seed process")
|
||||
|
||||
# Load configuration
|
||||
config = load_orders_config()
|
||||
customers_data = load_customers_data()
|
||||
|
||||
results = []
|
||||
|
||||
# Seed San Pablo (Individual Bakery)
|
||||
result_san_pablo = await generate_orders_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_SAN_PABLO,
|
||||
"San Pablo - Individual Bakery",
|
||||
config,
|
||||
customers_data
|
||||
)
|
||||
results.append(result_san_pablo)
|
||||
|
||||
# Seed La Espiga (Central Bakery)
|
||||
result_la_espiga = await generate_orders_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_LA_ESPIGA,
|
||||
"La Espiga - Central Bakery",
|
||||
config,
|
||||
customers_data
|
||||
)
|
||||
results.append(result_la_espiga)
|
||||
|
||||
total_orders = sum(r["orders_created"] for r in results)
|
||||
total_lines = sum(r["order_lines_created"] for r in results)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total_orders_created": total_orders,
|
||||
"total_lines_created": total_lines,
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution function"""
|
||||
# Get database URL from environment
|
||||
database_url = os.getenv("ORDERS_DATABASE_URL")
|
||||
if not database_url:
|
||||
logger.error("ORDERS_DATABASE_URL environment variable must be set")
|
||||
return 1
|
||||
|
||||
# Ensure asyncpg driver
|
||||
if database_url.startswith("postgresql://"):
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
# Create async engine
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await seed_all(session)
|
||||
|
||||
logger.info(
|
||||
"Orders seed completed successfully!",
|
||||
total_orders=result["total_orders_created"],
|
||||
total_lines=result["total_lines_created"],
|
||||
status=result["status"]
|
||||
)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*60)
|
||||
print("DEMO ORDERS SEED SUMMARY")
|
||||
print("="*60)
|
||||
for tenant_result in result["results"]:
|
||||
tenant_id = tenant_result["tenant_id"]
|
||||
orders = tenant_result["orders_created"]
|
||||
lines = tenant_result["order_lines_created"]
|
||||
skipped = tenant_result.get("skipped", False)
|
||||
status = "SKIPPED (already exists)" if skipped else f"CREATED {orders} orders, {lines} lines"
|
||||
print(f"Tenant {tenant_id}: {status}")
|
||||
print(f"\nTotal Orders: {result['total_orders_created']}")
|
||||
print(f"Total Order Lines: {result['total_lines_created']}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Orders seed failed: {str(e)}", exc_info=True)
|
||||
return 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
496
services/orders/scripts/demo/seed_demo_procurement.py
Executable file
496
services/orders/scripts/demo/seed_demo_procurement.py
Executable file
@@ -0,0 +1,496 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Demo Procurement Seeding Script for Orders Service
|
||||
Creates procurement plans and requirements for demo template tenants
|
||||
|
||||
This script runs as a Kubernetes init job inside the orders-service container.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime, timezone, timedelta, date
|
||||
from pathlib import Path
|
||||
from decimal import Decimal
|
||||
|
||||
# Add app to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
|
||||
from app.models.procurement import ProcurementPlan, ProcurementRequirement
|
||||
|
||||
# Configure logging
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
|
||||
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery
|
||||
|
||||
# Base reference date for date calculations
|
||||
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def load_procurement_config():
|
||||
"""Load procurement configuration from JSON file"""
|
||||
config_file = Path(__file__).parent / "compras_config_es.json"
|
||||
if not config_file.exists():
|
||||
raise FileNotFoundError(f"Procurement config file not found: {config_file}")
|
||||
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def calculate_date_from_offset(offset_days: int) -> date:
|
||||
"""Calculate a date based on offset from BASE_REFERENCE_DATE"""
|
||||
return (BASE_REFERENCE_DATE + timedelta(days=offset_days)).date()
|
||||
|
||||
|
||||
def calculate_datetime_from_offset(offset_days: int) -> datetime:
|
||||
"""Calculate a datetime based on offset from BASE_REFERENCE_DATE"""
|
||||
return BASE_REFERENCE_DATE + timedelta(days=offset_days)
|
||||
|
||||
|
||||
def weighted_choice(choices: list) -> dict:
|
||||
"""Make a weighted random choice from list of dicts with 'peso' key"""
|
||||
total_weight = sum(c.get("peso", 1.0) for c in choices)
|
||||
r = random.uniform(0, total_weight)
|
||||
|
||||
cumulative = 0
|
||||
for choice in choices:
|
||||
cumulative += choice.get("peso", 1.0)
|
||||
if r <= cumulative:
|
||||
return choice
|
||||
|
||||
return choices[-1]
|
||||
|
||||
|
||||
def generate_plan_number(tenant_id: uuid.UUID, index: int, plan_type: str) -> str:
|
||||
"""Generate a unique plan number"""
|
||||
tenant_prefix = "SP" if tenant_id == DEMO_TENANT_SAN_PABLO else "LE"
|
||||
type_code = plan_type[0:3].upper()
|
||||
return f"PROC-{tenant_prefix}-{type_code}-{BASE_REFERENCE_DATE.year}-{index:03d}"
|
||||
|
||||
|
||||
async def generate_procurement_for_tenant(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
tenant_name: str,
|
||||
business_model: str,
|
||||
config: dict
|
||||
):
|
||||
"""Generate procurement plans and requirements for a specific tenant"""
|
||||
logger.info(f"Generating procurement data for: {tenant_name}", tenant_id=str(tenant_id))
|
||||
|
||||
# Check if procurement plans already exist
|
||||
result = await db.execute(
|
||||
select(ProcurementPlan).where(ProcurementPlan.tenant_id == tenant_id).limit(1)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f"Procurement plans already exist for {tenant_name}, skipping seed")
|
||||
return {"tenant_id": str(tenant_id), "plans_created": 0, "requirements_created": 0, "skipped": True}
|
||||
|
||||
proc_config = config["configuracion_compras"]
|
||||
total_plans = proc_config["planes_por_tenant"]
|
||||
|
||||
plans_created = 0
|
||||
requirements_created = 0
|
||||
|
||||
for i in range(total_plans):
|
||||
# Determine temporal distribution
|
||||
rand_temporal = random.random()
|
||||
cumulative = 0
|
||||
temporal_category = None
|
||||
|
||||
for category, details in proc_config["distribucion_temporal"].items():
|
||||
cumulative += details["porcentaje"]
|
||||
if rand_temporal <= cumulative:
|
||||
temporal_category = details
|
||||
break
|
||||
|
||||
if not temporal_category:
|
||||
temporal_category = proc_config["distribucion_temporal"]["completados"]
|
||||
|
||||
# Calculate plan date
|
||||
offset_days = random.randint(
|
||||
temporal_category["offset_dias_min"],
|
||||
temporal_category["offset_dias_max"]
|
||||
)
|
||||
plan_date = calculate_date_from_offset(offset_days)
|
||||
|
||||
# Select status
|
||||
status = random.choice(temporal_category["estados"])
|
||||
|
||||
# Select plan type
|
||||
plan_type_choice = weighted_choice(proc_config["tipos_plan"])
|
||||
plan_type = plan_type_choice["tipo"]
|
||||
|
||||
# Select priority
|
||||
priority_rand = random.random()
|
||||
cumulative_priority = 0
|
||||
priority = "normal"
|
||||
for p, weight in proc_config["prioridades"].items():
|
||||
cumulative_priority += weight
|
||||
if priority_rand <= cumulative_priority:
|
||||
priority = p
|
||||
break
|
||||
|
||||
# Select procurement strategy
|
||||
strategy_choice = weighted_choice(proc_config["estrategias_compra"])
|
||||
procurement_strategy = strategy_choice["estrategia"]
|
||||
|
||||
# Select supply risk level
|
||||
risk_rand = random.random()
|
||||
cumulative_risk = 0
|
||||
supply_risk_level = "low"
|
||||
for risk, weight in proc_config["niveles_riesgo"].items():
|
||||
cumulative_risk += weight
|
||||
if risk_rand <= cumulative_risk:
|
||||
supply_risk_level = risk
|
||||
break
|
||||
|
||||
# Calculate planning horizon
|
||||
planning_horizon = proc_config["horizonte_planificacion_dias"][business_model]
|
||||
|
||||
# Calculate period dates
|
||||
period_start = plan_date
|
||||
period_end = plan_date + timedelta(days=planning_horizon)
|
||||
|
||||
# Generate plan number
|
||||
plan_number = generate_plan_number(tenant_id, i + 1, plan_type)
|
||||
|
||||
# Calculate safety stock buffer
|
||||
safety_stock_buffer = Decimal(str(random.uniform(
|
||||
proc_config["buffer_seguridad_porcentaje"]["min"],
|
||||
proc_config["buffer_seguridad_porcentaje"]["max"]
|
||||
)))
|
||||
|
||||
# Calculate approval/execution dates based on status
|
||||
approved_at = None
|
||||
execution_started_at = None
|
||||
execution_completed_at = None
|
||||
approved_by = None
|
||||
|
||||
if status in ["approved", "in_execution", "completed"]:
|
||||
approved_at = calculate_datetime_from_offset(offset_days - 1)
|
||||
approved_by = uuid.uuid4() # Would be actual user ID
|
||||
|
||||
if status in ["in_execution", "completed"]:
|
||||
execution_started_at = calculate_datetime_from_offset(offset_days)
|
||||
|
||||
if status == "completed":
|
||||
execution_completed_at = calculate_datetime_from_offset(offset_days + planning_horizon)
|
||||
|
||||
# Calculate performance metrics for completed plans
|
||||
fulfillment_rate = None
|
||||
on_time_delivery_rate = None
|
||||
cost_accuracy = None
|
||||
quality_score = None
|
||||
|
||||
if status == "completed":
|
||||
metrics = proc_config["metricas_rendimiento"]
|
||||
fulfillment_rate = Decimal(str(random.uniform(
|
||||
metrics["tasa_cumplimiento"]["min"],
|
||||
metrics["tasa_cumplimiento"]["max"]
|
||||
)))
|
||||
on_time_delivery_rate = Decimal(str(random.uniform(
|
||||
metrics["entrega_puntual"]["min"],
|
||||
metrics["entrega_puntual"]["max"]
|
||||
)))
|
||||
cost_accuracy = Decimal(str(random.uniform(
|
||||
metrics["precision_costo"]["min"],
|
||||
metrics["precision_costo"]["max"]
|
||||
)))
|
||||
quality_score = Decimal(str(random.uniform(
|
||||
metrics["puntuacion_calidad"]["min"],
|
||||
metrics["puntuacion_calidad"]["max"]
|
||||
)))
|
||||
|
||||
# Create procurement plan
|
||||
plan = ProcurementPlan(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant_id,
|
||||
plan_number=plan_number,
|
||||
plan_date=plan_date,
|
||||
plan_period_start=period_start,
|
||||
plan_period_end=period_end,
|
||||
planning_horizon_days=planning_horizon,
|
||||
status=status,
|
||||
plan_type=plan_type,
|
||||
priority=priority,
|
||||
business_model=business_model,
|
||||
procurement_strategy=procurement_strategy,
|
||||
total_requirements=0, # Will update after adding requirements
|
||||
total_estimated_cost=Decimal("0.00"), # Will calculate
|
||||
total_approved_cost=Decimal("0.00"),
|
||||
safety_stock_buffer=safety_stock_buffer,
|
||||
supply_risk_level=supply_risk_level,
|
||||
demand_forecast_confidence=Decimal(str(random.uniform(7.0, 9.5))),
|
||||
approved_at=approved_at,
|
||||
approved_by=approved_by,
|
||||
execution_started_at=execution_started_at,
|
||||
execution_completed_at=execution_completed_at,
|
||||
fulfillment_rate=fulfillment_rate,
|
||||
on_time_delivery_rate=on_time_delivery_rate,
|
||||
cost_accuracy=cost_accuracy,
|
||||
quality_score=quality_score,
|
||||
created_at=calculate_datetime_from_offset(offset_days - 2),
|
||||
updated_at=calculate_datetime_from_offset(offset_days)
|
||||
)
|
||||
|
||||
db.add(plan)
|
||||
await db.flush() # Get plan ID
|
||||
|
||||
# Generate requirements for this plan
|
||||
num_requirements = random.randint(
|
||||
proc_config["requisitos_por_plan"]["min"],
|
||||
proc_config["requisitos_por_plan"]["max"]
|
||||
)
|
||||
|
||||
# Select random ingredients
|
||||
selected_ingredients = random.sample(
|
||||
proc_config["ingredientes_demo"],
|
||||
min(num_requirements, len(proc_config["ingredientes_demo"]))
|
||||
)
|
||||
|
||||
total_estimated_cost = Decimal("0.00")
|
||||
|
||||
for req_num, ingredient in enumerate(selected_ingredients, 1):
|
||||
# Get quantity range for category
|
||||
category = ingredient["categoria"]
|
||||
cantidad_range = proc_config["rangos_cantidad"].get(
|
||||
category,
|
||||
{"min": 50.0, "max": 200.0}
|
||||
)
|
||||
|
||||
# Calculate required quantity
|
||||
required_quantity = Decimal(str(random.uniform(
|
||||
cantidad_range["min"],
|
||||
cantidad_range["max"]
|
||||
)))
|
||||
|
||||
# Calculate safety stock
|
||||
safety_stock_quantity = required_quantity * (safety_stock_buffer / 100)
|
||||
|
||||
# Total quantity needed
|
||||
total_quantity_needed = required_quantity + safety_stock_quantity
|
||||
|
||||
# Current stock simulation
|
||||
current_stock_level = required_quantity * Decimal(str(random.uniform(0.1, 0.4)))
|
||||
reserved_stock = current_stock_level * Decimal(str(random.uniform(0.0, 0.3)))
|
||||
available_stock = current_stock_level - reserved_stock
|
||||
|
||||
# Net requirement
|
||||
net_requirement = total_quantity_needed - available_stock
|
||||
|
||||
# Demand breakdown
|
||||
order_demand = required_quantity * Decimal(str(random.uniform(0.5, 0.7)))
|
||||
production_demand = required_quantity * Decimal(str(random.uniform(0.2, 0.4)))
|
||||
forecast_demand = required_quantity * Decimal(str(random.uniform(0.05, 0.15)))
|
||||
buffer_demand = safety_stock_quantity
|
||||
|
||||
# Pricing
|
||||
estimated_unit_cost = Decimal(str(ingredient["costo_unitario"])) * Decimal(str(random.uniform(0.95, 1.05)))
|
||||
estimated_total_cost = estimated_unit_cost * net_requirement
|
||||
|
||||
# Timing
|
||||
lead_time_days = ingredient["lead_time_dias"]
|
||||
required_by_date = period_start + timedelta(days=random.randint(3, planning_horizon - 2))
|
||||
lead_time_buffer_days = random.randint(1, 2)
|
||||
suggested_order_date = required_by_date - timedelta(days=lead_time_days + lead_time_buffer_days)
|
||||
latest_order_date = required_by_date - timedelta(days=lead_time_days)
|
||||
|
||||
# Requirement status based on plan status
|
||||
if status == "draft":
|
||||
req_status = "pending"
|
||||
elif status == "pending_approval":
|
||||
req_status = "pending"
|
||||
elif status == "approved":
|
||||
req_status = "approved"
|
||||
elif status == "in_execution":
|
||||
req_status = random.choice(["ordered", "partially_received"])
|
||||
elif status == "completed":
|
||||
req_status = "received"
|
||||
else:
|
||||
req_status = "pending"
|
||||
|
||||
# Requirement priority
|
||||
if priority == "critical":
|
||||
req_priority = "critical"
|
||||
elif priority == "high":
|
||||
req_priority = random.choice(["high", "critical"])
|
||||
else:
|
||||
req_priority = random.choice(["normal", "high"])
|
||||
|
||||
# Risk level
|
||||
if supply_risk_level == "critical":
|
||||
req_risk_level = random.choice(["high", "critical"])
|
||||
elif supply_risk_level == "high":
|
||||
req_risk_level = random.choice(["medium", "high"])
|
||||
else:
|
||||
req_risk_level = "low"
|
||||
|
||||
# Create requirement
|
||||
requirement = ProcurementRequirement(
|
||||
id=uuid.uuid4(),
|
||||
plan_id=plan.id,
|
||||
requirement_number=f"{plan_number}-REQ-{req_num:03d}",
|
||||
product_id=uuid.UUID(ingredient["id"]),
|
||||
product_name=ingredient["nombre"],
|
||||
product_sku=ingredient["sku"],
|
||||
product_category=ingredient["categoria"],
|
||||
product_type=ingredient["tipo"],
|
||||
required_quantity=required_quantity,
|
||||
unit_of_measure=ingredient["unidad"],
|
||||
safety_stock_quantity=safety_stock_quantity,
|
||||
total_quantity_needed=total_quantity_needed,
|
||||
current_stock_level=current_stock_level,
|
||||
reserved_stock=reserved_stock,
|
||||
available_stock=available_stock,
|
||||
net_requirement=net_requirement,
|
||||
order_demand=order_demand,
|
||||
production_demand=production_demand,
|
||||
forecast_demand=forecast_demand,
|
||||
buffer_demand=buffer_demand,
|
||||
supplier_lead_time_days=lead_time_days,
|
||||
minimum_order_quantity=Decimal(str(ingredient["cantidad_minima"])),
|
||||
estimated_unit_cost=estimated_unit_cost,
|
||||
estimated_total_cost=estimated_total_cost,
|
||||
required_by_date=required_by_date,
|
||||
lead_time_buffer_days=lead_time_buffer_days,
|
||||
suggested_order_date=suggested_order_date,
|
||||
latest_order_date=latest_order_date,
|
||||
shelf_life_days=ingredient["vida_util_dias"],
|
||||
status=req_status,
|
||||
priority=req_priority,
|
||||
risk_level=req_risk_level,
|
||||
created_at=plan.created_at,
|
||||
updated_at=plan.updated_at
|
||||
)
|
||||
|
||||
db.add(requirement)
|
||||
total_estimated_cost += estimated_total_cost
|
||||
requirements_created += 1
|
||||
|
||||
# Update plan totals
|
||||
plan.total_requirements = num_requirements
|
||||
plan.total_estimated_cost = total_estimated_cost
|
||||
if status in ["approved", "in_execution", "completed"]:
|
||||
plan.total_approved_cost = total_estimated_cost * Decimal(str(random.uniform(0.95, 1.05)))
|
||||
|
||||
plans_created += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Successfully created {plans_created} plans with {requirements_created} requirements for {tenant_name}")
|
||||
|
||||
return {
|
||||
"tenant_id": str(tenant_id),
|
||||
"plans_created": plans_created,
|
||||
"requirements_created": requirements_created,
|
||||
"skipped": False
|
||||
}
|
||||
|
||||
|
||||
async def seed_all(db: AsyncSession):
|
||||
"""Seed all demo tenants with procurement data"""
|
||||
logger.info("Starting demo procurement seed process")
|
||||
|
||||
# Load configuration
|
||||
config = load_procurement_config()
|
||||
|
||||
results = []
|
||||
|
||||
# Seed San Pablo (Individual Bakery)
|
||||
result_san_pablo = await generate_procurement_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_SAN_PABLO,
|
||||
"San Pablo - Individual Bakery",
|
||||
"individual_bakery",
|
||||
config
|
||||
)
|
||||
results.append(result_san_pablo)
|
||||
|
||||
# Seed La Espiga (Central Bakery)
|
||||
result_la_espiga = await generate_procurement_for_tenant(
|
||||
db,
|
||||
DEMO_TENANT_LA_ESPIGA,
|
||||
"La Espiga - Central Bakery",
|
||||
"central_bakery",
|
||||
config
|
||||
)
|
||||
results.append(result_la_espiga)
|
||||
|
||||
total_plans = sum(r["plans_created"] for r in results)
|
||||
total_requirements = sum(r["requirements_created"] for r in results)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total_plans_created": total_plans,
|
||||
"total_requirements_created": total_requirements,
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution function"""
|
||||
# Get database URL from environment
|
||||
database_url = os.getenv("ORDERS_DATABASE_URL")
|
||||
if not database_url:
|
||||
logger.error("ORDERS_DATABASE_URL environment variable must be set")
|
||||
return 1
|
||||
|
||||
# Ensure asyncpg driver
|
||||
if database_url.startswith("postgresql://"):
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
# Create async engine
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await seed_all(session)
|
||||
|
||||
logger.info(
|
||||
"Procurement seed completed successfully!",
|
||||
total_plans=result["total_plans_created"],
|
||||
total_requirements=result["total_requirements_created"],
|
||||
status=result["status"]
|
||||
)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "="*60)
|
||||
print("DEMO PROCUREMENT SEED SUMMARY")
|
||||
print("="*60)
|
||||
for tenant_result in result["results"]:
|
||||
tenant_id = tenant_result["tenant_id"]
|
||||
plans = tenant_result["plans_created"]
|
||||
requirements = tenant_result["requirements_created"]
|
||||
skipped = tenant_result.get("skipped", False)
|
||||
status = "SKIPPED (already exists)" if skipped else f"CREATED {plans} plans, {requirements} requirements"
|
||||
print(f"Tenant {tenant_id}: {status}")
|
||||
print(f"\nTotal Plans: {result['total_plans_created']}")
|
||||
print(f"Total Requirements: {result['total_requirements_created']}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Procurement seed failed: {str(e)}", exc_info=True)
|
||||
return 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
Reference in New Issue
Block a user