ultimatepos/Modules/IndustrialEngineering/Services/IntegrationBridgeService.php

299 lines
11 KiB
PHP

<?php
namespace Modules\IndustrialEngineering\Services;
use Illuminate\Support\Facades\DB;
use Modules\IndustrialEngineering\Models\Bom;
use Modules\IndustrialEngineering\Models\BomLine;
use Modules\IndustrialEngineering\Models\CustomerInstalledAsset;
use Modules\IndustrialEngineering\Models\ItemMaster;
use Modules\IndustrialEngineering\Models\LineTemplate;
use Modules\IndustrialEngineering\Support\CmmsAvailability;
use Modules\Maintenance\Models\Equipment as MaintenanceEquipment;
use Modules\Maintenance\Models\EquipmentPart;
use Modules\Maintenance\Models\PartCatalog;
/**
* Bridges IndustrialEngineering domain to existing Manufacturing, Maintenance, Project, and Sales modules.
*/
class IntegrationBridgeService
{
public function __construct(
protected TraceabilityService $traceabilityService,
protected BomService $bomService
) {
}
/**
* Link sold transaction to installed asset and sync to Maintenance equipment.
*/
public function linkSaleToInstalledAsset(
CustomerInstalledAsset $asset,
int $sellTransactionId,
int $sellLineId
): CustomerInstalledAsset {
$asset->update([
'sell_transaction_id' => $sellTransactionId,
'sell_line_id' => $sellLineId,
]);
return $this->syncToMaintenanceEquipment($asset);
}
/**
* Create or update maintenance_equipment from installed asset for CMMS/service.
*/
public function syncToMaintenanceEquipment(CustomerInstalledAsset $asset): CustomerInstalledAsset
{
$asset->load(['serializedUnit', 'customer', 'lineRevision']);
$equipmentData = [
'business_id' => $asset->business_id,
'customer_id' => $asset->customer_id,
'project_id' => $asset->project_id,
'code' => $asset->asset_code,
'name' => $asset->name,
'serial_number' => $asset->serial_number,
'manufacturer' => null,
'model' => $asset->lineRevision?->name,
'installation_date' => $asset->installed_at,
'warranty_expires_at' => null,
'location' => $asset->site_location,
'status' => $asset->installation_status === 'installed' ? 'active' : 'inactive',
'notes' => 'Synced from Industrial Engineering installed asset #'.$asset->id,
];
if ($asset->maintenance_equipment_id) {
MaintenanceEquipment::where('id', $asset->maintenance_equipment_id)->update($equipmentData);
} else {
$equipment = MaintenanceEquipment::create($equipmentData);
$asset->update(['maintenance_equipment_id' => $equipment->id]);
$this->syncComponentsToEquipmentParts($asset, $equipment);
}
return $asset->fresh(['maintenanceEquipment']);
}
/**
* Copy as-built components to maintenance_equipment_parts tree.
*/
public function syncComponentsToEquipmentParts(CustomerInstalledAsset $asset, MaintenanceEquipment $equipment): void
{
$unit = $asset->serializedUnit;
if (! $unit) {
return;
}
$unit->load('components.itemMaster');
foreach ($unit->components()->whereNull('parent_component_id')->get() as $component) {
$this->createEquipmentPartRecursive($equipment, $component, null);
}
$asset->update([
'as_installed_snapshot' => $this->traceabilityService->buildAsBuiltSnapshot($unit),
'installation_status' => 'installed',
'installed_at' => now()->toDateString(),
]);
}
protected function createEquipmentPartRecursive(
MaintenanceEquipment $equipment,
$component,
?int $parentPartId
): void {
$catalogId = null;
if ($component->item_master_id) {
$catalog = PartCatalog::firstOrCreate(
[
'business_id' => $equipment->business_id,
'catalog_code' => $component->itemMaster?->item_code ?? 'CMP-'.$component->id,
],
[
'name' => $component->component_label,
'manufacturer' => $component->manufacturer,
'manufacturer_part_number' => $component->model,
'part_type' => $component->itemMaster?->item_type ?? 'mechanical',
]
);
$catalogId = $catalog->id;
}
$part = EquipmentPart::create([
'business_id' => $equipment->business_id,
'equipment_id' => $equipment->id,
'parent_id' => $parentPartId,
'part_catalog_id' => $catalogId,
'part_name' => $component->component_label,
'part_code' => $component->position_code,
'quantity' => $component->quantity,
'warranty_provider' => $component->warranty_provider,
'warranty_expires_at' => $component->warranty_expires_at,
'warranty_status' => $component->warranty_expires_at && $component->warranty_expires_at->isFuture() ? 'active' : 'none',
'notes' => trim(implode(' | ', array_filter([
$component->serial_number ? 'SN: '.$component->serial_number : null,
$component->manufacturer ? 'Mfr: '.$component->manufacturer : null,
$component->brand ? 'Brand: '.$component->brand : null,
$component->model ? 'Model: '.$component->model : null,
]))),
]);
foreach ($component->children as $child) {
$this->createEquipmentPartRecursive($equipment, $child, $part->id);
}
}
/**
* Import legacy MfgRecipe as a flat MBOM for a line revision.
*/
public function importMfgRecipeToMbom(int $businessId, int $lineRevisionId, int $mfgRecipeId, string $bomCode): ?\Modules\IndustrialEngineering\Models\Bom
{
if (! class_exists(\Modules\Manufacturing\Entities\MfgRecipe::class)) {
return null;
}
$recipe = \Modules\Manufacturing\Entities\MfgRecipe::with('ingredients')->find($mfgRecipeId);
if (! $recipe) {
return null;
}
$bom = \Modules\IndustrialEngineering\Models\Bom::create([
'business_id' => $businessId,
'line_revision_id' => $lineRevisionId,
'bom_type' => 'mbom',
'bom_code' => $bomCode,
'name' => 'Imported from MfgRecipe #'.$recipe->id,
'revision_code' => 'A',
'status' => 'draft',
]);
$sort = 0;
foreach ($recipe->ingredients as $ingredient) {
$item = ItemMaster::firstOrCreate(
[
'business_id' => $businessId,
'variation_id' => $ingredient->variation_id,
],
[
'item_code' => 'VAR-'.$ingredient->variation_id,
'name' => 'Variation #'.$ingredient->variation_id,
'item_type' => 'part',
'status' => 'active',
]
);
\Modules\IndustrialEngineering\Models\BomLine::create([
'business_id' => $businessId,
'bom_id' => $bom->id,
'item_master_id' => $item->id,
'quantity' => $ingredient->quantity,
'sort_order' => $sort++,
]);
}
return $bom->fresh(['lines']);
}
/**
* Sync production line MBOM tree to Maintenance equipment parts (CMMS).
*/
public function syncLineBomToMaintenance(LineTemplate $line, Bom $mbom): array
{
if (! CmmsAvailability::isReady()) {
throw new \RuntimeException(__('industrialengineering::lang.cmms_not_installed'));
}
$line->load(['productPlatform', 'maintenanceEquipment']);
$equipment = $line->maintenanceEquipment;
if (! $equipment) {
$equipment = MaintenanceEquipment::create([
'business_id' => $line->business_id,
'code' => $line->effectiveCode(),
'name' => $line->name,
'model' => $line->productPlatform?->name,
'status' => 'active',
'notes' => __('industrialengineering::lang.cmms_sync_from_ie', ['line' => $line->name]),
]);
$line->update(['maintenance_equipment_id' => $equipment->id]);
}
EquipmentPart::where('equipment_id', $equipment->id)->delete();
$partsCount = 0;
$mbom->load(['rootLines.itemMaster', 'rootLines.children.itemMaster']);
foreach ($mbom->rootLines as $rootLine) {
$partsCount += $this->createEquipmentPartFromBomLine($equipment, $rootLine, null);
}
$line->update(['cmms_synced_at' => now()]);
return [
'line' => $line->fresh(['maintenanceEquipment']),
'parts_count' => $partsCount,
];
}
/**
* @return int Number of parts created (including children)
*/
protected function createEquipmentPartFromBomLine(
MaintenanceEquipment $equipment,
BomLine $bomLine,
?int $parentPartId
): int {
$bomLine->loadMissing(['itemMaster', 'children.itemMaster']);
$item = $bomLine->itemMaster;
$catalogId = null;
if ($item) {
$catalog = PartCatalog::firstOrCreate(
[
'business_id' => $equipment->business_id,
'catalog_code' => $item->item_code,
],
[
'name' => $item->name,
'part_type' => $this->mapItemTypeToPartType($item->item_type),
'manufacturer_part_number' => $item->manufacturer_part_number,
]
);
$catalogId = $catalog->id;
}
$part = EquipmentPart::create([
'business_id' => $equipment->business_id,
'equipment_id' => $equipment->id,
'parent_id' => $parentPartId,
'part_catalog_id' => $catalogId,
'part_name' => $item?->name ?? 'Component',
'part_code' => $bomLine->position_code ?: $bomLine->find_number,
'quantity' => $bomLine->quantity,
'part_type' => $this->mapItemTypeToPartType($item?->item_type ?? 'part'),
'is_consumable' => $item?->is_consumable ?? false,
'notes' => trim(implode(' | ', array_filter([
$item?->item_code ? 'IE:'.$item->item_code : null,
$bomLine->find_number ? 'FN:'.$bomLine->find_number : null,
]))),
]);
$count = 1;
foreach ($bomLine->children as $child) {
$count += $this->createEquipmentPartFromBomLine($equipment, $child, $part->id);
}
return $count;
}
protected function mapItemTypeToPartType(?string $itemType): string
{
return match ($itemType) {
'assembly', 'subassembly' => 'assembly',
'device' => 'mechanical',
'tool' => 'tool',
'consumable' => 'consumable',
default => 'mechanical',
};
}
}