Перейти к содержимому

Интернет-магазин на 1С-Битрикс - каталог, корзина, заказы

Магазин на 1С-Битрикс собирается из трёх модулей: контент товара живёт в инфоблоках, торговые данные - в каталоге, а корзина и заказы - в продажах. Разберём эту связку, программное создание заказа и типичные ошибки, из-за которых товар «есть, но не покупается».

Как это работает

Три модуля и разделение ответственности. Модуль iblock хранит карточку товара как элемент инфоблока. Модуль catalog отвечает за торговые данные: цену, остаток, НДС, доступность. Модуль sale - за корзину, заказы, оплаты, доставки и скидки уровня корзины.

Товар - это элемент плюс запись каталога. Пока не создана товарная запись с тем же идентификатором, элемент остаётся просто карточкой контента: его не увидят ни корзина, ни заказы, ни скидки. Это причина номер один ситуации «создал товар кодом, а он не работает».

Торговые предложения. Товар с вариантами - это родительская карточка в одном инфоблоке и набор предложений в другом. Связь задаёт специальное свойство предложения, которое создаётся автоматически при включении опции торговых предложений. Отбор варианта покупателем работает только по немножественным свойствам типов «список», «справочник» и «привязка к элементам».

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

Торговые данные - только в объектах каталога. Цена, остаток, единица измерения, НДС и доступность хранятся в таблицах модуля каталога. Если «рабочую» цену положить в свойство инфоблока, её не увидят ни корзина, ни заказ, ни скидки, ни склад.

Цены и типы цен. Тип цены - это ценовая колонка для группы покупателей. Базовый тип всегда один и неудаляем, дополнительные создаются рядом. Права на тип цены задаются по группам пользователей отдельно от доступности товара: товар может быть в наличии, но без права на тип цены покупатель его не увидит и не купит. Итоговую цену со скидками, купонами и диапазонами считает специальный метод - вручную её не собирают.

Скидки двух видов. Скидки каталога уменьшают цену товара до попадания в корзину. Правила корзины действуют на состав и сумму заказа. Логика описывается деревьями условий и действий, а не плоскими полями.

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

Примеры

1. Создание товара: два обязательных шага

use Bitrix\Main\Loader;
use Bitrix\Catalog\Model\Product;
use Bitrix\Catalog\Model\Price;
Loader::requireModule('iblock');
Loader::requireModule('catalog');
// шаг 1: элемент инфоблока
$el = new CIBlockElement();
$elementId = $el->Add([
'IBLOCK_ID' => $iblockId,
'NAME' => 'Кресло Осло',
'ACTIVE' => 'Y',
]);
// шаг 2: товарная запись - без неё это не товар
$result = Product::add([
'ID' => $elementId,
'TYPE' => Product::TYPE_PRODUCT,
'QUANTITY' => 10,
'AVAILABLE' => 'Y',
]);
// цена
Price::add([
'PRODUCT_ID' => $elementId,
'CATALOG_GROUP_ID' => 1, // тип цены
'PRICE' => 15900,
'CURRENCY' => 'RUB',
]);

Перед добавлением цены имеет смысл проверить, нет ли уже записи на пару «товар плюс тип цены» - иначе появится дубль. И помните: у товара с предложениями цена принадлежит предложению, а не родительской карточке.

2. Программное создание заказа

use Bitrix\Sale;
Loader::requireModule('sale');
$basket = Sale\Basket::create(SITE_ID);
foreach ($products as $product) {
$item = $basket->createItem('catalog', $product['PRODUCT_ID']);
$item->setFields([
'QUANTITY' => $product['QUANTITY'],
'CURRENCY' => 'RUB',
'LID' => SITE_ID,
'PRODUCT_PROVIDER_CLASS' => \CCatalogProductProvider::class,
]);
}
$order = Sale\Order::create(SITE_ID, $userId);
$order->setPersonTypeId(1);
$order->setBasket($basket);
// отгрузка
$shipmentCollection = $order->getShipmentCollection();
$shipment = $shipmentCollection->createItem(
Sale\Delivery\Services\Manager::getObjectById($deliveryId)
);
$shipmentItemCollection = $shipment->getShipmentItemCollection();
foreach ($basket as $item) {
$shipmentItem = $shipmentItemCollection->createItem($item);
$shipmentItem->setQuantity($item->getQuantity());
}
// оплата
$paymentCollection = $order->getPaymentCollection();
$payment = $paymentCollection->createItem(
Sale\PaySystem\Manager::getObjectById($paySystemId)
);
$payment->setField('SUM', $order->getPrice());
$result = $order->save();
if (!$result->isSuccess()) {
foreach ($result->getErrors() as $error) {
// обработка ошибок
}
}

Один вызов сохранения каскадно записывает заказ, корзину, отгрузки и платежи и возвращает объект результата. Для нераспределённых товаров система сама создаёт служебную отгрузку.

3. Смена статуса и оплата

Специализированные методы вместо общего обновления полей - принципиальный момент: только они гарантируют вызов всей сопутствующей логики, включая обработчики оплаты, списание остатков и почтовые уведомления.

Статусы бывают двух типов: статус заказа в целом и статус отгрузки. Есть неудаляемые служебные: начальный и финальный для заказа, ожидание обработки и отгрузка для отгрузки. При включённом уведомлении смена статуса порождает почтовое событие.

4. Своя служба доставки

class MyDeliveryHandler extends \Bitrix\Sale\Delivery\Services\Base
{
protected function calculateConcrete(\Bitrix\Sale\Shipment $shipment): \Bitrix\Sale\Delivery\CalculationResult
{
$result = new \Bitrix\Sale\Delivery\CalculationResult();
$result->setDeliveryPrice($this->calculatePrice($shipment));
return $result;
}
}

Служба состоит из профилей с тарифами, имеет ограничения по весу, габаритам, местоположению и типу плательщика. Для расчёта по адресу нужно свойство заказа типа местоположения с соответствующим флагом - по одному на тип плательщика.

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

Справочник API

APIНазначениеОсобенности
Catalog\Model\Productтоварная записьбез неё элемент не является товаром
Catalog\Model\Priceцены товарапроверяйте существующую запись перед добавлением
Catalog\ProductTable и другие таблицычтение товарных данныхзапись - через модели
CCatalogSKU::getInfoByIBlock()тип каталога и связь с предложениями
CCatalogProduct::GetOptimalPrice()итоговая цена со скидкамивручную цену не считают
CCatalogDiscountскидки каталогаD7-таблица только для чтения
Sale\Basket::create()корзинапозиции через createItem
Sale\Order::create()заказсохранение каскадное
getShipmentCollection()отгрузки заказасистемная отгрузка создаётся автоматически
getPaymentCollection()платежи заказаоплату можно разделить на части
Delivery\Services\Baseсвоя служба доставкиметод расчёта возвращает результат
PaySystem\ServiceHandlerобработчик платёжной системыответы приходят на служебный скрипт
CCatalogDocsскладские документыостатки меняют только ими
Catalog\Config\State::isUsedInventoryManagement()включён ли складской учёт

Частые ошибки

Элемент создан, но товаром не стал. Не вызвана регистрация товарной записи.

Цена не участвует в расчёте. Она добавлена родительской карточке, а не предложению. Для товаров с вариантами цена принадлежит предложению.

Дубли цен. Запись создана без проверки существующей пары «товар плюс тип цены».

Товар доступен, но не покупается. Кроме доступности проверяются активность элемента, даты публикации и права группы на тип цены.

Цена в каталоге и корзине различается. В корзине показывается наименьшая доступная пользователю цена, даже если этот тип цены скрыт в каталоге.

Запись скидок через D7-таблицу. Методы изменения там - заглушки, которые бросают исключение. Скидки пишут классическим API.

Прямые SQL-запросы в таблицы каталога. Обходят проверки, события и пересчёт доступности - данные разъезжаются.

Остатки меняют напрямую при включённом складском учёте. Движения должны идти через складские документы.

Частые вопросы

Почему созданный кодом товар не появляется в каталоге?

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

Где хранить цену и остаток - в свойствах инфоблока или в каталоге?

Только в объектах каталога. Свойство инфоблока подходит для справочной информации, но рабочая цена и остаток должны лежать в таблицах модуля каталога: иначе их не увидят корзина, заказ, скидки и складской учёт. Итоговую цену со скидками и правами считает штатный метод, а не ваш код.

Как правильно менять статус заказа из кода?

Специализированными методами, а не общим обновлением полей. Только они запускают сопутствующую логику: обработчики оплаты, списание и резервирование остатков, почтовые уведомления, автоматизацию процессов. Обновление полей напрямую формально сработает, но бизнес-логика вокруг него не выполнится.

Товар в наличии, а купить нельзя. Что проверить?

По порядку: активен ли элемент и попадает ли он в даты публикации; есть ли у группы пользователя право видеть и покупать по этому типу цены; есть ли вообще цена для нужного типа; для товаров с предложениями - есть ли доступное предложение. Доступность в товарной записи - лишь одно из условий, а не гарантия покупки.

Связанные темы

Первоисточники