resolveBom($mbom, true); $result['cmms_synced'] = false; $result['cmms_parts_count'] = 0; $line->refresh(); if ($syncCmms && CmmsAvailability::isReady()) { try { $sync = $this->integrationBridge->syncLineBomToMaintenance($line, $mbom->fresh()); $line = $sync['line']; $result['cmms_synced'] = true; $result['cmms_parts_count'] = $sync['parts_count'] ?? 0; $result['messages'][] = ['type' => 'success', 'text' => __('industrialengineering::lang.cmms_synced_parts', [ 'code' => $line->effectiveCode(), 'count' => $result['cmms_parts_count'], ])]; } catch (\Throwable $e) { Log::warning('IE resolve CMMS: '.$e->getMessage()); $result['messages'][] = ['type' => 'warning', 'text' => __('industrialengineering::lang.cmms_sync_failed', ['error' => $e->getMessage()])]; } } $mbom->unsetRelation('lines'); $result['gate'] = $this->connectionGate->assessBom($mbom->fresh(), 1); $result['fully_resolved'] = ($result['gate']['can_run_mrp'] ?? false) && (! CmmsAvailability::isReady() || (bool) $line->fresh()->maintenance_equipment_id); $result['summary'] = $this->buildSummary($result); return $result; } public function resolveBom(Bom $bom, bool $createMissing = true): array { $gate = $this->connectionGate->assessBom($bom, 1); $itemIds = collect($gate['analysis']['leaf_lines'] ?? []) ->filter(fn ($row) => empty($row['variation_id']) && ! empty($row['item_master_id'])) ->pluck('item_master_id') ->unique() ->values(); $stats = ['matched' => 0, 'created' => 0, 'failed' => 0]; $messages = []; $failures = []; foreach ($itemIds as $itemId) { $item = ItemMaster::forBusiness($bom->business_id)->find($itemId); if (! $item) { continue; } $outcome = $this->resolveItem($item, $createMissing); if ($outcome['action'] === 'failed') { $stats['failed']++; } elseif ($outcome['action'] === 'created') { $stats['created']++; } else { $stats['matched']++; } if ($outcome['action'] === 'failed') { $failures[] = $outcome['message']; $messages[] = ['type' => 'warning', 'text' => $outcome['message']]; } elseif ($outcome['action'] === 'created') { $messages[] = ['type' => 'success', 'text' => __('industrialengineering::lang.erp_product_created', [ 'code' => $item->item_code, 'name' => $item->name, ])]; } else { $messages[] = ['type' => 'info', 'text' => $outcome['message']]; } } return [ 'stats' => $stats, 'messages' => $messages, 'failures' => $failures, 'gate_before' => $gate, 'gate' => $this->connectionGate->assessBom($bom->fresh(), 1), ]; } protected function resolveActorId(int $businessId): int { return ActorResolver::id($businessId) ?? 1; } public function resolveItem(ItemMaster $item, bool $createMissing = true): array { if ($item->variation_id) { return ['action' => 'matched', 'message' => __('industrialengineering::lang.auto_erp_already_linked')]; } $match = $this->erpItemLink->autoMatchProduct($item->fresh()); if ($match['linked']) { return ['action' => 'matched', 'message' => $match['message']]; } if (! $createMissing) { return ['action' => 'failed', 'message' => $match['message']]; } return $this->provisionErpProduct($item); } public function provisionErpProduct(ItemMaster $item): array { try { return DB::transaction(function () use ($item) { $unitId = Unit::where('business_id', $item->business_id) ->whereNull('base_unit_id') ->value('id') ?? Unit::where('business_id', $item->business_id)->value('id'); if (! $unitId) { throw new \RuntimeException(__('industrialengineering::lang.erp_no_unit')); } $sku = trim((string) $item->item_code) ?: ('IE-'.$item->id); $existingVariation = Variation::query() ->whereHas('product', fn ($q) => $q->where('business_id', $item->business_id)) ->where('sub_sku', $sku) ->first(); if ($existingVariation) { $this->erpItemLink->linkItem($item, ['variation_id' => $existingVariation->id]); return [ 'action' => 'matched', 'message' => __('industrialengineering::lang.auto_erp_matched', [ 'name' => $this->erpItemLink->variationLabel($existingVariation), ]), 'variation_id' => $existingVariation->id, ]; } $cost = max(0, (float) ($item->standard_cost ?? 0)); $product = Product::create([ 'business_id' => $item->business_id, 'name' => $item->name, 'type' => 'single', 'unit_id' => $unitId, 'sku' => $sku, 'enable_stock' => 1, 'not_for_selling' => 0, 'created_by' => $this->resolveActorId($item->business_id), 'tax_type' => 'exclusive', 'barcode_type' => 'C128', ]); $locationIds = BusinessLocation::where('business_id', $item->business_id)->pluck('id'); if ($locationIds->isNotEmpty()) { $product->product_locations()->sync($locationIds); } $this->productUtil->createSingleProductVariation( $product->id, $sku, $cost, $cost, 0, $cost, $cost ); $variation = $product->fresh()->variations()->first(); if (! $variation) { throw new \RuntimeException(__('industrialengineering::lang.erp_provision_failed', ['code' => $item->item_code])); } $this->erpItemLink->linkItem($item, ['variation_id' => $variation->id]); return [ 'action' => 'created', 'message' => __('industrialengineering::lang.erp_product_created', [ 'code' => $item->item_code, 'name' => $item->name, ]), 'product_id' => $product->id, 'variation_id' => $variation->id, ]; }); } catch (\Throwable $e) { Log::error('IE ERP provision '.$item->item_code.': '.$e->getMessage()); return [ 'action' => 'failed', 'message' => __('industrialengineering::lang.erp_provision_failed', ['code' => $item->item_code]).' — '.$e->getMessage(), ]; } } protected function buildSummary(array $result): string { $s = $result['stats']; $gate = $result['gate'] ?? []; $parts = [ __('industrialengineering::lang.resolve_summary_matched', ['n' => $s['matched'] ?? 0]), __('industrialengineering::lang.resolve_summary_created', ['n' => $s['created'] ?? 0]), ]; if (($s['failed'] ?? 0) > 0) { $parts[] = __('industrialengineering::lang.resolve_summary_failed', ['n' => $s['failed']]); } if (! empty($gate['can_run_mrp'])) { $parts[] = __('industrialengineering::lang.resolve_mrp_ready'); } elseif (($s['failed'] ?? 0) === 0 && ($s['matched'] ?? 0) === 0 && ($s['created'] ?? 0) === 0) { $parts[] = __('industrialengineering::lang.resolve_nothing_todo'); } return implode(' — ', $parts); } }