#sale

Советы с тегом «sale»

3 совета

Резерв корзины в Sale — не складской документ

Резерв корзины в Sale — не складской документ

Проблема/контекст

Резерв позиции заказа и складской резерв — разные сущности. \Bitrix\Catalog\StoreProductTable::QUANTITY_RESERVED показывает итоговое число по паре товар-склад, а снимается оно документом TYPE_UNDO_RESERVE. Строку резерва конкретной позиции корзины ведёт модуль sale — через \Bitrix\Sale\Reservation\BasketReservationService. Правка складского остатка мимо него оставит заказ и склад рассогласованными.

Решение с кодом

Сервис зарегистрирован в sale/.settings.php под ключом sale.basketReservation, зависимость BasketReservationHistoryService собирается в constructorParams. Есть и статический ярлык BasketReservationService::getInstance().

Данные лежат в b_sale_basket_reservation (BasketReservationTable): QUANTITY, DATE_RESERVE, DATE_RESERVE_END, BASKET_ID — обязательные, плюс STORE_ID и RESERVED_BY. Все три метода записи возвращают Bitrix\Main\Result:

        <?php
declare(strict_types=1);

use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Loader;
use Bitrix\Main\Type\DateTime;
use Bitrix\Sale\Reservation\BasketReservationService;

Loader::includeModule('sale');

/** @var BasketReservationService $service */
$service = ServiceLocator::getInstance()->get('sale.basketReservation');

$result = $service->add([
    'BASKET_ID' => $basketId,
    'STORE_ID' => $storeId,
    'QUANTITY' => 3.0,
    'DATE_RESERVE' => new DateTime(),
    'DATE_RESERVE_END' => (new DateTime())->add('7 days'),
]);

if ($result->isSuccess())
{
    $service->update((int)$result->getId(), ['QUANTITY' => 5.0]);
}

    

Ключевое отличие от прямой работы с таблицей — история. add() после успешной вставки вызывает addByReservation(), update(int $id, array $fields) — updateByReservation(), delete(int $id) — deleteByReservation(). История в BasketReservationHistoryTable хранит не снимки, а дельты: при изменении количества с 80 на 90 добавляется строка на 10. По этим дельтам и их датам считается, сколько позиция реально может списать со склада:

        <?php
declare(strict_types=1);

// $ret[$productId][$storeId] => доступное количество
$byOrder = $service->getAvailableCountForOrder($orderId);

$available = $service->getAvailableCountForBasketItem($basketId, $storeId); // float

    

Логика такая: если первым зарезервировали 80 из 100, а вторым — 40, то первая сделка спишет 80, вторая — 20. Порядок резервирования учитывается.

В прикладном коде заказа отдельный вызов сервиса обычно не нужен: ReserveQuantity::addInternal(), updateInternal() и ReserveQuantityCollection::deleteInternal() уже ходят в sale.basketReservation, поэтому $basketItem->getReserveQuantityCollection() пишет историю автоматически. Сервис нужен для диагностики, миграций и внешних интеграций.

Условие резерва и срок хранения приходят из ReservationSettingsService (ключ sale.reservation.settings) — метод get() читает опции sale/product_reserve_condition и sale/product_reserve_clear_period, собирает ReservationSettings и рассылает ReservationSettingsBuildEvent с именем OnReservationSettingsBuild. В обработчике можно вызвать $event->getSettings()->setReserveCondition(ReserveCondition::ON_CREATE) и включить автоматический резерв, не меняя настройки модуля. Допустимые значения — ON_CREATE, ON_PAY, ON_FULL_PAY, ON_ALLOW_DELIVERY, ON_SHIP; setReserveCondition() валидирует их и бросает SystemException.

Итог

Резерв позиции — это строка b_sale_basket_reservation плюс история дельт, а не складской документ. Меняйте её через sale.basketReservation, а условие резервирования переопределяйте событием OnReservationSettingsBuild.

Как маркировать предупреждения в Bitrix\Sale

Как маркировать предупреждения в Bitrix\Sale

Проблема/контекст

В sale не каждая проблемная ситуация должна ронять сохранение заказа. Часто интеграция с доставкой, кассой или внутренней проверкой находит состояние, которое нужно показать оператору, но не превращать в фатальную ошибку. В ядре для этого есть не только поля MARKED и REASON_MARKED, но и отдельный механизм Bitrix\Sale\EntityMarker: он берет warnings из Bitrix\Sale\Result, создает marker-записи, записывает последнюю причину в заказ и помечает оплату или отгрузку как проблемную.

Практически это значит: если вы уже возвращаете Result с предупреждениями, не дублируйте еще один собственный реестр проблем. Используйте штатный контур Sale.

Решение с кодом

Ключевой момент в ядре такой: EntityMarker::addMarker() работает именно с warnings, а не с обычными errors. Поэтому для нефатального кейса нужен Bitrix\Sale\ResultWarning.

        <?php
declare(strict_types=1);

use Bitrix\Main\Loader;
use Bitrix\Sale\EntityMarker;
use Bitrix\Sale\Order;
use Bitrix\Sale\Result;
use Bitrix\Sale\ResultWarning;
use Bitrix\Sale\Shipment;

Loader::includeModule('sale');

function markShipmentIntegrationWarning(Order $order, Shipment $shipment): void
{
    $result = new Result();

    // Нефатальная проблема: заказ сохранять можно, но отгрузку нужно подсветить оператору
    $result->addWarning(new ResultWarning(
        'Не удалось подтвердить трек-номер во внешней службе доставки',
        'SHIPMENT_TRACKING_SYNC_WARNING'
    ));

    EntityMarker::addMarker($order, $shipment, $result);
    EntityMarker::saveMarkers($order);

    $saveResult = $order->save();
    if (!$saveResult->isSuccess())
    {
        throw new \RuntimeException(implode('; ', $saveResult->getErrorMessages()));
    }
}

    

После этого в заказ попадет REASON_MARKED, у отгрузки выставится MARKED = 'Y', а сам marker сохранится в таблице маркеров. Это важнее простого $shipment->setField('MARKED', 'Y'): в ядре сохраняются код проблемы, тип маркера (AUTO или MANUAL) и статус успешности исправления.

Warning в заказе

Когда причина уже устранена, снимайте проблему не только с поля, но и с marker-записи. Для этого можно удалить маркеры сущности и пересчитать состояние заказа через refreshMarkers().

        <?php
declare(strict_types=1);

use Bitrix\Sale\EntityMarker;
use Bitrix\Sale\Order;
use Bitrix\Sale\Shipment;

function clearShipmentMarkers(Order $order, Shipment $shipment): void
{
    // deleteByEntity() работает только для уже сохраненной сущности с ID
    EntityMarker::deleteByEntity($shipment);
    EntityMarker::refreshMarkers($order);

    $saveResult = $order->save();
    if (!$saveResult->isSuccess())
    {
        throw new \RuntimeException(implode('; ', $saveResult->getErrorMessages()));
    }
}

    

Итог

EntityMarker в sale нужен не для декоративного флага, а для нормального жизненного цикла предупреждений. Если проблема не фатальна, но должна остаться в истории и интерфейсе заказа, сохраняйте ее как marker, а после исправления очищайте через API маркеров, а не только через поле MARKED.

Обновление свойств и количества товаров в корзине Bitrix через Sale API

Обновление свойств и количества товаров в корзине Bitrix через Sale API

В современной разработке на платформе 1С-Битрикс любые манипуляции с корзиной покупателя должны выполняться исключительно через объектно-ориентированное API модуля «Интернет-магазин» (sale). Распространенной ошибкой среди начинающих разработчиков является попытка прямого изменения записей в таблице базы данных b_sale_basket или использование устаревших методов глобального класса CSaleBasket. Такой подход крайне опасен, поскольку он не инициирует сложную цепочку внутренних бизнес-процессов ядра: автоматический пересчет правил корзины, применение скидок, расчет налогов и актуализацию стоимости доставки. В результате данные в корзине становятся некорректными, что ведет к финансовым потерям и сбоям при оформлении заказа.

Для правильного и безопасного обновления данных необходимо работать с высокоуровневыми объектами корзины. Ключевым инструментом в данном контексте выступает класс Bitrix\Sale\Basket. Загрузка объектов обычно осуществляется через статический метод loadItemsForFUser, который принимает идентификатор владельца корзины (FUser ID) и идентификатор сайта. Это позволяет получить актуальный объект корзины со всеми вложенными элементами, даже если заказ еще не начал оформляться. После того как корзина загружена, вы можете получить доступ к конкретному элементу Bitrix\Sale\BasketItem, используя его внутренний идентификатор или выполнив поиск по PRODUCT_ID.

Объект BasketItem предоставляет набор методов для управления полями записи. Например, метод setField('QUANTITY', $value) позволяет изменить количество товара, а работа с кастомными характеристиками товара осуществляется через специальную коллекцию свойств, доступную через метод getPropertyCollection(). Это критически важно для товаров с торговыми предложениями или индивидуальными параметрами, которые выбирает пользователь.

Пример реализации кода для обновления параметров товара в корзине:

        use Bitrix\Main\Loader;
use Bitrix\Sale\Basket;
use Bitrix\Sale\Fuser;
use Bitrix\Main\Context;

// Обязательная проверка подключения модуля sale
if (Loader::includeModule('sale')) {
    // Получаем внутренний идентификатор корзины текущего пользователя
    $fUserId = Fuser::getId();
    $siteId = Context::getCurrent()->getSite();

    // Загружаем объект корзины со всеми товарами
    $basket = Basket::loadItemsForFUser($fUserId, $siteId);

    // Поиск конкретного элемента корзины по ID товара (PRODUCT_ID)
    // В реальных задачах ID часто передается через AJAX-запрос
    $productId = 201;
    $basketItems = $basket->getExistsItems('catalog', $productId, null); // здесь null - если нам не важны свойства товара, например, размер

    if (!empty($basketItems)) {
        // Берем первый найденный элемент
        $basketItem = current($basketItems);
			
        // 1. Изменение количества товара с автоматической проверкой доступности
        $basketItem->setField('QUANTITY', 5);

        // 2. Управление коллекцией свойств товара (например, цвет или размер)
        $propertyCollection = $basketItem->getPropertyCollection();
        
        // Метод setProperty позволяет массово обновить или добавить свойства
        $propertyCollection->setProperty([
            [
                'NAME' => 'Размер',
                'CODE' => 'SIZE',
                'VALUE' => 'XL',
                'SORT' => 100,
            ],
        ]);

        // 3. Вызов метода save() сохраняет изменения в БД и запускает пересчеты
        $saveResult = $basket->save();

        if (!$saveResult->isSuccess()) {
            // В случае ошибки возвращаем массив описаний проблем
            $errors = $saveResult->getErrorMessages();
            // Логирование ошибок или вывод пользователю
        }
    }
}

    

Важно учитывать контекст выполнения: если объект корзины уже является частью объекта заказа (Bitrix\Sale\Order), то вызывать метод save() непосредственно у корзины не рекомендуется. В этом случае правильнее вызвать сохранение всего заказа через $order->save(). Это гарантирует, что итоговая сумма заказа, налоги и все связанные сущности (отгрузки, оплаты) будут синхронизированы и пересчитаны в рамках одной транзакции.

Использование объектной модели BasketItem — это стандарт качественной разработки, который обеспечивает масштабируемость и стабильность вашего решения при любых обновлениях платформы.

Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…
Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти