import asyncio import calendar import datetime import json import logging import math from datetime import date, timedelta from typing import Dict, List, Optional, Tuple import aiohttp import httpx import numpy as np import requests from sqlalchemy import select from sqlalchemy.orm import selectinload from src.config import REALIBILITY_SERVICE_API, RBD_SERVICE_API from src.database.core import CollectorDbSession, DbSession from src.calculation_time_constrains.model import CalculationData, CalculationEquipmentResult from src.calculation_time_constrains.schema import CalculationResultsRead from src.calculation_time_constrains.utils import ( calculate_failures_per_month, create_time_series_data, get_months_between, ) from src.overhaul_scope.service import get as get_scope, get_prev_oh from src.sparepart.service import load_sparepart_data_from_db log = logging.getLogger(__name__) class OptimumCostModelWithSpareparts: def __init__( self, token: str, last_oh_date: date, next_oh_date: date, sparepart_manager, time_window_months: Optional[int] = None, base_url: str = "http://192.168.1.82:8000", ): """ Initialize the Optimum Cost Model with sparepart management """ self.api_base_url = base_url self.token = token self.last_oh_date = last_oh_date self.next_oh_date = next_oh_date self.session = None self.sparepart_manager = sparepart_manager # Calculate planned overhaul interval in months self.planned_oh_months = get_months_between(last_oh_date, next_oh_date) # Set analysis time window: next OH + 6 months self.time_window_months = time_window_months or (self.planned_oh_months + 6) # Pre-calculate date range for API calls self.date_range = self._generate_date_range() self.logger = log def _generate_date_range(self) -> List[datetime.datetime]: """Generate date range for analysis based on time window""" dates = [] current_date = datetime.datetime.combine(self.last_oh_date, datetime.datetime.min.time()) end_date = current_date + timedelta(days=self.time_window_months * 30) while current_date <= end_date: dates.append(current_date) current_date += timedelta(days=31) return dates async def _create_session(self): """Create aiohttp session with connection pooling""" if self.session is None: timeout = aiohttp.ClientTimeout(total=300) connector = aiohttp.TCPConnector( limit=500, limit_per_host=200, ttl_dns_cache=300, use_dns_cache=True, force_close=False, enable_cleanup_closed=True, ) self.session = aiohttp.ClientSession( timeout=timeout, connector=connector, headers={"Authorization": f"Bearer {self.token}"}, ) async def _close_session(self): """Close aiohttp session""" if self.session: await self.session.close() self.session = None async def get_failures_prediction(self, location_tag: str): """Get failure predictions for equipment from Reliability Predict service""" start_date = self.last_oh_date.strftime("%Y-%m-%d") end_date_val = self.next_oh_date + timedelta(days=6 * 30) end_date = end_date_val.strftime("%Y-%m-%d") predict_url = f"{REALIBILITY_SERVICE_API}/main/predict/{location_tag}/{start_date}/{end_date}" try: response = requests.get( predict_url, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {self.token}", }, timeout=30, ) response.raise_for_status() prediction_data = response.json() except (requests.RequestException, ValueError) as e: self.logger.error(f"Failed to fetch prediction data for {location_tag}: {e}") return None predictions = ( prediction_data.get("data", {}).get("predictions") if prediction_data.get("data") else None ) if not predictions: self.logger.warning(f"No prediction data available for {location_tag}") return None monthly_data = {} cumulative = 0.0 for i, pred in enumerate(predictions): month_key = pred["month"] monthly_fail = pred["predicted_failures"] source = pred.get("source", "predicted") cumulative += monthly_fail monthly_data[month_key] = { "cumulative_failures": cumulative, "monthly_failures": monthly_fail, "month_index": i + 1, "source": source, } return monthly_data async def get_simulation_results(self, simulation_id: str = "default"): """Get simulation results for Birnbaum importance calculations""" headers = { "Authorization": f"Bearer {self.token}", "Content-Type": "application/json", } calc_result_url = f"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode" plant_result_url = f"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}/plant" async with httpx.AsyncClient(timeout=300.0) as client: calc_task = client.get(calc_result_url, headers=headers) plant_task = client.get(plant_result_url, headers=headers) calc_response, plant_response = await asyncio.gather(calc_task, plant_task) calc_response.raise_for_status() plant_response.raise_for_status() calc_data = calc_response.json()["data"] plant_data = plant_response.json()["data"] return {"calc_result": calc_data, "plant_result": plant_data} def _calculate_equipment_costs_with_spareparts( self, failures_prediction: Dict, birnbaum_importance: float, preventive_cost: float, failure_replacement_cost: float, ecs, location_tag: str, planned_overhauls: List = None, loss_production_permonth=0, ) -> List[Dict]: """Calculate costs for each month including sparepart costs and availability""" if not failures_prediction: self.logger.warning(f"No failure prediction data for {location_tag}") return [] results = [] months = list(failures_prediction.keys()) num_months = len(months) failure_counts = [] monthly_risk_cost_per_failure = 0 if ecs: is_trip = 1 if ecs.get("Diskripsi Operasional Akibat Equip. Failure") == "Trip" else 0 is_series = 0 if not birnbaum_importance else math.floor(birnbaum_importance) if is_trip: downtime = ecs.get("Estimasi Waktu Maint. / Downtime / Gangguan (Jam)") monthly_risk_cost_per_failure = 660 * 1000000 * is_trip * downtime * is_series for month_key in months: data = failures_prediction[month_key] failure_counts.append(data["cumulative_failures"]) for i in range(num_months): month_index = i + 1 if month_index > self.time_window_months: continue sparepart_analysis = self._analyze_sparepart_availability( location_tag, month_index - 1, planned_overhauls or [] ) total_expected_failure_cost = failure_counts[i] * ( failure_replacement_cost + monthly_risk_cost_per_failure ) procurement_cost = sparepart_analysis["total_procurement_cost"] total_preventive_cost = preventive_cost + procurement_cost total_cycle_cost = total_expected_failure_cost + total_preventive_cost cput = total_cycle_cost / month_index results.append( { "month": month_index, "number_of_failures": failure_counts[i], "is_actual": failures_prediction[months[i]].get("source") == "actual", "failure_cost": total_expected_failure_cost / month_index, "preventive_cost": preventive_cost / month_index, "procurement_cost": procurement_cost / month_index, "total_cost": cput, "absolute_failure_cost": total_expected_failure_cost, "absolute_overhaul_cost": preventive_cost, "absolute_procurement_cost": procurement_cost, "total_cycle_cost": total_cycle_cost, "is_after_planned_oh": month_index > self.planned_oh_months, "delay_months": max(0, month_index - self.planned_oh_months), "procurement_details": sparepart_analysis, "sparepart_available": sparepart_analysis["available"], "sparepart_status": sparepart_analysis["message"], "can_proceed": sparepart_analysis["can_proceed_with_delays"], } ) return results def _analyze_sparepart_availability( self, equipment_tag: str, target_month: int, planned_overhauls: List ) -> Dict: """Analyze sparepart availability for equipment at target month""" if not self.sparepart_manager: return { "available": True, "message": "Sparepart manager not initialized", "total_procurement_cost": 0, "procurement_needed": [], "can_proceed_with_delays": True, } other_overhauls = [ (eq_tag, month) for eq_tag, month in planned_overhauls if eq_tag != equipment_tag and month <= target_month ] return self.sparepart_manager.check_sparepart_availability( equipment_tag, target_month, other_overhauls ) def _find_optimal_timing_with_spareparts( self, cost_results: List[Dict], location_tag: str ) -> Optional[Dict]: """Find optimal timing considering sparepart constraints""" if not cost_results: return None feasible_results = [r for r in cost_results if r["can_proceed"]] min_cost = float("inf") optimal_result = None for i, result in enumerate(cost_results): if result in feasible_results and result["total_cost"] < min_cost: min_cost = result["total_cost"] optimal_result = result if optimal_result is None: return None return self._create_optimal_result(optimal_result, location_tag, "OPTIMAL") def _create_optimal_result( self, optimal_result: Dict, location_tag: str, status: str ) -> Dict: """Create standardized optimal result dictionary""" return { "location_tag": location_tag, "optimal_month": optimal_result["month"], "optimal_index": optimal_result["month"] - 1, "optimal_cost": optimal_result["total_cost"], "failure_cost": optimal_result["failure_cost"], "preventive_cost": optimal_result["preventive_cost"], "procurement_cost": optimal_result["procurement_cost"], "number_of_failures": optimal_result["number_of_failures"], "is_delayed": optimal_result["is_after_planned_oh"], "delay_months": optimal_result["delay_months"], "planned_oh_month": self.planned_oh_months, "planned_cost": None, "cost_vs_planned": None, "savings_from_delay": 0, "cost_of_delay": 0, "sparepart_available": optimal_result["sparepart_available"], "sparepart_status": optimal_result["sparepart_status"], "procurement_details": optimal_result["procurement_details"], "optimization_status": status, "all_monthly_costs": [], } async def calculate_cost_all_equipment_with_spareparts( self, db_session, collector_db_session, equipments: List, calculation, preventive_cost: float, simulation_id: str = "default", ): """ Calculate optimal overhaul timing for entire fleet considering sparepart constraints """ self.logger.info( f"Starting fleet optimization with reliability prediction for {len(equipments)} equipment" ) max_interval = self.time_window_months # Phase 1: Calculate individual optimal timings without considering interactions individual_results = {} try: with open("src/calculation_time_constrains/full_equipment_with_downtime_opdesc.json", "r") as f: data = json.load(f) ecs_tags = {eq["Location"]: eq for eq in data} except FileNotFoundError: ecs_tags = {} for equipment in equipments: location_tag = equipment.location_tag contribution_factor = 1.0 ecs = ecs_tags.get(location_tag, None) monthly_data = await self.get_failures_prediction(location_tag) if not monthly_data: continue equipment_preventive_cost = equipment.overhaul_cost + equipment.service_cost failure_replacement_cost = equipment.material_cost + (3 * 111000 * 3) cost_results = self._calculate_equipment_costs_with_spareparts( failures_prediction=monthly_data, birnbaum_importance=contribution_factor, preventive_cost=equipment_preventive_cost, failure_replacement_cost=failure_replacement_cost, location_tag=location_tag, planned_overhauls=[], ecs=ecs, loss_production_permonth=0, ) if not cost_results: continue optimal_timing = self._find_optimal_timing_with_spareparts(cost_results, location_tag) if optimal_timing: optimal_timing["all_monthly_costs"] = cost_results individual_results[location_tag] = optimal_timing self.logger.info( f"Individual optimal for {location_tag}: Month {optimal_timing['optimal_month']}" ) # Phase 2: Optimize considering sparepart interactions self.logger.info("Phase 2: Optimizing with sparepart interactions...") improved_plan = self._optimize_fleet_with_sparepart_constraints( individual_results, equipments, simulation_id ) # Phase 3: Generate final results and database objects fleet_results = [] total_corrective_costs = np.zeros(max_interval) total_preventive_costs = np.zeros(max_interval) total_procurement_costs = np.zeros(max_interval) total_costs = np.zeros(max_interval) total_fleet_procurement_cost = 0 for equipment in equipments: location_tag = equipment.location_tag if location_tag not in individual_results: continue equipment_timing = next( (month for tag, month in improved_plan if tag == location_tag), individual_results[location_tag]["optimal_month"], ) cost_data = individual_results[location_tag]["all_monthly_costs"][equipment_timing - 1] all_costs = individual_results[location_tag]["all_monthly_costs"] corrective_costs = [r["failure_cost"] for r in all_costs] preventive_costs = [r["preventive_cost"] for r in all_costs] procurement_costs = [r["procurement_cost"] for r in all_costs] failures = [r["number_of_failures"] for r in all_costs] total_costs_equipment = [r["total_cost"] for r in all_costs] procurement_details = [r["procurement_details"] for r in all_costs] def pad_array(arr, target_length): if len(arr) < target_length: return arr + [arr[-1]] * (target_length - len(arr)) return arr[:target_length] corrective_costs = pad_array(corrective_costs, max_interval) preventive_costs = pad_array(preventive_costs, max_interval) procurement_costs = pad_array(procurement_costs, max_interval) failures = pad_array(failures, max_interval) total_costs_equipment = pad_array(total_costs_equipment, max_interval) procurement_details = pad_array(procurement_details, max_interval) is_actual_list = [r.get("is_actual", False) for r in all_costs] is_actual_list = pad_array(is_actual_list, max_interval) equipment_result = CalculationEquipmentResult( corrective_costs=corrective_costs, overhaul_costs=preventive_costs, procurement_costs=procurement_costs, daily_failures=failures, is_actual=is_actual_list, location_tag=equipment.location_tag, material_cost=equipment.material_cost, service_cost=equipment.service_cost, optimum_day=equipment_timing - 1, calculation_data_id=calculation.id, procurement_details=procurement_details, ) fleet_results.append(equipment_result) total_corrective_costs += np.array(corrective_costs) total_preventive_costs += np.array(preventive_costs) total_procurement_costs += np.array(procurement_costs) total_costs += np.array(total_costs_equipment) total_fleet_procurement_cost += cost_data["procurement_cost"] fleet_optimal_index = np.argmin(total_costs) calculation.optimum_oh_day = int(fleet_optimal_index) calculation.max_interval = max_interval calculation.rbd_simulation_id = simulation_id db_session.add_all(fleet_results) await db_session.commit() return int(fleet_optimal_index) def _optimize_fleet_with_sparepart_constraints( self, individual_results: Dict, equipments: List, simulation_id: str ) -> List[Tuple[str, int]]: """ Optimize fleet overhaul timing considering sparepart sharing constraints """ current_plan = [(tag, result["optimal_month"]) for tag, result in individual_results.items()] current_plan.sort(key=lambda x: x[1]) improved_plan = [] processed_equipment = [] for equipment_tag, optimal_month in current_plan: sparepart_analysis = self.sparepart_manager.check_sparepart_availability( equipment_tag, optimal_month - 1, processed_equipment ) if sparepart_analysis["available"] or sparepart_analysis["can_proceed_with_delays"]: improved_plan.append((equipment_tag, optimal_month)) processed_equipment.append((equipment_tag, optimal_month)) else: alternative_month = self._find_alternative_timing( equipment_tag, optimal_month, individual_results[equipment_tag]["all_monthly_costs"], processed_equipment, ) if alternative_month: improved_plan.append((equipment_tag, alternative_month)) processed_equipment.append((equipment_tag, alternative_month)) else: improved_plan.append((equipment_tag, optimal_month)) processed_equipment.append((equipment_tag, optimal_month)) return improved_plan def _find_alternative_timing( self, equipment_tag: str, preferred_month: int, cost_results: List[Dict], processed_equipment: List[Tuple[str, int]], ) -> Optional[int]: """ Find alternative timing when preferred month has sparepart constraints """ search_range = 6 candidates = [] for offset in range(-search_range // 2, search_range // 2 + 1): candidate_month = preferred_month + offset if candidate_month <= 0 or candidate_month > len(cost_results): continue if candidate_month == preferred_month: continue sparepart_analysis = self.sparepart_manager.check_sparepart_availability( equipment_tag, candidate_month - 1, processed_equipment ) if sparepart_analysis["available"] or sparepart_analysis["can_proceed_with_delays"]: cost_data = cost_results[candidate_month - 1] candidates.append((candidate_month, cost_data["total_cost"])) if not candidates: return None candidates.sort(key=lambda x: x[1]) return candidates[0][0] async def __aenter__(self): await self._create_session() return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self._close_session() def _analyze_optimal_timing( calculation_results: List, optimum_oh_day: int, prev_oh_scope, scope_overhaul ) -> Dict: """Analyze optimal timing and provide recommendations""" if not calculation_results: return {} optimal_result = None if 0 <= optimum_oh_day < len(calculation_results): optimal_result = calculation_results[optimum_oh_day] planned_oh_months = None if prev_oh_scope and scope_overhaul: planned_oh_months = get_months_between(prev_oh_scope.end_date, scope_overhaul.start_date) timing_recommendation = "OPTIMAL" if planned_oh_months: if optimum_oh_day + 1 < planned_oh_months: timing_recommendation = "EARLY" elif optimum_oh_day + 1 > planned_oh_months: timing_recommendation = "DELAYED" else: timing_recommendation = "ON_SCHEDULE" cost_trend = "STABLE" if len(calculation_results) > 1: early_costs = [r.total_cost for r in calculation_results[: len(calculation_results) // 3]] late_costs = [r.total_cost for r in calculation_results[-len(calculation_results) // 3 :]] avg_early = sum(early_costs) / len(early_costs) if early_costs else 0 avg_late = sum(late_costs) / len(late_costs) if late_costs else 0 if avg_late > avg_early * 1.2: cost_trend = "INCREASING" elif avg_late < avg_early * 0.8: cost_trend = "DECREASING" return { "optimal_month": optimum_oh_day + 1, "planned_month": planned_oh_months, "timing_recommendation": timing_recommendation, "optimal_total_cost": optimal_result.total_cost if optimal_result else 0, "optimal_breakdown": { "corrective_cost": optimal_result.corrective_cost if optimal_result else 0, "overhaul_cost": optimal_result.overhaul_cost if optimal_result else 0, "procurement_cost": optimal_result.procurement_cost if optimal_result else 0, "num_failures": optimal_result.num_failures if optimal_result else 0, }, "cost_trend": cost_trend, "months_from_planned": (optimum_oh_day + 1 - planned_oh_months) if planned_oh_months else None, "cost_savings_vs_planned": None, "sparepart_impact": { "equipment_with_constraints": optimal_result.sparepart_summary["equipment_requiring_procurement"] if optimal_result else 0, "critical_shortages": optimal_result.sparepart_summary["critical_shortages"] if optimal_result else 0, "procurement_investment": optimal_result.sparepart_summary["total_procurement_cost"] if optimal_result else 0, }, } async def run_simulation_with_spareparts( *, db_session: DbSession, calculation, token: str, collector_db_session: CollectorDbSession, time_window_months: Optional[int] = None, simulation_id: str = "default", ) -> Dict: """ Run complete overhaul optimization simulation with sparepart management """ from src.optimum_time_constraint.service import get_calculation_data_by_id from src.overhaul_activity.service import get_standard_scope_by_session_id equipments = await get_standard_scope_by_session_id( db_session=db_session, overhaul_session_id=calculation.overhaul_session_id, collector_db=collector_db_session, ) scope = await get_scope( db_session=db_session, overhaul_session_id=calculation.overhaul_session_id ) prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope) calculation_data = await get_calculation_data_by_id( db_session=db_session, calculation_id=calculation.id ) time_window_months = get_months_between(prev_oh_scope.end_date, scope.start_date) + 6 sparepart_manager = await load_sparepart_data_from_db( scope=scope, prev_oh_scope=prev_oh_scope, db_session=collector_db_session, app_db_session=db_session, analysis_window_months=time_window_months, ) optimum_oh_model = OptimumCostModelWithSpareparts( token=token, last_oh_date=prev_oh_scope.end_date, next_oh_date=scope.start_date, base_url=RBD_SERVICE_API, sparepart_manager=sparepart_manager, ) try: fleet_optimal_index = await optimum_oh_model.calculate_cost_all_equipment_with_spareparts( db_session=db_session, collector_db_session=collector_db_session, equipments=equipments, calculation=calculation_data, preventive_cost=calculation_data.parameter.overhaul_cost, simulation_id=simulation_id, ) finally: await optimum_oh_model._close_session() calculation_query = await db_session.execute( select(CalculationData) .options( selectinload(CalculationData.equipment_results), selectinload(CalculationData.parameter), ) .where(CalculationData.id == calculation.id) ) scope_calculation = calculation_query.scalar_one_or_none() data_num = scope_calculation.max_interval all_equipment = scope_calculation.equipment_results included_equipment = [eq for eq in all_equipment if eq.is_included] calculation_results = [] fleet_statistics = { "total_equipment": len(all_equipment), "included_equipment": len(included_equipment), "excluded_equipment": len(all_equipment) - len(included_equipment), "equipment_with_sparepart_constraints": 0, "total_procurement_items": 0, "critical_procurement_items": 0, "total_spareparts": 745, } avg_failure_cost = ( sum((eq.material_cost or 0) + (3 * 111000 * 3) for eq in all_equipment) / len(all_equipment) if all_equipment else 0 ) rbd_marginal_fails = [0] * data_num try: if scope_calculation.rbd_simulation_id: plant_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{scope_calculation.rbd_simulation_id}/plant" async with httpx.AsyncClient(timeout=30.0) as client: response = await client.get( plant_result_url, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) if response.status_code == 200: plant_data = response.json().get("data", {}) timestamp_outs = plant_data.get("timestamp_outs", []) if timestamp_outs: hourly_data = create_time_series_data( timestamp_outs, max_hours=data_num * 720 ) cumulative_rbd_fails = calculate_failures_per_month(hourly_data) rbd_fails_map = {m["month"]: m["failures"] for m in cumulative_rbd_fails} prev_fail = 0 for m in range(1, data_num + 1): curr_fail = rbd_fails_map.get(m, prev_fail) rbd_marginal_fails[m - 1] = curr_fail - prev_fail prev_fail = curr_fail except Exception as e: logger = logging.getLogger(__name__) logger.warning(f"Failed to fetch plant simulation: {e}") cumulative_plant_failures = 0 for month_index in range(data_num): historical_marginal_fail = 0 for eq in all_equipment: if eq.is_actual and month_index < len(eq.is_actual) and eq.is_actual[month_index]: curr_fail = ( eq.daily_failures[month_index] if month_index < len(eq.daily_failures) else 0 ) prev_fail = ( eq.daily_failures[month_index - 1] if month_index > 0 and (month_index - 1) < len(eq.daily_failures) else 0 ) historical_marginal_fail += max(0, curr_fail - prev_fail) marginal_fail = rbd_marginal_fails[month_index] + historical_marginal_fail cumulative_plant_failures += marginal_fail month_result = { "overhaul_cost": 0.0, "corrective_cost": 0.0, "procurement_cost": 0.0, "num_failures": cumulative_plant_failures, "day": month_index + 1, "month": month_index + 1, "procurement_details": {}, "sparepart_summary": { "total_procurement_cost": 0.0, "equipment_requiring_procurement": 0, "critical_shortages": 0, "existing_orders_value": 0.0, "new_orders_required": 0, "urgent_procurements": 0, }, } equipment_requiring_procurement = 0 total_existing_orders_value = 0.0 total_new_orders_value = 0.0 critical_shortages = 0 urgent_procurements = 0 for eq in all_equipment: if month_index >= len(eq.procurement_details): continue procurement_detail = eq.procurement_details[month_index] if ( procurement_detail and isinstance(procurement_detail, dict) and procurement_detail.get("procurement_needed") ): equipment_requiring_procurement += 1 pr_po_summary = procurement_detail.get("pr_po_summary", {}) existing_orders_value = pr_po_summary.get("total_existing_value", 0) total_existing_orders_value += existing_orders_value new_orders_value = pr_po_summary.get("total_new_orders_value", 0) total_new_orders_value += new_orders_value critical_missing = procurement_detail.get("critical_missing_parts", 0) if critical_missing > 0: critical_shortages += 1 recommendations = procurement_detail.get("recommendations", []) urgent_items = [ r for r in recommendations if r.get("priority") == "CRITICAL" ] if urgent_items: urgent_procurements += 1 is_included_eq = False if eq.is_initial else eq.is_included month_result["procurement_details"][eq.location_tag] = { "is_included": is_included_eq, "location_tag": eq.location_tag, "details": procurement_detail.get("procurement_needed", []), "detailed_message": procurement_detail.get("detailed_message", ""), "pr_po_summary": pr_po_summary, "recommendations": recommendations, "sparepart_available": procurement_detail.get("sparepart_available", True), "can_proceed": procurement_detail.get("can_proceed_with_delays", True), "critical_missing_parts": critical_missing, "existing_orders_value": existing_orders_value, "new_orders_value": new_orders_value, } if eq.is_included: if month_index < len(eq.overhaul_costs) and month_index < len( eq.procurement_costs ): month_result["overhaul_cost"] += float(eq.overhaul_costs[month_index]) month_result["procurement_cost"] += float(eq.procurement_costs[month_index]) month_result["corrective_cost"] = ( cumulative_plant_failures * avg_failure_cost ) / (month_index + 1) month_result["sparepart_summary"].update( { "total_procurement_cost": month_result["procurement_cost"], "equipment_requiring_procurement": equipment_requiring_procurement, "critical_shortages": critical_shortages, "existing_orders_value": total_existing_orders_value, "new_orders_required": len( [ eq for eq in all_equipment if month_index < len(eq.procurement_details) and eq.procurement_details[month_index] and eq.procurement_details[month_index].get("procurement_needed") ] ), "urgent_procurements": urgent_procurements, } ) month_result["total_cost"] = ( month_result["corrective_cost"] + month_result["overhaul_cost"] + month_result["procurement_cost"] ) calculation_results.append(month_result) optimum_day = np.argmin([month["total_cost"] for month in calculation_results]) scope_calculation.optimum_oh_day = int(optimum_day) fleet_statistics["equipment_with_sparepart_constraints"] = len( [ eq for eq in all_equipment if any( detail and detail.get("procurement_needed") for detail in eq.procurement_details if detail ) ] ) fleet_statistics["total_procurement_items"] = sum( [ len(detail.get("procurement_needed", [])) for eq in all_equipment for detail in eq.procurement_details if detail and isinstance(detail, dict) ] ) analysis_metadata = { "planned_month": (scope.start_date.year - prev_oh_scope.end_date.year) * 12 + (scope.start_date.month - prev_oh_scope.end_date.month) if prev_oh_scope and scope else 0, "total_fleet_procurement_cost": sum( [ eq.procurement_costs[int(scope_calculation.optimum_oh_day)] for eq in all_equipment if eq.procurement_costs ] ), "last_oh_date": prev_oh_scope.end_date.isoformat() if prev_oh_scope else None, "next_oh_date": scope.start_date.isoformat() if scope else None, "optimal_stat": None, } calc_results_read = [CalculationResultsRead(**r) for r in calculation_results] optimal_analysis = _analyze_optimal_timing( calc_results_read, scope_calculation.optimum_oh_day, prev_oh_scope, scope ) scope_calculation.plant_results = calculation_results scope_calculation.fleet_statistics = fleet_statistics scope_calculation.analysis_metadata = analysis_metadata scope_calculation.optimum_analysis = optimal_analysis await db_session.commit() return {"id": calculation.id, "optimum": optimal_analysis}