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', }; } }