Резерв корзины в 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.
Комментарии (0)
Пожалуйста, войдите в аккаунт, чтобы оставить комментарий
Оставить комментарийЗагрузка...
Пока нет ни одного комментария. Будьте первым!