|MrpRun $runOrData * @return array */ public function afterMrpRun($runOrData): array { if (! $this->moduleUtil->isModuleInstalled('SupplyChain')) { return ['forecasts' => 0, 'rfq_id' => null]; } $run = $runOrData instanceof MrpRun ? $runOrData : null; $businessId = $run?->business_id ?? (int) ($runOrData['business_id'] ?? 0); $analysis = $run?->shortage_analysis ?? ($runOrData['shortage_analysis'] ?? []); $shortages = $analysis['shortages'] ?? []; if ($businessId <= 0 || empty($shortages)) { return ['forecasts' => 0, 'rfq_id' => null]; } $forecastMonth = now()->addMonth()->startOfMonth(); $count = 0; foreach ($shortages as $shortage) { $productId = $this->resolveProductId($shortage); if (! $productId) { continue; } DemandForecast::updateOrCreate( [ 'business_id' => $businessId, 'product_id' => $productId, 'variation_id' => ! empty($shortage['variation_id']) ? (int) $shortage['variation_id'] : null, 'forecast_month' => $forecastMonth->toDateString(), 'method' => 'mrp_shortage', ], [ 'forecast_qty' => (float) ($shortage['shortage_qty'] ?? 0), 'confidence' => 85.0, 'source' => 'mrp', 'meta' => [ 'mrp_run_id' => $run?->id, 'mrp_run_code' => $run?->run_code, ], ] ); $count++; } return ['forecasts' => $count, 'rfq_id' => null]; } /** * Create RFQ from latest MRP run shortages. */ public function createRfqFromMrpRun(int $businessId, int $mrpRunId, ?array $supplierIds = null): ?int { if (! $this->moduleUtil->isModuleInstalled('IndustrialEngineering') || ! Schema::hasTable('ie_mrp_runs')) { return null; } $run = MrpRun::forBusiness($businessId)->find($mrpRunId); if (! $run || empty($run->shortage_analysis['shortages'])) { return null; } $rfq = $this->rfqService->createFromMrpShortages( $businessId, $run->shortage_analysis['shortages'], __('supplychain::lang.rfq_from_mrp_run', ['code' => $run->run_code]), $supplierIds ); return $rfq->id; } /** * Demand inputs for MRP — merges forecast qty into planned demand. * * @return array variation_id => additional_qty */ public function getMrpDemandInputs(int $businessId, ?Carbon $month = null): array { try { return $this->demandForecastService->getForecastByVariation($businessId, $month); } catch (\Throwable $e) { Log::warning('SupplyChain MRP demand inputs failed: '.$e->getMessage()); return []; } } /** * Apply demand forecast adjustments to MRP shortage analysis. * * @param array $analysis * @return array */ public function enrichMrpAnalysis(int $businessId, array $analysis): array { $forecasts = $this->getMrpDemandInputs($businessId); if (empty($forecasts) || empty($analysis['shortages'])) { return $analysis; } foreach ($analysis['shortages'] as $idx => $shortage) { $variationId = (int) ($shortage['variation_id'] ?? 0); if ($variationId && isset($forecasts[$variationId])) { $additional = $forecasts[$variationId]; $currentShortage = (float) ($shortage['shortage_qty'] ?? 0); $analysis['shortages'][$idx]['forecast_demand_qty'] = $additional; $analysis['shortages'][$idx]['shortage_qty'] = $currentShortage + $additional; } } $analysis['supply_chain_forecast_applied'] = true; return $analysis; } /** * @param array $shortage */ protected function resolveProductId(array $shortage): ?int { if (! empty($shortage['product_id'])) { return (int) $shortage['product_id']; } if (! empty($shortage['variation_id'])) { $productId = Variation::where('id', (int) $shortage['variation_id'])->value('product_id'); return $productId ? (int) $productId : null; } return null; } }