log("Начало импорта каталога запчастей и справочника расшифровок"); DB::beginTransaction(); try { // Загружаем файл Excel $spreadsheet = IOFactory::load($this->filePath); // Импорт каталога запчастей (первая вкладка) $this->importSpareParts($spreadsheet->getSheet(0)); // Импорт справочника расшифровок (вторая вкладка, если есть) if ($spreadsheet->getSheetCount() > 1) { $this->importPricingCodes($spreadsheet->getSheet(1)); } DB::commit(); $this->log("Импорт успешно завершён"); return [ 'success' => true, 'logs' => $this->logs, ]; } catch (Exception $e) { DB::rollBack(); throw $e; } } catch (Exception $e) { $this->log("ОШИБКА: " . $e->getMessage()); Log::error("Ошибка импорта запчастей: " . $e->getMessage(), [ 'trace' => $e->getTraceAsString(), ]); return [ 'success' => false, 'error' => $e->getMessage(), 'logs' => $this->logs, ]; } } private function importSpareParts($sheet): void { $this->log("Импорт каталога запчастей..."); $highestRow = $sheet->getHighestRow(); $imported = 0; $updated = 0; $skipped = 0; // Начинаем со 2-й строки (пропускаем заголовок) for ($row = 2; $row <= $highestRow; $row++) { $article = trim($sheet->getCell('B' . $row)->getValue()); // Пропускаем пустые строки if (empty($article)) { $skipped++; continue; } // Парсим множественные шифры расценки (через запятую, точку с запятой или перенос строки) $pricingCodesRaw = $sheet->getCell('L' . $row)->getValue(); $pricingCodeStrings = []; if (!empty($pricingCodesRaw)) { $pricingCodeStrings = preg_split('/[,;\n]+/', $pricingCodesRaw); $pricingCodeStrings = array_map('trim', $pricingCodeStrings); $pricingCodeStrings = array_filter($pricingCodeStrings); } $data = [ 'article' => $article, 'used_in_maf' => $sheet->getCell('C' . $row)->getValue(), 'note' => $sheet->getCell('G' . $row)->getValue(), 'purchase_price' => $this->parsePrice($sheet->getCell('H' . $row)->getValue()), 'customer_price' => $this->parsePrice($sheet->getCell('I' . $row)->getValue()), 'expertise_price' => $this->parsePrice($sheet->getCell('J' . $row)->getValue()), 'tsn_number' => $sheet->getCell('K' . $row)->getValue(), 'min_stock' => (int)$sheet->getCell('M' . $row)->getValue() ?: 0, ]; // Создаём или обновляем запчасть (включая удалённые) $sparePart = SparePart::withTrashed()->where('article', $article)->first(); if ($sparePart) { // Восстанавливаем, если была удалена if ($sparePart->trashed()) { $sparePart->restore(); } $sparePart->update($data); $updated++; } else { $sparePart = SparePart::create($data); $imported++; } // Синхронизируем связи с pricing_codes $this->syncPricingCodes($sparePart, $pricingCodeStrings); } $this->log("Каталог запчастей: импортировано {$imported}, обновлено {$updated}, пропущено {$skipped}"); } private function importPricingCodes($sheet): void { $this->log("Импорт справочника расшифровок..."); $highestRow = $sheet->getHighestRow(); $imported = 0; $updated = 0; $skipped = 0; // Начинаем со 2-й строки (пропускаем заголовок) for ($row = 2; $row <= $highestRow; $row++) { $typeText = trim($sheet->getCell('B' . $row)->getValue()); $code = trim($sheet->getCell('C' . $row)->getValue()); $description = trim($sheet->getCell('D' . $row)->getValue()); // Пропускаем пустые строки if (empty($code)) { $skipped++; continue; } // Определяем тип $type = null; if (strpos($typeText, 'ТСН') !== false) { $type = PricingCode::TYPE_TSN_NUMBER; } elseif (strpos($typeText, 'Шифр') !== false || strpos($typeText, 'расценк') !== false) { $type = PricingCode::TYPE_PRICING_CODE; } if (!$type) { $this->log("Предупреждение: неизвестный тип '{$typeText}' для кода '{$code}' в строке {$row}"); $skipped++; continue; } // Создаём или обновляем запись $pricingCode = PricingCode::where('type', $type)->where('code', $code)->first(); if ($pricingCode) { $pricingCode->update(['description' => $description]); $updated++; } else { PricingCode::create([ 'type' => $type, 'code' => $code, 'description' => $description, ]); $imported++; } } $this->log("Справочник расшифровок: импортировано {$imported}, обновлено {$updated}, пропущено {$skipped}"); } /** * Парсинг цены * Если цена уже в копейках (целое число > 1000), оставляем как есть * Если цена в рублях (дробное или < 1000), конвертируем в копейки */ private function parsePrice($value): ?int { if (empty($value)) { return null; } $price = (float)$value; // Если число целое и большое (скорее всего уже в копейках) if ($price == (int)$price && $price >= 1000) { return (int)$price; } // Иначе конвертируем из рублей в копейки return round($price * 100); } private function syncPricingCodes(SparePart $sparePart, array $codeStrings): void { $pricingCodeIds = []; foreach ($codeStrings as $code) { if (empty($code)) { continue; } // Находим или создаём PricingCode $pricingCode = PricingCode::where('type', PricingCode::TYPE_PRICING_CODE) ->where('code', $code) ->first(); if (!$pricingCode) { $pricingCode = PricingCode::create([ 'type' => PricingCode::TYPE_PRICING_CODE, 'code' => $code, 'description' => null, ]); } $pricingCodeIds[] = $pricingCode->id; } $sparePart->pricingCodes()->sync($pricingCodeIds); } private function log(string $message): void { $this->logs[] = '[' . date('H:i:s') . '] ' . $message; Log::info($message); } public function getLogs(): array { return $this->logs; } }