353 lines
12 KiB
PHP
353 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Modules\AdvancedPlanning\Services;
|
|
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Modules\AdvancedPlanning\Models\CapacityResource;
|
|
|
|
class IntegrationBridgeService
|
|
{
|
|
public function isIndustrialEngineeringAvailable(): bool
|
|
{
|
|
return \Module::has('IndustrialEngineering')
|
|
&& \Module::isEnabled('IndustrialEngineering')
|
|
&& class_exists(\Modules\IndustrialEngineering\Models\ManufacturingWorkOrder::class);
|
|
}
|
|
|
|
public function isShopFloorAvailable(): bool
|
|
{
|
|
return \Module::has('ShopFloor') && \Module::isEnabled('ShopFloor');
|
|
}
|
|
|
|
public function isMaintenanceAvailable(): bool
|
|
{
|
|
return \Module::has('Maintenance')
|
|
&& \Module::isEnabled('Maintenance')
|
|
&& class_exists(\Modules\Maintenance\Models\Equipment::class);
|
|
}
|
|
|
|
/**
|
|
* Sync capacity resources from IE line templates, work centers, and CMMS equipment.
|
|
*
|
|
* @return array{created: int, updated: int}
|
|
*/
|
|
public function syncCapacityResources(int $businessId): array
|
|
{
|
|
$created = 0;
|
|
$updated = 0;
|
|
|
|
if ($this->isIndustrialEngineeringAvailable()) {
|
|
$result = $this->syncFromIndustrialEngineering($businessId);
|
|
$created += $result['created'];
|
|
$updated += $result['updated'];
|
|
}
|
|
|
|
if ($this->isMaintenanceAvailable()) {
|
|
$result = $this->syncFromMaintenanceEquipment($businessId);
|
|
$created += $result['created'];
|
|
$updated += $result['updated'];
|
|
}
|
|
|
|
if ($this->isShopFloorAvailable()) {
|
|
$result = $this->syncFromShopFloor($businessId);
|
|
$created += $result['created'];
|
|
$updated += $result['updated'];
|
|
}
|
|
|
|
return ['created' => $created, 'updated' => $updated];
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, object>
|
|
*/
|
|
public function loadSchedulableWorkOrders(
|
|
int $businessId,
|
|
Carbon $horizonStart,
|
|
Carbon $horizonEnd,
|
|
array $options = []
|
|
): Collection {
|
|
if (! $this->isIndustrialEngineeringAvailable()) {
|
|
return collect();
|
|
}
|
|
|
|
$woClass = \Modules\IndustrialEngineering\Models\ManufacturingWorkOrder::class;
|
|
$query = $woClass::forBusiness($businessId)
|
|
->whereIn('status', $options['statuses'] ?? ['planned', 'released', 'in_progress'])
|
|
->with(['lineRevision']);
|
|
|
|
if (! ($options['ignore_dates'] ?? false)) {
|
|
$query->where(function ($q) use ($horizonStart, $horizonEnd) {
|
|
$q->whereNull('planned_end_date')
|
|
->orWhereBetween('planned_start_date', [$horizonStart, $horizonEnd])
|
|
->orWhereBetween('planned_end_date', [$horizonStart, $horizonEnd]);
|
|
});
|
|
}
|
|
|
|
if (! empty($options['work_order_ids'])) {
|
|
$query->whereIn('id', $options['work_order_ids']);
|
|
}
|
|
|
|
return $query->orderBy('planned_start_date')->orderBy('id')->get();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function extractRoutingOperations($workOrder): array
|
|
{
|
|
$snapshot = $workOrder->routing_snapshot ?? [];
|
|
|
|
if (is_string($snapshot)) {
|
|
$snapshot = json_decode($snapshot, true) ?: [];
|
|
}
|
|
|
|
if (! is_array($snapshot) || empty($snapshot)) {
|
|
return [];
|
|
}
|
|
|
|
return collect($snapshot)
|
|
->sortBy('sequence')
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function loadMrpDemandForPeriod(int $businessId, Carbon $monthStart, Carbon $monthEnd): array
|
|
{
|
|
if (! class_exists(\Modules\IndustrialEngineering\Models\MrpRun::class)) {
|
|
return [];
|
|
}
|
|
|
|
$runs = \Modules\IndustrialEngineering\Models\MrpRun::forBusiness($businessId)
|
|
->whereBetween('created_at', [$monthStart, $monthEnd])
|
|
->with('workOrder')
|
|
->latest('id')
|
|
->limit(50)
|
|
->get();
|
|
|
|
return $runs->map(fn ($run) => [
|
|
'mrp_run_id' => $run->id,
|
|
'run_code' => $run->run_code,
|
|
'planned_quantity' => (float) $run->planned_quantity,
|
|
'work_order_id' => $run->work_order_id,
|
|
'work_order_number' => $run->workOrder?->work_order_number,
|
|
])->values()->all();
|
|
}
|
|
|
|
/**
|
|
* @return array{created: int, updated: int}
|
|
*/
|
|
protected function syncFromIndustrialEngineering(int $businessId): array
|
|
{
|
|
$created = 0;
|
|
$updated = 0;
|
|
$defaultHours = (float) config('advancedplanning.default_daily_capacity_hours', 8);
|
|
$defaultEff = (float) config('advancedplanning.default_efficiency_pct', 85);
|
|
|
|
if (class_exists(\Modules\IndustrialEngineering\Models\LineTemplate::class)) {
|
|
$lines = \Modules\IndustrialEngineering\Models\LineTemplate::forBusiness($businessId)
|
|
->productionLines()
|
|
->get();
|
|
|
|
foreach ($lines as $line) {
|
|
$attrs = [
|
|
'name' => $line->name,
|
|
'resource_type' => 'line',
|
|
'ie_line_template_id' => $line->id,
|
|
'maintenance_equipment_id' => $line->maintenance_equipment_id,
|
|
'daily_capacity_hours' => $defaultHours,
|
|
'efficiency_pct' => $defaultEff,
|
|
'is_active' => true,
|
|
];
|
|
|
|
$resource = CapacityResource::forBusiness($businessId)
|
|
->where('ie_line_template_id', $line->id)
|
|
->first();
|
|
|
|
if ($resource) {
|
|
$resource->update($attrs);
|
|
$updated++;
|
|
} else {
|
|
CapacityResource::create(array_merge($attrs, ['business_id' => $businessId]));
|
|
$created++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (class_exists(\Modules\IndustrialEngineering\Models\WorkCenter::class)) {
|
|
$centers = \Modules\IndustrialEngineering\Models\WorkCenter::forBusiness($businessId)
|
|
->where('is_active', true)
|
|
->get();
|
|
|
|
foreach ($centers as $center) {
|
|
$type = ((float) $center->machine_rate_per_hour > 0) ? 'equipment' : 'labor';
|
|
$attrs = [
|
|
'name' => $center->name,
|
|
'resource_type' => $type,
|
|
'ie_work_center_id' => $center->id,
|
|
'daily_capacity_hours' => $defaultHours,
|
|
'efficiency_pct' => $defaultEff,
|
|
'is_active' => true,
|
|
];
|
|
|
|
$resource = CapacityResource::forBusiness($businessId)
|
|
->where('ie_work_center_id', $center->id)
|
|
->first();
|
|
|
|
if ($resource) {
|
|
$resource->update($attrs);
|
|
$updated++;
|
|
} else {
|
|
CapacityResource::create(array_merge($attrs, ['business_id' => $businessId]));
|
|
$created++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ['created' => $created, 'updated' => $updated];
|
|
}
|
|
|
|
/**
|
|
* @return array{created: int, updated: int}
|
|
*/
|
|
protected function syncFromMaintenanceEquipment(int $businessId): array
|
|
{
|
|
$created = 0;
|
|
$updated = 0;
|
|
|
|
$equipment = \Modules\Maintenance\Models\Equipment::where('business_id', $businessId)->get();
|
|
$defaultHours = (float) config('advancedplanning.default_daily_capacity_hours', 8);
|
|
$defaultEff = (float) config('advancedplanning.default_efficiency_pct', 85);
|
|
|
|
foreach ($equipment as $item) {
|
|
$existing = CapacityResource::forBusiness($businessId)
|
|
->where('maintenance_equipment_id', $item->id)
|
|
->whereNull('ie_line_template_id')
|
|
->first();
|
|
|
|
$attrs = [
|
|
'name' => $item->name ?? $item->equipment_code ?? 'Equipment #'.$item->id,
|
|
'resource_type' => 'equipment',
|
|
'maintenance_equipment_id' => $item->id,
|
|
'daily_capacity_hours' => $defaultHours,
|
|
'efficiency_pct' => $defaultEff,
|
|
'is_active' => true,
|
|
];
|
|
|
|
if ($existing) {
|
|
$existing->update($attrs);
|
|
$updated++;
|
|
} else {
|
|
CapacityResource::create(array_merge($attrs, ['business_id' => $businessId]));
|
|
$created++;
|
|
}
|
|
}
|
|
|
|
return ['created' => $created, 'updated' => $updated];
|
|
}
|
|
|
|
/**
|
|
* Schedule work orders created by an MRP run within the given horizon.
|
|
*/
|
|
public function scheduleMrpWorkOrders(
|
|
int $businessId,
|
|
Carbon $horizonStart,
|
|
Carbon $horizonEnd,
|
|
FiniteCapacitySchedulerService $scheduler,
|
|
?int $mrpRunId = null
|
|
): ?\Modules\AdvancedPlanning\Models\ScheduleRun {
|
|
if (! $this->isIndustrialEngineeringAvailable()) {
|
|
return null;
|
|
}
|
|
|
|
$options = ['ignore_dates' => true];
|
|
if ($mrpRunId) {
|
|
$run = \Modules\IndustrialEngineering\Models\MrpRun::forBusiness($businessId)->find($mrpRunId);
|
|
if ($run?->work_order_id) {
|
|
$options['work_order_ids'] = [$run->work_order_id];
|
|
}
|
|
}
|
|
|
|
return $scheduler->run($businessId, $horizonStart, $horizonEnd, 'MRP Follow-up', $options);
|
|
}
|
|
|
|
/**
|
|
* @return array{created: int, updated: int}
|
|
*/
|
|
protected function syncFromShopFloor(int $businessId): array
|
|
{
|
|
$created = 0;
|
|
$updated = 0;
|
|
|
|
if (! class_exists(\Modules\ShopFloor\Models\WorkCenter::class)) {
|
|
return ['created' => 0, 'updated' => 0];
|
|
}
|
|
|
|
$defaultHours = (float) config('advancedplanning.default_daily_capacity_hours', 8);
|
|
$defaultEff = (float) config('advancedplanning.default_efficiency_pct', 85);
|
|
|
|
$centers = \Modules\ShopFloor\Models\WorkCenter::where('business_id', $businessId)
|
|
->where('status', 'active')
|
|
->get();
|
|
|
|
foreach ($centers as $center) {
|
|
$dailyHours = $defaultHours;
|
|
if ((float) $center->capacity_per_hour > 0) {
|
|
$dailyHours = min(24, (float) $center->capacity_per_hour * $defaultHours);
|
|
}
|
|
|
|
$efficiency = $this->shopFloorEfficiencyForCenter($businessId, (int) $center->id, $defaultEff);
|
|
|
|
$attrs = [
|
|
'name' => $center->name.' ('.$center->code.')',
|
|
'resource_type' => 'equipment',
|
|
'ie_line_template_id' => $center->ie_line_template_id,
|
|
'maintenance_equipment_id' => $center->maintenance_equipment_id,
|
|
'daily_capacity_hours' => round($dailyHours, 2),
|
|
'efficiency_pct' => $efficiency,
|
|
'is_active' => true,
|
|
];
|
|
|
|
$resourceQuery = CapacityResource::forBusiness($businessId);
|
|
if ($center->ie_line_template_id) {
|
|
$resource = (clone $resourceQuery)->where('ie_line_template_id', $center->ie_line_template_id)->first();
|
|
} elseif ($center->maintenance_equipment_id) {
|
|
$resource = (clone $resourceQuery)->where('maintenance_equipment_id', $center->maintenance_equipment_id)->first();
|
|
} else {
|
|
$resource = (clone $resourceQuery)->where('name', $attrs['name'])->first();
|
|
}
|
|
|
|
if ($resource) {
|
|
$resource->update($attrs);
|
|
$updated++;
|
|
} else {
|
|
CapacityResource::create(array_merge($attrs, ['business_id' => $businessId]));
|
|
$created++;
|
|
}
|
|
}
|
|
|
|
return ['created' => $created, 'updated' => $updated];
|
|
}
|
|
|
|
protected function shopFloorEfficiencyForCenter(int $businessId, int $workCenterId, float $fallback): float
|
|
{
|
|
if (! class_exists(\Modules\ShopFloor\Models\OeeSnapshot::class)) {
|
|
return $fallback;
|
|
}
|
|
|
|
$oee = \Modules\ShopFloor\Models\OeeSnapshot::where('business_id', $businessId)
|
|
->where('work_center_id', $workCenterId)
|
|
->orderByDesc('snapshot_date')
|
|
->value('oee_pct');
|
|
|
|
if ($oee === null || (float) $oee <= 0) {
|
|
return $fallback;
|
|
}
|
|
|
|
return max(1, min(100, round((float) $oee, 2)));
|
|
}
|
|
}
|