Для выгрузки каталога товаров в различные площадки, в разных форматах на Marketplace можно подобрать подходящий модуль, и для непрограммиста это будет отличное решение!
Но для себя понял главное - все эти модули очень сложно кастомизировать, если нужно! Если вы программист - лучше сделайте себе свою заготовку, которую в дальнейшем можно расширять, или убедитесь, что вам достаточно того функционала, который предоставляет покупаемый вами модуль.
В данном посте я выложу свою заготовку создания xml-файла с актуальной версией небольшого каталога товаров.
Существует 2 варианта представления каталога товаров на сайте: простой - когда товар и все его торговые предложения имеют один и тот же url-адрес (и частный случай - каталог без торговых предложений) и более сложный - когда торговые предложения имеют собственные url-адреса, отличающиеся от основного товара какой-то дополнительной солью.
Для первого варианта, нужно обрабатывать именно каталог, а цены и остатки искать в торговых предложениях. Для второго - обрабатывать нужно торговые предложения, а недостающие данные брать из товара.
В данном посте рассмотрю простой вариант, т.к. он встречается чаще (в моей работе), хотя с точки зрения сео - вариант отдельных адресов может быть и не совсем верно.
В любом случае, принимать решение о том, какой вариант лучше - вам, причем, в каждой конкретной ситуации.
Для начала кладем где-то файл с классом, например сюда: /bitrix/php_interface/classes/ccatalogexport.php
следующего содержания:
namespace Pai\Tools; use \Bitrix\Main\Data\Cache; use Bitrix\Main\IO, Bitrix\Main\Application; use Bitrix\Main\Loader; class CCatalogExport { var $CATALOG_IBLOCK; var $OFFER_IBLOCK; var $EXCLUDE_SECTIONS = [83, 88, 89]; // тут указываем разделы, которые выгружать не нужно var $FirmName = 'ТМ Торговая Марка'; var $url = "https://site.ua"; var $Scheme; var $result; function __construct($CATALOG_IBLOCK, $OFFER_IBLOCK) { $this->CATALOG_IBLOCK = $CATALOG_IBLOCK; $this->OFFER_IBLOCK = $OFFER_IBLOCK; } private function GetSectionsList() { $result = false; $needNavChain = false; $cache_params = array('function' => __FUNCTION__, 'IBLOCK_ID' => $this->CATALOG_IBLOCK, 'EXCLUDE' => $this->EXCLUDE_SECTIONS, $needNavChain); $cache_id = md5(serialize($cache_params)); $cache_dir = __CLASS__; $cache = Cache::createInstance(); $life_time = 60 * 60 * 24 * 30; if ($life_time < 0) { $cache->clean($cache_id, $cache_dir); } if ($cache->initCache($life_time, $cache_id, $cache_dir)) { $result = $cache->getVars(); } elseif ($cache->startDataCache() && \Bitrix\Main\Loader::includeModule('iblock')) { $Sect_list = \CIBlockSection::GetList(array("DEPTH_LEVEL" => "ASC", "ID" => "ASC"), array('IBLOCK_ID' => $this->CATALOG_IBLOCK, 'GLOBAL_ACTIVE' => 'Y', '!ID' => $this->EXCLUDE_SECTIONS), false, array('ID', 'IBLOCK_ID', 'NAME', 'SECTION_PAGE_URL', 'IBLOCK_SECTION_ID')); while ($Section = $Sect_list->GetNext()) { if ($needNavChain) { $nav = \CIBlockSection::GetNavChain(false, $Section['ID']); while ($arSectionPath = $nav->GetNext()) { $Section['SECTIONS_PATH'][$arSectionPath['DEPTH_LEVEL']] = $arSectionPath; } } $result[$Section['ID']] = $Section; } $cache->endDataCache($result); } $this->result['SECTIONS'] = $result; } private function GetIblockElements() { $result = false; $cache_params = array('function' => __FUNCTION__, 'IBLOCK_ID' => $this->CATALOG_IBLOCK, 'EXCLUDE' => $this->EXCLUDE_SECTIONS); $cache_id = md5(serialize($cache_params)); $cache_dir = __CLASS__; $cache = Cache::createInstance(); $life_time = 60 * 60 * 24; if ($life_time < 0) { $cache->clean($cache_id, $cache_dir); } if ($cache->initCache($life_time, $cache_id, $cache_dir)) { $result = $cache->getVars(); } elseif ($cache->startDataCache() && \Bitrix\Main\Loader::includeModule('iblock')) { $arSelect = Array("ID", "NAME", "IBLOCK_ID", "CATALOG_TYPE", "IBLOCK_SECTION_ID", "DETAIL_PAGE_URL", "DETAIL_PICTURE", "PREVIEW_PICTURE", "DETAIL_TEXT"); $arFilter = Array("IBLOCK_ID" => $this->CATALOG_IBLOCK, "ACTIVE_DATE" => "Y", "ACTIVE" => "Y", '!IBLOCK_SECTION_ID' => $this->EXCLUDE_SECTIONS, "CATALOG_TYPE" => array(1, 3)); $res = \CIBlockElement::GetList(Array(), $arFilter, false, false, $arSelect); while ($ob = $res->GetNextElement()) { $arFields = $ob->GetFields(); /*Loader::includeModule('pai.tools'); CDebug::pre($arFields,true);*/ $arFields['CATALOG_QUANTITY'] = intval($arFields['CATALOG_QUANTITY']); $price = 0; if (intval($arFields['DETAIL_PICTURE']) <= 0 && intval($arFields['PREVIEW_PICTURE']) > 0) { $arFields['DETAIL_PICTURE'] = $arFields['PREVIEW_PICTURE']; } if ($arFields['CATALOG_TYPE'] == 3) { // у товара есть торговые предложения - их нужно найти :( $arOfferSelect = Array("ID", "NAME", "IBLOCK_ID", "CATALOG_QUANTITY"); if (intval($arFields['DETAIL_PICTURE']) <= 0 && intval($arFields['PREVIEW_PICTURE']) <= 0) { $arOfferSelect[] = 'DETAIL_PICTURE'; $arOfferSelect[] = 'PREVIEW_PICTURE'; } $arOfferFilter = Array("IBLOCK_ID" => $this->OFFER_IBLOCK, "PROPERTY_CML2_LINK" => $arFields['ID'], "ACTIVE_DATE" => "Y", "ACTIVE" => "Y"); $resOffer = \CIBlockElement::GetList(Array(), $arOfferFilter, false, false, $arOfferSelect); while ($arOfferFields = $resOffer->GetNext()) { $dbProductPrice = \CPrice::GetListEx( array(), array("PRODUCT_ID" => $arOfferFields['ID'], "CATALOG_GROUP_ID" => 1), // берем товары с базовой ценой, с ID=1 false, false, array("ID", "CATALOG_GROUP_ID", "PRICE", "CURRENCY") ); while ($arPrice = $dbProductPrice->Fetch()) { if (!isset($arFields['PRICE']) || floatval($arFields['PRICE']) > floatval($arPrice['PRICE'])) { $arFields['PRICE'] = floatval($arPrice['PRICE']); $arFields['CURRENCY'] = $arPrice['CURRENCY']; } } $arFields['CATALOG_QUANTITY'] += intval($arOfferFields['CATALOG_QUANTITY']); if (intval($arFields['DETAIL_PICTURE']) <= 0 && (intval($arOfferFields['DETAIL_PICTURE']) > 0 || intval($arOfferFields['PREVIEW_PICTURE']) > 0)) { $arFields['DETAIL_PICTURE'] = (intval($arOfferFields['DETAIL_PICTURE']) > 0) ? $arOfferFields['DETAIL_PICTURE'] : $arOfferFields['PREVIEW_PICTURE']; } } } else { // простой товар $dbProductPrice = \CPrice::GetListEx( array(), array("PRODUCT_ID" => $arFields['ID'], "CATALOG_GROUP_ID" => 1), // берем товары с базовой ценой, с ID=1 false, false, array("ID", "CATALOG_GROUP_ID", "PRICE", "CURRENCY") ); while ($arPrice = $dbProductPrice->Fetch()) { if (!isset($arFields['PRICE']) || floatval($arFields['PRICE']) > floatval($arPrice['PRICE'])) { $arFields['PRICE'] = floatval($arPrice['PRICE']); $arFields['CURRENCY'] = $arPrice['CURRENCY']; } } } if (floatval($arFields['PRICE']) <= 0) continue; if (intval($arFields['DETAIL_PICTURE']) <= 0) continue; $arProperties = $ob->GetProperties(); $arDisplay_properties = []; $MorePictures = []; foreach ($arProperties as $arProperty) { if($arProperty['CODE']=='MORE_PHOTO' && !empty($arProperty['VALUE'])){ foreach ($arProperty['VALUE'] as $fid){ $MorePictures[] = \CFile::GetPath($fid); } } if (in_array($arProperty['CODE'], array( 'MINIMUM_PRICE', 'MAXIMUM_PRICE', 'HIT', 'POPUP_VIDEO', 'PODBORKI', 'PRODUCTION', 'COLOR', 'SPECIAL_OFFERS', 'META_DESCRIPTION', 'META_KEYWORDS', 'MORE_PHOTO', // ??? 'COLOR_REF2', 'SALE_TEXT', 'vote_count', 'vote_sum', 'rating', 'FORUM_TOPIC_ID', 'FORUM_MESSAGE_CNT', 'COLOR_VARIANTS', 'PRODUCT_TYPE', 'VIDEO_YOUTUBE', 'EXPANDABLES', 'ASSOCIATED', ))) continue; $arDisplay_properties[$arProperty['CODE']] = \CIBlockFormatProperties::GetDisplayValue( array('ID' => $arFields['ID'], 'NAME' => $arFields['NAME']), $arProperty, ''); } $row = array( 'id' => $arFields['ID'], 'available' => (intval($arFields['CATALOG_QUANTITY']) > 0) ? "true" : "false", 'categoryId' => $arFields['IBLOCK_SECTION_ID'], 'vendor' => 'Brand name', 'name' => $arFields['NAME'], 'url' => $arFields['DETAIL_PAGE_URL'], 'price' => $arFields['PRICE'], 'stock' => (intval($arFields['CATALOG_QUANTITY']) > 0) ? 'На складе' : "Нет на складе", 'params' => [], 'picture' => \CFile::GetPath($arFields['DETAIL_PICTURE']), 'more_pictures' => $MorePictures, 'description' => ((strlen($arFields['DETAIL_TEXT']) > 0) ? '' . PHP_EOL : "") ); foreach ($arDisplay_properties as $arProperty) { if (is_array($arProperty['DISPLAY_VALUE'])) $arProperty['DISPLAY_VALUE'] = implode(', ', $arProperty['DISPLAY_VALUE']); if (strlen($arProperty['DISPLAY_VALUE']) > 0) { $row['params'][$arProperty['NAME']] = $arProperty['DISPLAY_VALUE']; } } $result[$arFields['ID']] = $row; } $cache->endDataCache($result); } $this->result['PRODUCTS'] = $result; } function GetFileSheme() { $Scheme = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" . PHP_EOL; $Scheme .= "<price>" . PHP_EOL; $Scheme .= "\t<date>" . date(str_replace("_", " ", '"Y-m-dTh:i:s±h:i"')) . "</date>" . PHP_EOL; $Scheme .= "\t<firmName>" . $this->FirmName . "</firmName>" . PHP_EOL; $Scheme .= "\t<rate>1</rate>" . PHP_EOL; $Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL; $Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL; $Scheme .= "\t<categories>" . PHP_EOL; $Scheme .= "#CATEGORIES#" . PHP_EOL; $Scheme .= "\t</categories>" . PHP_EOL; $Scheme .= "\t<offers>" . PHP_EOL; $Scheme .= "#PRODUCTS#" . PHP_EOL; $Scheme .= "\t</offers>" . PHP_EOL; $Scheme .= "</price>"; $this->Scheme = $Scheme; } function putSectionsToScheme() { $categories = ''; foreach ($this->result['SECTIONS'] as $arSection) { $categories .= "\t\t<category id=\"" . $arSection['ID'] . "\" " . ( (intval($arSection['IBLOCK_SECTION_ID']) > 0) ? "parentId=\"" . $arSection['IBLOCK_SECTION_ID'] . "\" " : "") . "portal_url=\"" . $arSection['SECTION_PAGE_URL'] . "\">" . $arSection["NAME"] . "</category>" . PHP_EOL; } $this->Scheme = str_replace('#CATEGORIES#', TrimEx($categories, PHP_EOL, 'right'), $this->Scheme); } function putProductsToScheme() { $productsLine = ''; foreach ($this->result['PRODUCTS'] as $arProduct) { $productsLine .= "\t\t<offer id=\"" . $arProduct["id"] . "\" available=\"" . $arProduct["available"] . "\">" . PHP_EOL; $productsLine .= "\t\t\t<categoryId>" . $arProduct['categoryId'] . "</categoryId>" . PHP_EOL; $productsLine .= "\t\t\t<vendor>" . $arProduct['vendor'] . "</vendor>" . PHP_EOL; $productsLine .= "\t\t\t<name>" . $arProduct['name'] . "</name>" . PHP_EOL; $productsLine .= "\t\t\t<url>" . $this->url . $arProduct['url'] . "</url>" . PHP_EOL; $productsLine .= "\t\t\t<price>" . $arProduct['price'] . "</price>" . PHP_EOL; $productsLine .= "\t\t\t<stock>" . $arProduct['stock'] . "</stock>" . PHP_EOL; $productsLine .= "\t\t\t<picture>" . $this->url . $arProduct['picture'] . "</picture>" . PHP_EOL; $productsLine .= "\t\t\t<description>" . $arProduct['description'] . PHP_EOL . "</description>" . PHP_EOL; foreach ($arProduct['params'] as $param => $paramValue) { $productsLine .= "\t\t\t<param name=\"" . $param . "\">" . $paramValue . "</param>" . PHP_EOL; } $productsLine .= "\t\t</offer>" . PHP_EOL; } $this->Scheme = str_replace('#PRODUCTS#', TrimEx($productsLine, PHP_EOL, 'right'), $this->Scheme); } function putSchemeToFile() { $file = new IO\File(Application::getDocumentRoot() . "/upload/export/tmp.xml"); $file->putContents($this->Scheme); $file->rename(Application::getDocumentRoot() . "/upload/export/siteCatalog.xml"); echo 'Ok'; } function process() { $this->GetFileSheme(); $this->GetSectionsList(); $this->putSectionsToScheme(); $this->GetIblockElements(); $this->putProductsToScheme(); $this->putSchemeToFile(); } static function Agent($IBLOCK_ID, $OFFERS_ID) { $processor = new CCatalogExport($IBLOCK_ID, $OFFERS_ID); $processor->process(); return "\Pai\Tools\CCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);"; } }
Дальше, подключаем данный класс к автозагрузке:
Bitrix\Main\Loader::registerAutoLoadClasses(null, array( 'Pai\Tools\CCatalogExport' => '/bitrix/php_interface/classes/ccatalogexport.php', ));
Проверяем работу класса запустив скрипт:
$processor = new Pai\Tools\CCatalogExport(, ); $processor->process();
И последним шагом - создаем периодический агент с выполнением функции:
\Pai\Tools\CCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);
Имейте ввиду, что данный вариант скрипта не подходит для больших каталогов, которые нельзя обработать за один хит. Если у вас большой каталог - стоит разбить его на части с помощью пагинации и информацию о товарах записывать в файл пошагово.
2018-12-27. Создание дополнительной выгрузки на базе основной.
Предположим, вы создали и настроили основную выгрузку, но вам нужна еще одна (а в реальной ситуации - не одна) с незначительными изменениями структуры. Для этого, нужно создать еще один класс с наследованинем от текущего.
Итак, создаем рядом с файлом с основным классом ее один, например, класс CYmlCatalogExport в файле cymlcatalogexport. В init.php добавляем автозагрузку нового класса:
Bitrix\Main\Loader::registerAutoLoadClasses(null, array( 'Pai\Tools\CCatalogExport' => '/bitrix/php_interface/classes/ccatalogexport.php', 'Pai\Tools\CYmlCatalogExport' => '/bitrix/php_interface/classes/cymlcatalogexport.php', ));
Сам класс примет такой вид:
namespace Pai\Tools; use \Bitrix\Main\Data\Cache; use Bitrix\Main\IO, Bitrix\Main\Application; use Bitrix\Main\Loader; class CYmlCatalogExport extends CCatalogExport { function GetFileSheme() { $Scheme = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" . PHP_EOL; $Scheme .= "<!DOCTYPE yml_catalog SYSTEM \"shops.dtd\" >".PHP_EOL; $Scheme .= "<yml_catalog date=\"".date('Y-m-d H:i')."\">" . PHP_EOL; $Scheme .= "<shop>".PHP_EOL; $Scheme .= "\t<name>" . $this->FirmName . "</name>" . PHP_EOL; $Scheme .= "\t<company>" . $this->FirmName . "</company>" . PHP_EOL; $Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL; $Scheme .= "\t<platform>" . "1С-Битрикс" . "</platform>" . PHP_EOL; $Scheme .= "\t<currencies>".PHP_EOL; $Scheme .= "\t\t<currency id=\"RUR\" rate=\"1.0\" />".PHP_EOL; $Scheme .= "\t</currencies>".PHP_EOL; $Scheme .= "\t<categories>" . PHP_EOL; $Scheme .= "#CATEGORIES#" . PHP_EOL; $Scheme .= "\t</categories>" . PHP_EOL; $Scheme .= "\t<offers>" . PHP_EOL; $Scheme .= "#PRODUCTS#" . PHP_EOL; $Scheme .= "\t</offers>" . PHP_EOL; $Scheme .= "</shop>".PHP_EOL; $Scheme .= "</yml_catalog>"; $this->Scheme = $Scheme; } function putSectionsToScheme() { $categories = ''; foreach ($this->result['SECTIONS'] as $arSection) { $categories .= "\t\t<category id=\"" . $arSection['ID'] . "\" " . ( (intval($arSection['IBLOCK_SECTION_ID']) > 0) ? "parentId=\"" . $arSection['IBLOCK_SECTION_ID'] . "\" " : "") . "portal_url=\"" . $arSection['SECTION_PAGE_URL'] . "\">" . $arSection["NAME"] . "</category>" . PHP_EOL; } $this->Scheme = str_replace('#CATEGORIES#', TrimEx($categories, PHP_EOL, 'right'), $this->Scheme); } function putProductsToScheme() { $productsLine = ''; foreach ($this->result['PRODUCTS'] as $arProduct) { $productsLine .= "\t\t<offer id=\"" . $arProduct["id"] . "\" available=\"" . $arProduct["available"] . "\">" . PHP_EOL; $productsLine .= "\t\t\t<categoryId>" . $arProduct['categoryId'] . "</categoryId>" . PHP_EOL; $productsLine .= "\t\t\t<currencyId>" . $arProduct['currencyId'] . "</currencyId>" . PHP_EOL; $productsLine .= "\t\t\t<name>" . htmlspecialchars($arProduct['name']) . "</name>" . PHP_EOL; $productsLine .= "\t\t\t<url>" . $this->url . $arProduct['url'] . "</url>" . PHP_EOL; $productsLine .= "\t\t\t<price>" . $arProduct['price'] . "</price>" . PHP_EOL; $productsLine .= "\t\t\t<picture>" . $this->url . $arProduct['picture'] . "</picture>" . PHP_EOL; if(!empty($arProduct['more_pictures'])){ $productsLine .= "\t\t\t<add_picture>" . implode(',',$arProduct['more_pictures'])."</add_picture>".PHP_EOL; } $productsLine .= "\t\t\t<description>" . $arProduct['description'] . PHP_EOL . "</description>" . PHP_EOL; $productsLine .= "\t\t\t<store>false</store>".PHP_EOL; $productsLine .= "\t\t\t<pickup>true</pickup>".PHP_EOL; $productsLine .= "\t\t\t<delivery>true</delivery>".PHP_EOL; foreach ($arProduct['params'] as $param => $paramValue) { $productsLine .= "\t\t\t<param name=\"" . $param . "\">" . $paramValue . "</param>" . PHP_EOL; } $productsLine .= "\t\t</offer>" . PHP_EOL; } $this->Scheme = str_replace('#PRODUCTS#', TrimEx($productsLine, PHP_EOL, 'right'), $this->Scheme); } function putSchemeToFile() { $file = new IO\File(Application::getDocumentRoot() . "/upload/export/tmp.xml"); $file->putContents($this->Scheme); $file->rename(Application::getDocumentRoot() . "/upload/export/ymlCatalog.xml"); echo 'Ok'; } static function Agent($IBLOCK_ID, $OFFERS_ID) { $processor = new CYmlCatalogExport($IBLOCK_ID, $OFFERS_ID); $processor->process(); return "\Pai\Tools\CYmlCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);"; } }
Вот так получаем вторую выгрузку на базе первой.
Разработка сайта
Подайте заявку на разработку сайта на базе готового решения от компании 1С-Битрикс или одного из партнеров компании. Максимально подробно опишите, чему будет посвящен сайт, если это интернет-магазин - что он будет продавать, нужна ли мультиязычность, будут ли разные типы цен (розница, опт, крупный опт), будет ли интеграция с 1С, будет ли выгрузка товаров на различные торговые площадки...
Сопровождение сайта
Вы можете подать заявку на сопровождение вашего сайта на базе 1С-Битрикс. Сопровождение включает в себя: проверка актуальности обновлений сайта, проверка актуальности резервной копии, консультации по сайту. Опишите в заявке, какие еще объемы планируются на сопровождении и на какой срок вы планируете заключить договор на сопровождение - мы подберем подходящий вам бюджет на сопровождение
Работы по сайту
Вы можете подать заявку на выполнение определенного объема работ по сайту. Опишите в заявке объем работ. Это может быть разработка какого-то нового функционала, доработки по имеющемуся функционалу, доработки под требования сео-специалистов. На основании заявки вам будет сформирован бюджет работ, а также названы сроки на выполнение тех или иных работ.