163 lines
5.2 KiB
PHP
163 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace Modules\SupplyChain\Services;
|
|
|
|
use App\Utils\ModuleUtil;
|
|
use App\Variation;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\IndustrialEngineering\Models\MrpRun;
|
|
use Modules\SupplyChain\Models\DemandForecast;
|
|
|
|
class SupplyChainIntegrationService
|
|
{
|
|
public function __construct(
|
|
protected DemandForecastService $demandForecastService,
|
|
protected RfqService $rfqService,
|
|
protected ModuleUtil $moduleUtil
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Hook after IE MRP run — import shortages as demand forecasts and optionally create RFQ.
|
|
*
|
|
* @param array<string, mixed>|MrpRun $runOrData
|
|
* @return array<string, mixed>
|
|
*/
|
|
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<int, float> 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<string, mixed> $analysis
|
|
* @return array<string, mixed>
|
|
*/
|
|
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<string, mixed> $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;
|
|
}
|
|
}
|