Improve the frontend 3
This commit is contained in:
568
services/procurement/app/services/procurement_service.py
Normal file
568
services/procurement/app/services/procurement_service.py
Normal file
@@ -0,0 +1,568 @@
|
||||
"""
|
||||
Procurement Service - ENHANCED VERSION
|
||||
Integrates advanced replenishment planning with:
|
||||
- Lead-time-aware planning
|
||||
- Dynamic safety stock
|
||||
- Inventory projection
|
||||
- Shelf-life management
|
||||
- MOQ optimization
|
||||
- Multi-criteria supplier selection
|
||||
|
||||
This is a COMPLETE REWRITE integrating all new planning services.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
from datetime import datetime, date, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
import structlog
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.procurement_plan import ProcurementPlan, ProcurementRequirement
|
||||
from app.models.replenishment import ReplenishmentPlan, ReplenishmentPlanItem
|
||||
from app.repositories.procurement_plan_repository import ProcurementPlanRepository, ProcurementRequirementRepository
|
||||
from app.schemas.procurement_schemas import (
|
||||
AutoGenerateProcurementRequest, AutoGenerateProcurementResponse
|
||||
)
|
||||
from app.core.config import settings
|
||||
from shared.clients.inventory_client import InventoryServiceClient
|
||||
from shared.clients.forecast_client import ForecastServiceClient
|
||||
from shared.clients.suppliers_client import SuppliersServiceClient
|
||||
from shared.clients.recipes_client import RecipesServiceClient
|
||||
from shared.config.base import BaseServiceSettings
|
||||
from shared.messaging.rabbitmq import RabbitMQClient
|
||||
from shared.monitoring.decorators import monitor_performance
|
||||
from shared.utils.tenant_settings_client import TenantSettingsClient
|
||||
|
||||
# NEW: Import all planning services
|
||||
from app.services.replenishment_planning_service import (
|
||||
ReplenishmentPlanningService,
|
||||
IngredientRequirement
|
||||
)
|
||||
from app.services.moq_aggregator import (
|
||||
MOQAggregator,
|
||||
ProcurementRequirement as MOQProcurementRequirement,
|
||||
SupplierConstraints
|
||||
)
|
||||
from app.services.supplier_selector import (
|
||||
SupplierSelector,
|
||||
SupplierOption
|
||||
)
|
||||
from app.services.recipe_explosion_service import RecipeExplosionService
|
||||
from app.services.smart_procurement_calculator import SmartProcurementCalculator
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class ProcurementService:
|
||||
"""
|
||||
Enhanced Procurement Service with Advanced Planning
|
||||
|
||||
NEW WORKFLOW:
|
||||
1. Generate forecast (from Orchestrator)
|
||||
2. Get current inventory
|
||||
3. Build ingredient requirements
|
||||
4. Generate replenishment plan (NEW - with all planning algorithms)
|
||||
5. Apply MOQ aggregation (NEW)
|
||||
6. Select suppliers (NEW - multi-criteria)
|
||||
7. Create purchase orders
|
||||
8. Save everything to database
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
config: BaseServiceSettings,
|
||||
inventory_client: Optional[InventoryServiceClient] = None,
|
||||
forecast_client: Optional[ForecastServiceClient] = None,
|
||||
suppliers_client: Optional[SuppliersServiceClient] = None,
|
||||
recipes_client: Optional[RecipesServiceClient] = None,
|
||||
):
|
||||
self.db = db
|
||||
self.config = config
|
||||
self.plan_repo = ProcurementPlanRepository(db)
|
||||
self.requirement_repo = ProcurementRequirementRepository(db)
|
||||
|
||||
# Initialize service clients
|
||||
self.inventory_client = inventory_client or InventoryServiceClient(config)
|
||||
self.forecast_client = forecast_client or ForecastServiceClient(config, "procurement-service")
|
||||
self.suppliers_client = suppliers_client or SuppliersServiceClient(config)
|
||||
self.recipes_client = recipes_client or RecipesServiceClient(config)
|
||||
|
||||
# Initialize tenant settings client
|
||||
tenant_service_url = getattr(config, 'TENANT_SERVICE_URL', 'http://tenant-service:8000')
|
||||
self.tenant_settings_client = TenantSettingsClient(tenant_service_url=tenant_service_url)
|
||||
|
||||
# Initialize RabbitMQ client
|
||||
rabbitmq_url = getattr(config, 'RABBITMQ_URL', 'amqp://guest:guest@localhost:5672/')
|
||||
self.rabbitmq_client = RabbitMQClient(rabbitmq_url, "procurement-service")
|
||||
|
||||
# Initialize Recipe Explosion Service
|
||||
self.recipe_explosion_service = RecipeExplosionService(
|
||||
config=config,
|
||||
recipes_client=self.recipes_client,
|
||||
inventory_client=self.inventory_client
|
||||
)
|
||||
|
||||
# Initialize Smart Calculator (keep for backward compatibility)
|
||||
self.smart_calculator = SmartProcurementCalculator(
|
||||
inventory_client=self.inventory_client,
|
||||
forecast_client=self.forecast_client
|
||||
)
|
||||
|
||||
# NEW: Initialize advanced planning services
|
||||
self.replenishment_planner = ReplenishmentPlanningService(
|
||||
projection_horizon_days=getattr(settings, 'REPLENISHMENT_PROJECTION_HORIZON_DAYS', 7),
|
||||
default_service_level=getattr(settings, 'REPLENISHMENT_SERVICE_LEVEL', 0.95),
|
||||
default_buffer_days=getattr(settings, 'REPLENISHMENT_BUFFER_DAYS', 1)
|
||||
)
|
||||
|
||||
self.moq_aggregator = MOQAggregator(
|
||||
consolidation_window_days=getattr(settings, 'MOQ_CONSOLIDATION_WINDOW_DAYS', 7),
|
||||
allow_early_ordering=getattr(settings, 'MOQ_ALLOW_EARLY_ORDERING', True)
|
||||
)
|
||||
|
||||
self.supplier_selector = SupplierSelector(
|
||||
price_weight=getattr(settings, 'SUPPLIER_PRICE_WEIGHT', 0.40),
|
||||
lead_time_weight=getattr(settings, 'SUPPLIER_LEAD_TIME_WEIGHT', 0.20),
|
||||
quality_weight=getattr(settings, 'SUPPLIER_QUALITY_WEIGHT', 0.20),
|
||||
reliability_weight=getattr(settings, 'SUPPLIER_RELIABILITY_WEIGHT', 0.20),
|
||||
diversification_threshold=getattr(settings, 'SUPPLIER_DIVERSIFICATION_THRESHOLD', Decimal('1000')),
|
||||
max_single_supplier_percentage=getattr(settings, 'SUPPLIER_MAX_SINGLE_PERCENTAGE', 0.70)
|
||||
)
|
||||
|
||||
logger.info("ProcurementServiceEnhanced initialized with advanced planning")
|
||||
|
||||
@monitor_performance("auto_generate_procurement_enhanced")
|
||||
async def auto_generate_procurement(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
request: AutoGenerateProcurementRequest
|
||||
) -> AutoGenerateProcurementResponse:
|
||||
"""
|
||||
Auto-generate procurement plan with ADVANCED PLANNING
|
||||
|
||||
NEW WORKFLOW (vs old):
|
||||
OLD: Forecast → Simple stock check → Create POs
|
||||
NEW: Forecast → Replenishment Planning → MOQ Optimization → Supplier Selection → Create POs
|
||||
"""
|
||||
try:
|
||||
target_date = request.target_date or date.today()
|
||||
forecast_data = request.forecast_data
|
||||
|
||||
logger.info("Starting ENHANCED auto-generate procurement",
|
||||
tenant_id=tenant_id,
|
||||
target_date=target_date,
|
||||
has_forecast_data=bool(forecast_data))
|
||||
|
||||
# ============================================================
|
||||
# STEP 1: Get Current Inventory (Use cached if available)
|
||||
# ============================================================
|
||||
if request.inventory_data:
|
||||
# Use cached inventory from Orchestrator (NEW)
|
||||
inventory_items = request.inventory_data.get('ingredients', [])
|
||||
logger.info(f"Using cached inventory snapshot: {len(inventory_items)} items")
|
||||
else:
|
||||
# Fallback: Fetch from Inventory Service
|
||||
inventory_items = await self._get_inventory_list(tenant_id)
|
||||
logger.info(f"Fetched inventory from service: {len(inventory_items)} items")
|
||||
|
||||
if not inventory_items:
|
||||
return AutoGenerateProcurementResponse(
|
||||
success=False,
|
||||
message="No inventory items found",
|
||||
errors=["Unable to retrieve inventory data"]
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# STEP 2: Get All Suppliers (Use cached if available)
|
||||
# ============================================================
|
||||
if request.suppliers_data:
|
||||
# Use cached suppliers from Orchestrator (NEW)
|
||||
suppliers = request.suppliers_data.get('suppliers', [])
|
||||
logger.info(f"Using cached suppliers snapshot: {len(suppliers)} suppliers")
|
||||
else:
|
||||
# Fallback: Fetch from Suppliers Service
|
||||
suppliers = await self._get_all_suppliers(tenant_id)
|
||||
logger.info(f"Fetched suppliers from service: {len(suppliers)} suppliers")
|
||||
|
||||
# ============================================================
|
||||
# STEP 3: Parse Forecast Data
|
||||
# ============================================================
|
||||
forecasts = self._parse_forecast_data(forecast_data, inventory_items)
|
||||
logger.info(f"Parsed {len(forecasts)} forecast items")
|
||||
|
||||
# ============================================================
|
||||
# STEP 4: Build Ingredient Requirements
|
||||
# ============================================================
|
||||
ingredient_requirements = await self._build_ingredient_requirements(
|
||||
tenant_id=tenant_id,
|
||||
forecasts=forecasts,
|
||||
inventory_items=inventory_items,
|
||||
suppliers=suppliers,
|
||||
target_date=target_date
|
||||
)
|
||||
|
||||
if not ingredient_requirements:
|
||||
logger.warning("No ingredient requirements generated")
|
||||
return AutoGenerateProcurementResponse(
|
||||
success=False,
|
||||
message="No procurement requirements identified",
|
||||
errors=["No items need replenishment"]
|
||||
)
|
||||
|
||||
logger.info(f"Built {len(ingredient_requirements)} ingredient requirements")
|
||||
|
||||
# ============================================================
|
||||
# STEP 5: Generate Replenishment Plan (NEW!)
|
||||
# ============================================================
|
||||
replenishment_plan = await self.replenishment_planner.generate_replenishment_plan(
|
||||
tenant_id=str(tenant_id),
|
||||
requirements=ingredient_requirements,
|
||||
forecast_id=forecast_data.get('forecast_id'),
|
||||
production_schedule_id=request.production_schedule_id
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Replenishment plan generated: {replenishment_plan.total_items} items, "
|
||||
f"{replenishment_plan.urgent_items} urgent, "
|
||||
f"{replenishment_plan.high_risk_items} high risk"
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# STEP 6: Apply MOQ Aggregation (NEW!)
|
||||
# ============================================================
|
||||
moq_requirements, supplier_constraints = self._prepare_moq_inputs(
|
||||
replenishment_plan,
|
||||
suppliers
|
||||
)
|
||||
|
||||
aggregated_orders = self.moq_aggregator.aggregate_requirements(
|
||||
requirements=moq_requirements,
|
||||
supplier_constraints=supplier_constraints
|
||||
)
|
||||
|
||||
moq_efficiency = self.moq_aggregator.calculate_order_efficiency(aggregated_orders)
|
||||
logger.info(
|
||||
f"MOQ aggregation: {len(aggregated_orders)} aggregated orders from "
|
||||
f"{len(moq_requirements)} requirements "
|
||||
f"(consolidation ratio: {moq_efficiency['consolidation_ratio']:.2f})"
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# STEP 7: Multi-Criteria Supplier Selection (NEW!)
|
||||
# ============================================================
|
||||
supplier_selections = await self._select_suppliers_for_requirements(
|
||||
replenishment_plan,
|
||||
suppliers
|
||||
)
|
||||
|
||||
logger.info(f"Supplier selection completed for {len(supplier_selections)} items")
|
||||
|
||||
# ============================================================
|
||||
# STEP 8: Save to Database
|
||||
# ============================================================
|
||||
# Create traditional procurement plan
|
||||
plan_data = {
|
||||
'tenant_id': tenant_id,
|
||||
'plan_number': await self._generate_plan_number(),
|
||||
'plan_date': target_date,
|
||||
'planning_horizon_days': request.planning_horizon_days,
|
||||
'status': 'draft',
|
||||
'forecast_id': forecast_data.get('forecast_id'),
|
||||
'production_schedule_id': request.production_schedule_id,
|
||||
'total_estimated_cost': replenishment_plan.total_estimated_cost,
|
||||
'seasonality_adjustment': Decimal('1.0')
|
||||
}
|
||||
|
||||
plan = await self.plan_repo.create_plan(plan_data)
|
||||
|
||||
# Create procurement requirements from replenishment plan
|
||||
requirements_data = self._convert_replenishment_to_requirements(
|
||||
plan_id=plan.id,
|
||||
tenant_id=tenant_id,
|
||||
replenishment_plan=replenishment_plan,
|
||||
supplier_selections=supplier_selections
|
||||
)
|
||||
|
||||
# Save requirements
|
||||
created_requirements = await self.requirement_repo.create_requirements_batch(requirements_data)
|
||||
|
||||
# Update plan totals
|
||||
await self.plan_repo.update_plan(plan.id, tenant_id, {
|
||||
'total_requirements': len(requirements_data),
|
||||
'primary_suppliers_count': len(set(
|
||||
r.get('preferred_supplier_id') for r in requirements_data
|
||||
if r.get('preferred_supplier_id')
|
||||
)),
|
||||
'supplier_diversification_score': moq_efficiency.get('consolidation_ratio', 1.0)
|
||||
})
|
||||
|
||||
# ============================================================
|
||||
# STEP 9: Optionally Create Purchase Orders
|
||||
# ============================================================
|
||||
created_pos = []
|
||||
if request.auto_create_pos:
|
||||
po_result = await self._create_purchase_orders_from_plan(
|
||||
tenant_id=tenant_id,
|
||||
plan_id=plan.id,
|
||||
auto_approve=request.auto_approve_pos
|
||||
)
|
||||
if po_result.get('success'):
|
||||
created_pos = po_result.get('created_pos', [])
|
||||
|
||||
await self.db.commit()
|
||||
|
||||
# ============================================================
|
||||
# STEP 10: Publish Events
|
||||
# ============================================================
|
||||
await self._publish_plan_generated_event(tenant_id, plan.id)
|
||||
|
||||
logger.info(
|
||||
"ENHANCED procurement plan completed successfully",
|
||||
tenant_id=tenant_id,
|
||||
plan_id=plan.id,
|
||||
requirements_count=len(requirements_data),
|
||||
pos_created=len(created_pos),
|
||||
urgent_items=replenishment_plan.urgent_items,
|
||||
high_risk_items=replenishment_plan.high_risk_items
|
||||
)
|
||||
|
||||
return AutoGenerateProcurementResponse(
|
||||
success=True,
|
||||
message="Enhanced procurement plan generated successfully",
|
||||
plan_id=plan.id,
|
||||
plan_number=plan.plan_number,
|
||||
requirements_created=len(requirements_data),
|
||||
purchase_orders_created=len(created_pos),
|
||||
purchase_orders_auto_approved=sum(1 for po in created_pos if po.get('auto_approved')),
|
||||
total_estimated_cost=replenishment_plan.total_estimated_cost,
|
||||
created_pos=created_pos
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await self.db.rollback()
|
||||
logger.error("Error in enhanced auto_generate_procurement",
|
||||
error=str(e), tenant_id=tenant_id, exc_info=True)
|
||||
return AutoGenerateProcurementResponse(
|
||||
success=False,
|
||||
message="Failed to generate enhanced procurement plan",
|
||||
errors=[str(e)]
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Helper Methods
|
||||
# ============================================================
|
||||
|
||||
async def _build_ingredient_requirements(
|
||||
self,
|
||||
tenant_id: uuid.UUID,
|
||||
forecasts: List[Dict],
|
||||
inventory_items: List[Dict],
|
||||
suppliers: List[Dict],
|
||||
target_date: date
|
||||
) -> List[IngredientRequirement]:
|
||||
"""
|
||||
Build ingredient requirements from forecasts
|
||||
"""
|
||||
requirements = []
|
||||
|
||||
for forecast in forecasts:
|
||||
ingredient_id = forecast.get('ingredient_id')
|
||||
ingredient = next((i for i in inventory_items if str(i['id']) == str(ingredient_id)), None)
|
||||
|
||||
if not ingredient:
|
||||
continue
|
||||
|
||||
# Calculate required quantity
|
||||
predicted_demand = Decimal(str(forecast.get('predicted_demand', 0)))
|
||||
current_stock = Decimal(str(ingredient.get('quantity', 0)))
|
||||
|
||||
if predicted_demand > current_stock:
|
||||
required_quantity = predicted_demand - current_stock
|
||||
|
||||
# Find preferred supplier
|
||||
preferred_supplier = self._find_preferred_supplier(ingredient, suppliers)
|
||||
|
||||
# Get lead time
|
||||
lead_time_days = preferred_supplier.get('lead_time_days', 3) if preferred_supplier else 3
|
||||
|
||||
# Build requirement
|
||||
req = IngredientRequirement(
|
||||
ingredient_id=str(ingredient_id),
|
||||
ingredient_name=ingredient.get('name', 'Unknown'),
|
||||
required_quantity=required_quantity,
|
||||
required_by_date=target_date + timedelta(days=7),
|
||||
supplier_id=str(preferred_supplier['id']) if preferred_supplier else None,
|
||||
lead_time_days=lead_time_days,
|
||||
shelf_life_days=ingredient.get('shelf_life_days'),
|
||||
is_perishable=ingredient.get('category') in ['fresh', 'dairy', 'produce'],
|
||||
category=ingredient.get('category', 'dry'),
|
||||
unit_of_measure=ingredient.get('unit_of_measure', 'kg'),
|
||||
current_stock=current_stock,
|
||||
daily_consumption_rate=float(predicted_demand) / 7, # Estimate
|
||||
demand_std_dev=float(forecast.get('confidence_score', 0)) * 10 # Rough estimate
|
||||
)
|
||||
|
||||
requirements.append(req)
|
||||
|
||||
return requirements
|
||||
|
||||
def _prepare_moq_inputs(
|
||||
self,
|
||||
replenishment_plan,
|
||||
suppliers: List[Dict]
|
||||
) -> Tuple[List[MOQProcurementRequirement], Dict[str, SupplierConstraints]]:
|
||||
"""
|
||||
Prepare inputs for MOQ aggregator
|
||||
"""
|
||||
moq_requirements = []
|
||||
supplier_constraints = {}
|
||||
|
||||
for item in replenishment_plan.items:
|
||||
req = MOQProcurementRequirement(
|
||||
id=str(item.id),
|
||||
ingredient_id=item.ingredient_id,
|
||||
ingredient_name=item.ingredient_name,
|
||||
quantity=item.final_order_quantity,
|
||||
required_date=item.required_by_date,
|
||||
supplier_id=item.supplier_id or 'unknown',
|
||||
unit_of_measure=item.unit_of_measure
|
||||
)
|
||||
moq_requirements.append(req)
|
||||
|
||||
# Build supplier constraints
|
||||
for supplier in suppliers:
|
||||
supplier_id = str(supplier['id'])
|
||||
supplier_constraints[supplier_id] = SupplierConstraints(
|
||||
supplier_id=supplier_id,
|
||||
supplier_name=supplier.get('name', 'Unknown'),
|
||||
min_order_quantity=Decimal(str(supplier.get('min_order_quantity', 0))) if supplier.get('min_order_quantity') else None,
|
||||
min_order_value=Decimal(str(supplier.get('min_order_value', 0))) if supplier.get('min_order_value') else None,
|
||||
package_size=None, # Not in current schema
|
||||
max_order_quantity=None # Not in current schema
|
||||
)
|
||||
|
||||
return moq_requirements, supplier_constraints
|
||||
|
||||
async def _select_suppliers_for_requirements(
|
||||
self,
|
||||
replenishment_plan,
|
||||
suppliers: List[Dict]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Select best suppliers for each requirement
|
||||
"""
|
||||
selections = {}
|
||||
|
||||
for item in replenishment_plan.items:
|
||||
# Build supplier options
|
||||
supplier_options = []
|
||||
for supplier in suppliers:
|
||||
option = SupplierOption(
|
||||
supplier_id=str(supplier['id']),
|
||||
supplier_name=supplier.get('name', 'Unknown'),
|
||||
unit_price=Decimal(str(supplier.get('unit_price', 10))), # Default price
|
||||
lead_time_days=supplier.get('lead_time_days', 3),
|
||||
min_order_quantity=Decimal(str(supplier.get('min_order_quantity', 0))) if supplier.get('min_order_quantity') else None,
|
||||
quality_score=0.85, # Default quality
|
||||
reliability_score=0.90 # Default reliability
|
||||
)
|
||||
supplier_options.append(option)
|
||||
|
||||
if supplier_options:
|
||||
# Select suppliers
|
||||
result = self.supplier_selector.select_suppliers(
|
||||
ingredient_id=item.ingredient_id,
|
||||
ingredient_name=item.ingredient_name,
|
||||
required_quantity=item.final_order_quantity,
|
||||
supplier_options=supplier_options
|
||||
)
|
||||
|
||||
selections[item.ingredient_id] = result
|
||||
|
||||
return selections
|
||||
|
||||
def _convert_replenishment_to_requirements(
|
||||
self,
|
||||
plan_id: uuid.UUID,
|
||||
tenant_id: uuid.UUID,
|
||||
replenishment_plan,
|
||||
supplier_selections: Dict
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Convert replenishment plan items to procurement requirements
|
||||
"""
|
||||
requirements_data = []
|
||||
|
||||
for item in replenishment_plan.items:
|
||||
# Get supplier selection
|
||||
selection = supplier_selections.get(item.ingredient_id)
|
||||
primary_allocation = selection.allocations[0] if selection and selection.allocations else None
|
||||
|
||||
req_data = {
|
||||
'procurement_plan_id': plan_id,
|
||||
'tenant_id': tenant_id,
|
||||
'ingredient_id': uuid.UUID(item.ingredient_id),
|
||||
'ingredient_name': item.ingredient_name,
|
||||
'required_quantity': item.final_order_quantity,
|
||||
'unit_of_measure': item.unit_of_measure,
|
||||
'estimated_unit_price': primary_allocation.unit_price if primary_allocation else Decimal('10'),
|
||||
'estimated_total_cost': primary_allocation.total_cost if primary_allocation else item.final_order_quantity * Decimal('10'),
|
||||
'required_by_date': item.required_by_date,
|
||||
'priority': 'urgent' if item.is_urgent else 'normal',
|
||||
'preferred_supplier_id': uuid.UUID(primary_allocation.supplier_id) if primary_allocation else None,
|
||||
'calculation_method': 'ENHANCED_REPLENISHMENT_PLANNING',
|
||||
'ai_suggested_quantity': item.base_quantity,
|
||||
'adjusted_quantity': item.final_order_quantity,
|
||||
'adjustment_reason': f"Safety stock: {item.safety_stock_quantity}, Shelf-life adjusted",
|
||||
'lead_time_days': item.lead_time_days
|
||||
}
|
||||
|
||||
requirements_data.append(req_data)
|
||||
|
||||
return requirements_data
|
||||
|
||||
# Additional helper methods (shortened for brevity)
|
||||
async def _get_inventory_list(self, tenant_id):
|
||||
"""Get inventory items"""
|
||||
return await self.inventory_client.get_ingredients(str(tenant_id))
|
||||
|
||||
async def _get_all_suppliers(self, tenant_id):
|
||||
"""Get all suppliers"""
|
||||
return await self.suppliers_client.get_suppliers(str(tenant_id))
|
||||
|
||||
def _parse_forecast_data(self, forecast_data, inventory_items):
|
||||
"""Parse forecast data from orchestrator"""
|
||||
forecasts = forecast_data.get('forecasts', [])
|
||||
return forecasts
|
||||
|
||||
def _find_preferred_supplier(self, ingredient, suppliers):
|
||||
"""Find preferred supplier for ingredient"""
|
||||
# Simple: return first supplier (can be enhanced with logic)
|
||||
return suppliers[0] if suppliers else None
|
||||
|
||||
async def _generate_plan_number(self):
|
||||
"""Generate unique plan number"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
return f"PLAN-{timestamp}"
|
||||
|
||||
async def _create_purchase_orders_from_plan(self, tenant_id, plan_id, auto_approve):
|
||||
"""Create POs from plan (placeholder)"""
|
||||
return {'success': True, 'created_pos': []}
|
||||
|
||||
async def _publish_plan_generated_event(self, tenant_id, plan_id):
|
||||
"""Publish plan generated event"""
|
||||
try:
|
||||
await self.rabbitmq_client.publish_event(
|
||||
exchange='procurement',
|
||||
routing_key='plan.generated',
|
||||
message={
|
||||
'tenant_id': str(tenant_id),
|
||||
'plan_id': str(plan_id),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to publish event: {e}")
|
||||
Reference in New Issue
Block a user