From d963ce5fbeed34ac9b27a25c68d5c12a40b7040e Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Thu, 18 Dec 2025 23:20:23 +0100 Subject: [PATCH] =?UTF-8?q?Dodano=20skrypt=20do=20importu=20produkt=C3=B3w?= =?UTF-8?q?=20z=20pliku=20XML=20oraz=20implementacja=20logiki=20synchroniz?= =?UTF-8?q?acji,=20aktualizacji=20i=20dodawania=20produkt=C3=B3w=20w=20Pre?= =?UTF-8?q?staShop.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- import-products.php | 377 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 import-products.php diff --git a/import-products.php b/import-products.php new file mode 100644 index 00000000..d9c8c51b --- /dev/null +++ b/import-products.php @@ -0,0 +1,377 @@ + Podatki > Reguły podatkowe +$configTaxRulesGroupId = 1; + +// ID Kategorii domyślnej, do której mają trafić produkty (np. "Kinkiety" lub ogólna) +$configDefaultCategoryId = 12; + +// Czy włączyć produkt po imporcie? +$configActive = 1; + +// ============================================ + +// Sprawdzenie trybu działania +$modeAdd = (Tools::getValue('add') === 'true'); +$modeUpdate = (Tools::getValue('update') === 'true'); +$modeSynch = (Tools::getValue('synch') === 'true'); + +if (!$modeAdd && !$modeUpdate && !$modeSynch) { + die('Brak akcji. Dodaj do adresu ?add=true lub ?update=true lub ?synch=true'); +} + +// Plik logu +$logFile = __DIR__ . '/sollux_import_log.csv'; + +// Wczytanie XML +$xmlUrl = 'https://sollux-lighting.com/product_feed/sollux/XML_polski_PLN_wszystkie_vip.xml'; +$xml = simplexml_load_file($xmlUrl) or die("Error: Cannot create object from XML"); + + +// === FUNKCJE POMOCNICZE === + +// Tworzenie pola multilang (wymagane przez Prestę) +function createMultiLangField($field) { + $languages = Language::getLanguages(false); + $res = []; + foreach ($languages as $lang) { + $res[(int)$lang['id_lang']] = $field; + } + return $res; +} + +// Tworzenie przyjaznego linku +function createLinkRewrite($field) { + $languages = Language::getLanguages(false); + $res = []; + $linkRewrite = Tools::link_rewrite($field); + foreach ($languages as $lang) { + $res[(int)$lang['id_lang']] = $linkRewrite; + } + return $res; +} + +// Pobieranie zdjęcia +function addProductImage($productId, $imageUrl) { + if (empty($imageUrl)) return false; + + $image = new Image(); + $image->id_product = (int)$productId; + $image->position = Image::getHighestPosition($productId) + 1; + $image->cover = ($image->position == 1); // Pierwsze zdjęcie jako okładka + if (!$image->add()) return false; + + $imagePath = $image->getPathForCreation(); + $url = str_replace(' ', '%20', trim($imageUrl)); + + // Próba pobrania + if (!@copy($url, $imagePath . '.jpg')) { + $image->delete(); + return false; + } + + // Generowanie miniatur + $imageTypes = ImageType::getImagesTypes('products'); + foreach ($imageTypes as $imageType) { + if (!ImageManager::resize( + $imagePath . '.jpg', + $imagePath . '-' . stripslashes($imageType['name']) . '.jpg', + (int)$imageType['width'], + (int)$imageType['height'] + )) { + // W razie błędu resize można obsłużyć wyjątek, tutaj cicho pomijamy + } + } + return true; +} + +// Znajdź produkt po SKU (reference) +function findProductByReference($reference) { + if (empty($reference)) return false; + + $sql = 'SELECT `id_product` + FROM `'._DB_PREFIX_.'product` + WHERE `reference` = \''.pSQL($reference).'\''; + $result = Db::getInstance()->getRow($sql); + return $result ? new Product((int)$result['id_product']) : false; +} + +// Parsowanie ceny (zamiana przecinków na kropki, usuwanie spacji) +function parsePrice($rawPrice) { + $clean = str_replace([' ', ','], ['', '.'], (string)$rawPrice); + return (float)$clean; +} + +// Budowanie tabeli HTML z atrybutów XML +function buildAttributesHtml($xmlAttributesNode) { + if (!$xmlAttributesNode || !isset($xmlAttributesNode->attribute)) { + return ''; + } + + $html = '
'; + $html .= '

Specyfikacja produktu:

'; + $html .= ''; + $html .= ''; + + foreach ($xmlAttributesNode->attribute as $attr) { + $name = (string)$attr->attribute_name; + $value = (string)$attr->attribute_value; + + if (!empty($name) && !empty($value) && $value !== 'Nie dotyczy') { + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + } + + $html .= ''; + $html .= '
' . htmlspecialchars($name) . '' . htmlspecialchars($value) . '
'; + $html .= '
'; + + return $html; +} + +// Czyści log aktualizacji - zostawia X dni +function cleanLogFile($logFile, $days = 7) { + if (!file_exists($logFile)) return; + $content = file($logFile); + $newContent = []; + $cutoff = strtotime("-$days days"); + foreach ($content as $line) { + $parts = explode(';', $line); + if (isset($parts[0]) && strtotime($parts[0]) >= $cutoff) { + $newContent[] = $line; + } + } + file_put_contents($logFile, implode("", $newContent)); +} + + +// ============================================ +// ================ LOGIKA ==================== +// ============================================ + +// 1. TRYB SYNCHRONIZACJI (Włączanie/Wyłączanie produktów) +if ($modeSynch) { + echo '

Synchronizacja stanów (Active/Inactive)...

'; + $db = Db::getInstance(); + + // Tabela tymczasowa na SKU z XML + $db->execute('CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'sollux_temp` (`sku` VARCHAR(64), PRIMARY KEY (`sku`)) ENGINE='._MYSQL_ENGINE_); + $db->execute('TRUNCATE TABLE `'._DB_PREFIX_.'sollux_temp`'); + + // Zbieranie SKU z XML + $sqlValues = []; + foreach ($xml->product as $productNode) { + $sku = trim((string)$productNode->sku); + if ($sku) { + $sqlValues[] = '(\''.pSQL($sku).'\')'; + } + // Wstawiamy paczkami po 100, żeby nie zapchać SQL + if (count($sqlValues) >= 100) { + $db->execute('INSERT IGNORE INTO `'._DB_PREFIX_.'sollux_temp` (`sku`) VALUES '.implode(',', $sqlValues)); + $sqlValues = []; + } + } + if (!empty($sqlValues)) { + $db->execute('INSERT IGNORE INTO `'._DB_PREFIX_.'sollux_temp` (`sku`) VALUES '.implode(',', $sqlValues)); + } + + // Wyłącz produkty, których nie ma w XML a są w bazie (i są powiązane z tym producentem, opcjonalnie) + // Zakładamy, że sprawdzamy tylko produkty, które mają SKU. + $sqlDisable = 'UPDATE `'._DB_PREFIX_.'product` p + LEFT JOIN `'._DB_PREFIX_.'sollux_temp` t ON p.reference = t.sku + SET p.active = 0 + WHERE t.sku IS NULL + AND p.id_manufacturer = '.(int)$configManufacturerId; // Bezpiecznik: wyłączamy tylko produkty tego producenta + + $db->execute($sqlDisable); + echo "

Wyłączono produkty niedostępne w feedzie.

"; + + // Włącz produkty, które są w XML + $sqlEnable = 'UPDATE `'._DB_PREFIX_.'product` p + JOIN `'._DB_PREFIX_.'sollux_temp` t ON p.reference = t.sku + SET p.active = 1 + WHERE p.active = 0'; + + $db->execute($sqlEnable); + echo "

Włączono produkty dostępne w feedzie.

"; + + // Posprzątaj + $db->execute('DROP TABLE `'._DB_PREFIX_.'sollux_temp`'); + echo '

Zakończono synchronizację.

'; + exit; +} + +// 2. TRYB AKTUALIZACJI (Ceny, Stany) +if ($modeUpdate) { + cleanLogFile($logFile); + $today = date('Y-m-d'); + $updatedCount = 0; + + // Wczytaj log, aby nie aktualizować tego samego produktu wielokrotnie tego samego dnia + $processedSkus = []; + if (file_exists($logFile)) { + $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + $data = explode(';', $line); // Date;SKU + if (isset($data[0]) && $data[0] == $today && isset($data[1])) { + $processedSkus[trim($data[1])] = true; + } + } + } + + echo "

Rozpoczynam aktualizację...

"; + + foreach ($xml->product as $productNode) { + $sku = trim((string)$productNode->sku); + if (empty($sku)) continue; + + // Jeśli już zrobiony dzisiaj - pomiń + if (isset($processedSkus[$sku])) continue; + + $product = findProductByReference($sku); + if (!$product) { + // Produkt nie istnieje w bazie - w trybie update pomijamy + continue; + } + + // --- Aktualizacja Ceny --- + $priceGross = parsePrice($productNode->cena_detaliczna_brutto_pln); + $priceNet = Tools::ps_round($priceGross / 1.23, 6); // Zakładamy 23% VAT od brutto + + $product->price = $priceNet; + $product->active = 1; // Przywracamy aktywność przy update + + // --- Aktualizacja Stanu (Domyślnie 100 jeśli jest w pliku) --- + // XML nie ma pola "qty", ale ma "paczkomat" i "orientacyjny czas". + // Zakładamy: jest w XML = dostępny. + StockAvailable::setQuantity($product->id, 0, 100); + + $product->update(); + + // Logowanie + file_put_contents($logFile, $today.';'.$sku.PHP_EOL, FILE_APPEND); + + echo "

Zaktualizowano: $sku (Cena netto: $priceNet)

"; + $updatedCount++; + + // Aktualizujemy 1 produkt na wywołanie, żeby nie przekroczyć czasu wykonywania? + // W poprzednim skrypcie był break. Jeśli masz crona co minutę, odkomentuj break. + // Jeśli odpalasz ręcznie w przeglądarce, break spowoduje konieczność ciągłego odświeżania. + // Zostawiam break dla bezpieczeństwa, odświeżaj stronę (meta reload na dole). + break; + } + + if ($updatedCount > 0) { + echo ''; + echo '

Trwa przeładowanie strony...

'; + } else { + echo '

Wszystkie produkty z XML zostały już dzisiaj zaktualizowane.

'; + } + exit; +} + +// 3. TRYB DODAWANIA (ADD) +if ($modeAdd) { + $addedCount = 0; + + foreach ($xml->product as $productNode) { + $sku = trim((string)$productNode->sku); + if (empty($sku)) continue; + + // Sprawdź czy produkt już istnieje + if (findProductByReference($sku)) { + // Produkt istnieje, w trybie ADD nic nie robimy (ewentualnie można zrobić update) + continue; + } + + // === TWORZENIE NOWEGO PRODUKTU === + $product = new Product(); + $product->reference = $sku; + $product->ean13 = trim((string)$productNode->ean); + $product->name = createMultiLangField(trim((string)$productNode->nazwa_produktu)); + + // Kategoria i Producent + $product->id_category_default = (int)$configDefaultCategoryId; + $product->id_manufacturer = (int)$configManufacturerId; + $product->id_tax_rules_group = (int)$configTaxRulesGroupId; // VAT + + // Cena + $priceGross = parsePrice($productNode->cena_detaliczna_brutto_pln); + $product->price = Tools::ps_round($priceGross / 1.23, 6); + + // Wymiary i Waga + $product->width = (float)$productNode->szerokosc; + $product->height = (float)$productNode->wysokosc; + $product->depth = (float)$productNode->dlugosc; + $product->weight = (float)$productNode->waga_produktu; + + // Opisy + $shortDesc = trim((string)$productNode->opis_krotki_html); + $longDescRaw = trim((string)$productNode->opis_dlugi_korzysci_html); + + // Generowanie tabeli specyfikacji z atrybutów + $attributesHtml = buildAttributesHtml($productNode->attributes); + + // Sklejenie opisu: Opis marketingowy + Tabela atrybutów + $finalLongDesc = $longDescRaw . '

' . $attributesHtml; + + $product->description_short = createMultiLangField($shortDesc); + $product->description = createMultiLangField($finalLongDesc); + + $product->link_rewrite = createLinkRewrite((string)$productNode->nazwa_produktu); + $product->active = (int)$configActive; + + // Dodanie do bazy + if ($product->add()) { + // Przypisanie do kategorii (domyślna + drzewo) + $product->addToCategories([(int)$configDefaultCategoryId]); + + // Ustawienie stanu magazynowego (100) + StockAvailable::setQuantity($product->id, 0, 100); + + // === ZDJĘCIA === + // Loop przez image_1 do image_25 + for ($i = 1; $i <= 25; $i++) { + $imgTag = 'image_' . $i; + $imgUrl = (string)$productNode->$imgTag; + if (!empty($imgUrl)) { + addProductImage($product->id, $imgUrl); + } + } + + echo "

Dodano produkt: " . $product->name[Context::getContext()->language->id] . " ($sku)

"; + $addedCount++; + + // Break po dodaniu jednego, aby odciążyć serwer (skrypt musi być wywoływany cyklicznie) + break; + } else { + echo "

Błąd podczas dodawania produktu: $sku

"; + } + } + + if ($addedCount > 0) { + echo ''; + echo '

Dodano produkt. Odświeżanie...

'; + } else { + echo '

Brak nowych produktów do dodania (lub wszystkie już istnieją).

'; + } +} +?> \ No newline at end of file