06.07.2019

Добавление товаров в корзину из Excel прайса

Представьте что Вам необходимо работать с оптовыми продажами. Или же сайт специализируется на дешевых товарах, которые пользователь будет покупать в больших количествах. Вы столкнетесь с проблемой, когда для добавления сотни товаров в корзину нужно будет тратить довольно много времени.
В этом случае Вам может помочь добавление товаров в корзину из excel файла.

Основная идея в том, чтобы дать скачать пользователю прайс с товарами, в котором будет проставлено необходимое количество товара для покупки.
После этого пользователь загружает такой прайс обратно на сайт, и скрипт добавляет товары из прайса в корзину.

Я реализовал это в виде компонента gricuk:basket.from.file. Дальше приведу код:

class.php

<?php

use Bitrix\Main\Application;
use Bitrix\Main\Loader;
use Bitrix\Sale;
use Bitrix\Main\SystemException;

if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true) die();

class BasketFromFile extends CBitrixComponent
{

    public function onPrepareComponentParams($arParams)
    {
        \Bitrix\Main\Loader::includeModule("iblock");
        \Bitrix\Main\Loader::includeModule("catalog");
        \Bitrix\Main\Loader::includeModule("sale");


        return $arParams;
    }

    public function executeComponent()
    {
        $request = Application::getInstance()->getContext()->getRequest();
        $this->arResult["ACTION"] = $request->get("action");
        if ($this->arResult["ACTION"] == "download") {
            $this->downloadPriceForCurrentUser();
        }

        if ($this->arResult["ACTION"] == "addToBasket") {
            $this->addFromFile();
        }

        $this->includeComponentTemplate();
    }

    public function addFromFile()
    {
        global $USER;

        $dbProducts = \Bitrix\Catalog\ProductTable::getList([
            "select" => [
                "NAME" => "IBLOCK_ELEMENT.NAME",
                "*"
            ],
            "filter" => [
                "IBLOCK_ELEMENT.ACTIVE" => "Y"
            ],
        ]);
        $existProducts = [];
        foreach ($dbProducts as $product) {
            $existProducts[$product["ID"]] = $product;
        }


        $resultArray = [["#", "Категория", "Название", "Доступно, шт", "Цена, руб", "Количество"]];

        try {
            $inputFileName = $_FILES["excelFile"]["tmp_name"];

            $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
            $spreadsheet = $reader->load($inputFileName);

            $worksheet = $spreadsheet->getActiveSheet();
            $products = [];
            foreach ($worksheet->getRowIterator() as $row) {
                $cellIterator = $row->getCellIterator();
                $cellIterator->setIterateOnlyExistingCells(false);
                $cells = [];
                foreach ($cellIterator as $cell) {
                    $cells[] = $cell->getValue();
                }
                if (intval($cells[5]) > 0) {
                    $products[] = [
                        "ID" => $cells[0],
                        "QUANTITY" => $cells[5],
                    ];
                }

            }
            unset($products[0]);
            \CSaleBasket::DeleteAll(\CSaleBasket::GetBasketUserID());
            foreach ($products as $product) {
                if (isset($existProducts[$product["ID"]])) {
                    $addResult = $this->addProductToBasket($product["ID"], $product["QUANTITY"]);
                    if (!$addResult->isSuccess()) {
                        $this->arResult["ERRORS"][] = $addResult->getErrorMessages();

                    }
                }
            }
        } catch (\Exception $e) {
            $this->arResult["ERRORS"][] = $e->getMessage();
        }

    }

    public function addProductToBasket($productId, $quantity)
    {
        \Bitrix\Main\Loader::includeModule("catalog");
        $product = \Bitrix\Catalog\ProductTable::getById($productId)->fetch();
        $productToAdd = [
            "PRODUCT_ID" => $productId
        ];

        if ($product["TYPE"] == \Bitrix\Catalog\ProductTable::TYPE_OFFER) {
            $parentProduct = \CCatalogSku::GetProductInfo($productId);
            $productProperties = CIBlockPriceTools::GetOfferProperties(
                $productId,
                $parentProduct["IBLOCK_ID"],
                [
                    "VOLUME"
                ],
                []
            );
            if (!empty($productProperties)) {
                $productToAdd["PROPS"] = $productProperties;
            }
        }

        return \Bitrix\Catalog\Product\Basket::addProduct(
            $productToAdd,
            [
                "QUANTITY" => $quantity
            ]
        );
    }

    public function downloadPriceForCurrentUser()
    {
        global $USER;

        $dbProducts = \Bitrix\Catalog\ProductTable::getList([
            "select" => [
                "NAME" => "IBLOCK_ELEMENT.NAME",
                "SECTION_ID" => "IBLOCK_ELEMENT.IBLOCK_SECTION_ID",
                "SECTION_NAME" => "IBLOCK_ELEMENT.IBLOCK_SECTION.NAME",
                "*"
            ],
            "filter" => [
                "IBLOCK_ELEMENT.ACTIVE" => "Y",
                "IBLOCK_ELEMENT.IBLOCK_ID" => [
                    \Gricuk\Main\Conf::ID_IBLOCK_CATALOG,
                    \Gricuk\Main\Conf::ID_IBLOCK_OFFERS,
                ],
                [
                    "LOGIC" => "OR",
                    [
                        "!TYPE" => \Bitrix\Catalog\ProductTable::TYPE_SKU,
                        ">QUANTITY" => 0,
                    ],
                    [
                        "TYPE" => \Bitrix\Catalog\ProductTable::TYPE_SKU,
                    ]
                ]
            ],
            "order" => [
                "IBLOCK_ELEMENT.IBLOCK_SECTION.LEFT_MARGIN",
                "IBLOCK_ELEMENT.SORT",
                "IBLOCK_ELEMENT.ID",
            ]
        ]);
        $products = [];
        $skuProducts = [];
        foreach ($dbProducts as $product) {
            if ($product["TYPE"] != \Bitrix\Catalog\ProductTable::TYPE_SKU) {
                $product["PRICE"] = CCatalogProduct::GetOptimalPrice($product["ID"]);
            } else {
                $skuProducts[$product["ID"]] = $product["ID"];
            }

            $products[$product["ID"]] = $product;
        }

        $skuOffersMap = CCatalogSku::getOffersList(array_keys($skuProducts));

        foreach ($skuOffersMap as $skuId => $offers) {
            foreach ($offers as $offerId => $offer) {
                if (isset($products[$skuId]) && isset($products[$offerId])) {
                    $products[$skuId]["OFFERS"][$offerId] = $products[$offerId];
                    unset($products[$offerId]);
                }
            }
        }

        $resultArray = [["#", "Категория", "Название", "Доступно, шт", "Цена, руб", "Количество"]];

        foreach ($products as $product) {
            if ($product["TYPE"] == \Bitrix\Catalog\ProductTable::TYPE_OFFER) {
                continue;
            }

            if ($product["TYPE"] != \Bitrix\Catalog\ProductTable::TYPE_SKU) {
                $resultArray[] = [
                    $product["ID"],
                    $product["SECTION_NAME"],
                    $product["NAME"],
                    $product["QUANTITY"],
                    $product["PRICE"]["RESULT_PRICE"]["DISCOUNT_PRICE"],
                    ""
                ];
            }

            if (isset($product["OFFERS"])) {
                foreach ($product["OFFERS"] as $offer) {
                    $resultArray[] = [
                        $offer["ID"],
                        $product["SECTION_NAME"],
                        $offer["NAME"],
                        $offer["QUANTITY"],
                        $offer["PRICE"]["RESULT_PRICE"]["DISCOUNT_PRICE"],
                        ""
                    ];
                }
            }
        }


        $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
        $sheet = $spreadsheet->getActiveSheet();
        $sheet->fromArray($resultArray);
        $sheet->getColumnDimension('A')->setWidth(10);//#
        $sheet->getColumnDimension('B')->setWidth(35);//Категория
        $sheet->getColumnDimension('C')->setWidth(62);//Название
        $sheet->getColumnDimension('D')->setWidth(14);//Доступно, шт
        $sheet->getColumnDimension('E')->setWidth(14);//Цена, руб
        $sheet->getColumnDimension('F')->setWidth(14);//Количество


        $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
        $filePath = \Bitrix\Main\Application::getDocumentRoot() . "/upload/tmp/price-{$USER->GetID()}.xlsx";

        $writer->save($filePath);

        \Gricuk\Utils\Helpers::file_force_download($filePath);
        die();
    }
}

template.php

<? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true) die();
/** @var array $arParams */
/** @var array $arResult */
/** @global CMain $APPLICATION */
/** @global CUser $USER */
/** @global CDatabase $DB */
/** @var CBitrixComponentTemplate $this */
/** @var string $templateName */
/** @var string $templateFile */
/** @var string $templateFolder */
/** @var string $componentPath */
/** @var CBitrixComponent $component */
$this->setFrameMode(true);
?>
<div class="row">
    <div class="col-sm-12">
        Для того чтобы добавить товары в корзину с помощью Excell прайса необходимо:
        <ul>
            <li>Скачать пример прайса, нажав кнопку <b>"Скачать шаблон"</b></li>
            <li>В полученном xlsx файле ввести необходимое количество товара в колонку <b>"Количество"</b></li>
            <li>Сохранить файл</li>
            <li>На данной странице нажать кнопку <b>"Выберите файл"</b>. В открывшемся окне, указать файл, который был
                ранее сохранен
            </li>
            <li>На странице сайта нажать на кноку <b>"Добавить в корзину"</b></li>
        </ul>
        <h4><b>Внимание!</b> Цены в файле не окончательные! При добавлении в корзину они могут измениться.</h4>
        <h4><b>Внимание!</b> Всё что уже есть в корзине будет удалено!</h4>
    </div>
    <div class="col-sm-12">
        <form action="<?= POST_FORM_ACTION_URI ?>" method="POST" enctype="multipart/form-data">
            <input type="hidden" name="action" value="addToBasket">
            <div class="form-group">
                <label for="excel-price">Файл Excel</label>
                <input type="file" id="excel-price" class="form-control" name="excelFile" required>
            </div>
            <div class="form-group">
                <a href="?action=download" class="btn btn-link">Скачать шаблон</a>
                <button class="btn btn-default">Добавить в корзину</button>
            </div>
        </form>
    </div>
    <? if ($arResult["ACTION"] == "addToBasket" && empty($arResult["ERRORS"])): ?>
        <div class="col-sm-12">
            <?
            ShowMessage(["MESSAGE" => "Товары из файла успешно добавлены в корзину", "TYPE" => "OK"]);
            ?>
        </div>
    <? endif ?>
    <? if ($arResult["ERRORS"]): ?>
        <div class="col-sm-12">
            <?
            foreach ($arResult["ERRORS"] as $error) {
                ShowError($error);
            }
            ?>
        </div>
    <? endif ?>
</div>

Шаблон выполнен без нормального внешнего вида, так как в моем случае компонент используют менеджеры со стороны сайта. Поэтому не было каких то требований к его оформлению.
На сайте выглядит как то так:
Шаблон корзины из файла

Теперь о коде компонента. Как можно увидеть, в нем для работы с Excel используется библиотека PhpSpreadsheet's , поэтому ее надо будет скачать и подключить на сайт.

Код класса можно разделить на 2 части. 1я - формирует прайс для текущего пользователя (метод downloadPriceForCurrentUser). 2я - добавляет товары в корзину из загруженного файла (методы addFromFile и addProductToBasket).
Особое внимание стоит уделить строке в методе addProductToBasket со следующим кодом:

$productProperties = CIBlockPriceTools::GetOfferProperties(
    $productId,
    $parentProduct["IBLOCK_ID"],
    [
        "VOLUME"
    ],
    []
);

В качестве 3 параметра идет массив с символьными кодами свойств торговых предложений, которые необходимо добавлять в корзину. В моем случае это объем.
В конце можно увидеть вызов \Gricuk\Utils\Helpers::file_force_download($filePath);, подробнее об этом можно почитать в статье Принудительная загрузка файла