language->id; $sql = 'SELECT `id_attribute_group` FROM `'._DB_PREFIX_.'attribute_group_lang` WHERE `name` = \''.pSQL($name).'\' AND `id_lang` = '.(int)$id_lang; $result = Db::getInstance()->getRow($sql); return $result ? new AttributeGroup($result['id_attribute_group']) : false; } // Function to find attribute by name function findAttributeByName($id_attribute_group, $name) { $id_lang = Context::getContext()->language->id; $sql = 'SELECT a.`id_attribute` FROM `'._DB_PREFIX_.'attribute` a JOIN `'._DB_PREFIX_.'attribute_lang` al ON a.`id_attribute` = al.`id_attribute` WHERE al.`name` = \''.pSQL($name).'\' AND al.`id_lang` = '.(int)$id_lang.' AND a.`id_attribute_group` = '.(int)$id_attribute_group; $result = Db::getInstance()->getRow($sql); return $result ? new Attribute($result['id_attribute']) : false; } // Function to create attribute if it doesn't exist function createAttribute($name, $values) { $attributeGroup = findAttributeGroupByName($name); if (!$attributeGroup) { $attributeGroup = new AttributeGroup(); $attributeGroup->name = createMultiLangField($name); $attributeGroup->public_name = createMultiLangField($name); $attributeGroup->group_type = 'select'; $attributeGroup->add(); } foreach ($values as $value) { $attribute = findAttributeByName($attributeGroup->id, $value); if (!$attribute) { $attribute = new Attribute(); $attribute->id_attribute_group = $attributeGroup->id; $attribute->name = createMultiLangField($value); $attribute->add(); } } return $attributeGroup->id; } // Helper function to create a multi-language field function createMultiLangField($field) { $languages = Language::getLanguages(false); $res = []; foreach ($languages as $lang) { $res[(int)$lang['id_lang']] = $field; } return $res; } // Helper function to create a valid link_rewrite function createLinkRewrite($field) { $languages = Language::getLanguages(false); $res = []; $linkRewrite = Tools::link_rewrite($field); // PrestaShop's function to create valid link_rewrite foreach ($languages as $lang) { $res[(int)$lang['id_lang']] = $linkRewrite; } return $res; } // Function to get category ID from name (nieużywana, ale poprawiona) function getCategoryId($categoryName) { $result = Category::searchByName(1, $categoryName); // 1 for default language id if (!empty($result) && isset($result[0]['id_category'])) { return (int)$result[0]['id_category']; } else { // Create category if not exists $category = new Category(); $category->name = createMultiLangField($categoryName); $category->link_rewrite = createLinkRewrite($categoryName); $category->id_parent = 2; // Default parent category $category->add(); return (int)$category->id; } } // Function to download image from URL and associate it with a product function addProductImage($productId, $imageUrl) { $image = new Image(); $image->id_product = (int)$productId; $image->position = Image::getHighestPosition($productId) + 1; $image->cover = true; // Set the first image as cover $image->add(); $imagePath = $image->getPathForCreation(); $url = str_replace(' ', '%20', $imageUrl); if (!copy($url, $imagePath . '.jpg')) { $image->delete(); return false; } $imageTypes = ImageType::getImagesTypes('products'); foreach ($imageTypes as $imageType) { ImageManager::resize( $imagePath . '.jpg', $imagePath . '-' . stripslashes($imageType['name']) . '.jpg', (int)$imageType['width'], (int)$imageType['height'] ); } return true; } // Function to find product by reference function findProductByReference($reference) { $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; } // Function to find combination by product ID and attribute IDs function findCombinationByAttributes($id_product, $attributeIds) { if (empty($attributeIds)) { return false; } sort($attributeIds); $conditions = []; foreach ($attributeIds as $id_attr) { $conditions[] = 'pac.`id_attribute` = '.(int)$id_attr; } $sql = 'SELECT c.`id_product_attribute` FROM `'._DB_PREFIX_.'product_attribute` c JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON c.`id_product_attribute` = pac.`id_product_attribute` WHERE c.`id_product` = '.(int)$id_product.' GROUP BY c.`id_product_attribute` HAVING SUM('.implode(' OR ', $conditions).') = '.count($attributeIds); $result = Db::getInstance()->getRow($sql); return $result ? new Combination((int)$result['id_product_attribute']) : false; } // znajdowanie kombinacji po indeksie/SKU (reference) function findCombinationByReference($id_product, $reference) { $sql = 'SELECT `id_product_attribute` FROM `'._DB_PREFIX_.'product_attribute` WHERE `id_product` = '.(int)$id_product.' AND `reference` = \''.pSQL($reference).'\''; $id = Db::getInstance()->getValue($sql); return $id ? new Combination((int)$id) : false; } function parsePrice($rawPrice) { // Na wszelki wypadek rzutujemy na string i obcinamy spacje $rawPrice = trim((string)$rawPrice); if ($rawPrice === '') { return 0.0; } // Zostawiamy tylko cyfry, przecinek, kropkę i minus // Usuwamy walutę, spacje, itp. $clean = preg_replace('/[^\d,.\-]/', '', $rawPrice); $hasComma = strpos($clean, ',') !== false; $hasDot = strpos($clean, '.') !== false; // Przypadek typowo polski: "1234,56" lub "125,00" if ($hasComma && !$hasDot) { $clean = str_replace(',', '.', $clean); } // Przypadek mieszany: "1.234,56" -> usuń kropki (separatory tysięcy), przecinek zamień na kropkę elseif ($hasComma && $hasDot) { $clean = str_replace('.', '', $clean); // usuwamy separatory tysięcy $clean = str_replace(',', '.', $clean); // przecinek na kropkę } // Jeśli jest tylko kropka ("1234.56") – nic nie zmieniamy return (float)$clean; } // Bazowa cena dla grupy – najtańszy wariant (zwraca [brutto, netto]) function getBasePricesFromGroup($products) { $minNet = null; $minGross = null; foreach ($products as $p) { $gross = parsePrice((string)$p->price); if ($gross <= 0) { continue; } $net = Tools::ps_round($gross / 1.23, 6); if ($minNet === null || $net < $minNet) { $minNet = $net; $minGross = $gross; } } if ($minNet === null) { return [0.0, 0.0]; } return [$minGross, $minNet]; } // Czyści log aktualizacji cen - zostawia tylko wpisy z ostatnich X dni function cleanUpdateLog($logFile, $daysToKeep = 30) { if (!file_exists($logFile)) { return; } $cutoffDate = date('Y-m-d', strtotime('-'.(int)$daysToKeep.' days')); $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if ($lines === false || empty($lines)) { return; } $newLines = []; foreach ($lines as $line) { $parts = explode(';', $line); if (count($parts) < 1) { // linia uszkodzona – pomijamy continue; } $logDate = trim($parts[0]); // Jeśli data ma format YYYY-MM-DD i jest nowsza/równa cutoff – zostawiamy if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $logDate) && $logDate >= $cutoffDate) { $newLines[] = $line; } // Jeśli format daty jest dziwny – na wszelki wypadek zachowajmy wpis elseif (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $logDate)) { $newLines[] = $line; } } // Nadpisujemy plik przefiltrowaną zawartością if (!empty($newLines)) { file_put_contents($logFile, implode(PHP_EOL, $newLines) . PHP_EOL); } else { // Jeśli wszystko stare/uszkodzone – czyścimy plik file_put_contents($logFile, ''); } } // Zwraca id_tax_rules_group dla stawki VAT (np. 23) dla kraju domyślnego function getTaxRulesGroupIdForRate($rate, $id_country = null) { if ($id_country === null) { $id_country = (int)Configuration::get('PS_COUNTRY_DEFAULT'); } $sql = 'SELECT trg.`id_tax_rules_group` FROM `'._DB_PREFIX_.'tax_rules_group` trg INNER JOIN `'._DB_PREFIX_.'tax_rule` tr ON (trg.`id_tax_rules_group` = tr.`id_tax_rules_group`) INNER JOIN `'._DB_PREFIX_.'tax` t ON (tr.`id_tax` = t.`id_tax`) WHERE trg.`active` = 1 AND tr.`id_country` = '.(int)$id_country.' AND t.`rate` = '.(float)$rate.' ORDER BY trg.`id_tax_rules_group` ASC'; $id = Db::getInstance()->getValue($sql); return $id ? (int)$id : 0; } // Zwraca główny produkt z grupy – pierwszy, którego SKU istnieje w PrestaShop function findMainProductDataFromGroup($products) { foreach ($products as $p) { $ref = (string)$p->sku; if ($ref !== '' && findProductByReference($ref)) { return $p; // to główny produkt } } return null; } // === GRUPOWANIE PRODUKTÓW PO SYMBOLU === $productsBySymbol = []; foreach ($xml->product as $productData) { $symbol = (string)$productData->item_group_id; if (!isset($productsBySymbol[$symbol])) { $productsBySymbol[$symbol] = []; } $productsBySymbol[$symbol][] = $productData; } // ID grupy VAT 23% $idTaxRulesGroup23 = getTaxRulesGroupIdForRate(23); // ======================================= // =========== TRYB AKTUALIZACJI ========= // ======================================= if ($modeUpdate) { cleanUpdateLog($logFile, 30); $today = date('Y-m-d'); $updatedToday = []; // Wczytanie logu aktualizacji if (file_exists($logFile)) { $lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $parts = explode(';', $line); if (count($parts) >= 3) { $logDate = trim($parts[0]); $logType = trim($parts[1]); // np. 'product' $logRef = trim($parts[2]); if ($logDate === $today) { $updatedToday[$logType.'_'.$logRef] = true; } } } } $updatedSomething = false; foreach ($productsBySymbol as $symbol => $products) { if (empty($products)) { continue; } // Główny produkt – referencja z pierwszego elementu grupy // $mainProductData = $products[0]; // $reference = (string)$mainProductData->sku; // $key = 'product_'.$reference; $mainProductData = findMainProductDataFromGroup($products); if (!$mainProductData) { continue; // w grupie nie ma produktu, który istnieje w Presta } $reference = (string)$mainProductData->sku; $key = 'product_'.$reference; // Jeśli już zaktualizowany dzisiaj – pomijamy if (isset($updatedToday[$key])) { continue; } $product = findProductByReference($reference); if (!$product) { // produkt nie istnieje w Presta - pomijamy w trybie update continue; } // BAZA: najtańszy wariant w grupie (brutto i netto) list($grossBase, $netPrice) = getBasePricesFromGroup($products); if ($grossBase <= 0 || $netPrice <= 0) { // brak sensownej ceny – pomiń continue; } // Flagi / liczniki $updatedProductPrice = false; $updatedCombinationCount = 0; // Aktualizacja ceny produktu (NETTO) – baza = najtańszy wariant $product->price = $netPrice; $updatedProductPrice = true; // Produkt zawsze aktywny + delivery times $product->active = 1; $product->delivery_in_stock = createMultiLangField('2-7 dni roboczych'); $product->delivery_out_stock = createMultiLangField('4-10 tygodni'); // Upewnij się, że produkt ma ustawioną grupę VAT 23% if (!empty($idTaxRulesGroup23) && (int)$product->id_tax_rules_group !== (int)$idTaxRulesGroup23) { $product->id_tax_rules_group = (int)$idTaxRulesGroup23; } // Sprawdź kategorię domyślną – jeśli "Strona główna", zamień na "Meble" (ID 107) $id_lang = (int)$context->language->id; $defaultCategory = new Category($product->id_category_default, $id_lang); if (Validate::isLoadedObject($defaultCategory) && $defaultCategory->name == 'Strona główna') { $newCategoryId = 107; // Meble // Ustaw nową kategorię domyślną $product->id_category_default = (int)$newCategoryId; // Podmień kategorie produktu (zachowując ewentualne inne) $categories = $product->getCategories(); $categories = array_diff($categories, [(int)$defaultCategory->id]); $categories[] = (int)$newCategoryId; $categories = array_unique(array_map('intval', $categories)); $product->updateCategories($categories); } // Zapis produktu if ($product->update()) { // --- STANY MAGAZYNOWE PRODUKTU (ID_PRODUCT_ATTRIBUTE = 0) --- $mainStatus = (string)$mainProductData->Status_magazynowy; $mainQty = ($mainStatus === 'instock') ? 100 : 0; StockAvailable::setQuantity($product->id, 0, $mainQty); // --- AKTUALIZACJA CEN I STANÓW KOMBINACJI NA PODSTAWIE XML --- foreach ($products as $productDataVariant) { $variantRef = (string)$productDataVariant->sku; // Szukamy kombinacji po indeksie/SKU $combination = findCombinationByReference($product->id, $variantRef); if (!$combination) { continue; } // Cena brutto wariantu z XML $variantGross = parsePrice((string)$productDataVariant->price); if ($variantGross <= 0) { continue; } // Netto wariantu $variantNet = Tools::ps_round($variantGross / 1.23, 6); // Impact względem ceny bazowej produktu (najtańszy wariant) $impact = $variantNet - $netPrice; // zabezpieczenie na wypadek minimalnych różnic/zaokrągleń if ($impact < 0) { $impact = 0; } $impact = Tools::ps_round($impact, 6); $combination->price = $impact; // Stan magazynowy kombinacji $variantStatus = (string)$productDataVariant->Status_magazynowy; $variantQty = ($variantStatus === 'instock') ? 100 : 0; StockAvailable::setQuantity($product->id, $combination->id, $variantQty); if ($combination->update()) { $updatedCombinationCount++; } } // --- KONIEC AKTUALIZACJI KOMBINACJI --- // Zapis do logu – że ten produkt został dziś zaktualizowany $logLine = $today.';product;'.$reference.';'.$product->id.PHP_EOL; file_put_contents($logFile, $logLine, FILE_APPEND); echo '
Zaktualizowano produkt: '.htmlspecialchars((string)$mainProductData->title).' ('.$reference.')
'; echo 'Nowa bazowa cena brutto (najtańszy wariant) z XML: '.$grossBase.'
'; echo 'Nowa cena netto produktu (bazowa) w Presta: '.$netPrice.'
'; // INFO: czy zaktualizowano cenę produktu if ($updatedProductPrice) { echo 'Cena produktu została zaktualizowana.
'; } else { echo 'Cena produktu nie uległa zmianie.
'; } // INFO: ile kombinacji zaktualizowano if ($updatedCombinationCount > 0) { echo 'Zaktualizowano ceny/ilości kombinacji: '.$updatedCombinationCount.' szt.
'; } else { echo 'Nie zaktualizowano żadnej kombinacji (brak dopasowanych SKU albo cen).
'; } $updatedSomething = true; } // Aktualizujemy tylko jeden produkt na jedno wywołanie break; } if ($updatedSomething) { // echo ''; } else { echo 'Brak produktów do aktualizacji na dzisiaj (wszystkie z XML zostały już zaktualizowane).
'; } exit; } // ======================================= // =========== TRYB DODAWANIA ============ // ======================================= if ($modeAdd) { $productAdded = false; $combinationAdded = false; // Tworzenie lub aktualizacja produktów z kombinacjami (dodawanie) foreach ($productsBySymbol as $symbol => $products) { if (empty($products)) { continue; } // ===== Filter item_group_id ===== // $mainProductDataTemp = $products[0]; // if ((string)$mainProductDataTemp->item_group_id !== '68590') { // continue; // } // Główny produkt – dane z pierwszego w grupie // $mainProductData = $products[0]; // $mainProduct = findProductByReference((string)$mainProductData->sku); $mainProductData = findMainProductDataFromGroup($products); if (!$mainProductData) { $mainProductData = $products[0]; // fallback na etapie dodawania } $mainProduct = findProductByReference((string)$mainProductData->sku); // BAZA: najtańszy wariant w grupie (brutto i netto) list($grossBase, $netPrice) = getBasePricesFromGroup($products); if (!$mainProduct) { // Create a new product if it doesn't exist $mainProduct = new Product(); $mainProduct->name = createMultiLangField((string)$mainProductData->title); $description = (string)$mainProductData->description; $description = str_replace("\n", "Dodałem produkt: " . htmlspecialchars((string)$mainProductData->title) . "
"; } if ($combinationAdded) { echo "Dodałem kombinację: " . htmlspecialchars((string)$mainProductData->title) . "
"; } break; // Break if a new product or combination was added } } } // ======================================= // ========= TRYB SYNCHRONIZACJI ========= // ======================================= if ($modeSynch) { echo '