299 lines
11 KiB
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',
|
|
};
|
|
}
|
|
}
|