Рассмотрим вариант, в котором товар помещается в резерв при оформлении заказа на 3 дня.
Резервирование фиксируется как в модуле Торгового каталога - изменение полей у конкретных товаров
- Доступное количество
- Зарезервированное количество
Также о резервировании сохраняется информация и в модуле Интернет-магазин, а именно:
добавляются записи в таблицы - b_sale_basket_reservation, b_sale_basket_reservation_history
Формирование резерва
Резервирование в модуле Интернет-магазин
При оформлении заказа происходит вызов метода, порождающего вызов следующего: \Bitrix\Sale\Order::save() -> \Bitrix\Sale\Basket::save() -> \Bitrix\Sale\BasketItem::save() ->\Bitrix\Sale\ReserveQuantity::save().
В последнем формируются поля описывающие какие товары помещаются в резерв и вставляются в таблицу - b_sale_basket_reservation.
Содержимое полей вставляемой записи
array (
'STORE_ID' => 0,
'BASKET_ID' => 307712,
'QUANTITY' => 1.0,
'DATE_RESERVE_END' => 2023-05-19 00:00:00.000000
'DATE_RESERVE' => 2023-05-16 21:37:39.618866',
)
В это же время помещается запись о резерве в историю резервов \Bitrix\Sale\Reservation\BasketReservationHistoryService::addByReservation()
Резервирование в модуле Торговый каталог
Перед сохранением заказа происходит резервирование товара, при котором:
- доступное количество уменьшается
- зарезервированное количество увеличивается
Stack trace
catalogprovider.php:2260, Bitrix\Catalog\Product\CatalogProvider::reserveStoreQuantityWithEnabledReservation()
catalogprovider.php:2037, Bitrix\Catalog\Product\CatalogProvider::reserveProduct()
catalogprovider.php:1999, Bitrix\Catalog\Product\CatalogProvider->reserve()
transferprovider.php:31, Bitrix\Sale\Internals\TransferProvider->callProviderMethod()
transferprovider.php:87, Bitrix\Sale\Internals\TransferProvider->reserve()
providerbuilderbase.php:89, Bitrix\Sale\Internals\ProviderBuilderBase->callTransferMethod()
providerbuilderbase.php:304, Bitrix\Sale\Internals\ProviderBuilderBase->reserve()
providercreator.php:486, Bitrix\Sale\Internals\ProviderCreator->callBuilderMethod()
providercreator.php:400, Bitrix\Sale\Internals\ProviderCreator->reserve()
shipmentrules.php:538, Bitrix\Sale\Internals\ShipmentRules::saveRules()
provider.php:1289, Bitrix\Sale\Internals\Catalog\Provider::save()
order.php:2122, Bitrix\Sale\Order->onBeforeSave()
orderbase.php:1109, Bitrix\Sale\OrderBase->save()
order.php:2176, Bitrix\Sale\Order->save()
class.php:6389, SaleOrderAjax->saveOrder()
class.php:4803, SaleOrderAjax->saveOrderAjaxAction()
class.php:6200, SaleOrderAjax->doAction()
class.php:6458, SaleOrderAjax->executeComponent()
component.php:660, CBitrixComponent->includeComponent()
main.php:1068, CAllMain->IncludeComponent()
ajax.php:45, {main}()
Окончание резерва со списанием
Чтобы резерв был списан (подразумевается что товар отправлен покупателю и его общее количество уменьшилось), необходимо чтобы в заказе была разрешена доставка и отгрузка. Отгрузка может быть разрешена автоматически при разрешении доставки.
Разрешение доставки при оплате
Чтобы разрешить доставку и отгрузку при получении оплаты необходимо это включить в настройках модуля интернет магазина.
Списание резерва в модуле Торговый катлог
При разрешении доставки и отгрузки
- доступное количество не меняется
- зарезервированное количество уменьшается
Stack trace:
catalogprovider.php:1525, Bitrix\Catalog\Product\CatalogProvider::shipQuantityWithoutStoreControl()
catalogprovider.php:1292, Bitrix\Catalog\Product\CatalogProvider::shipProduct()
catalogprovider.php:1134, Bitrix\Catalog\Product\CatalogProvider->shipProducts()
catalogprovider.php:841, Bitrix\Catalog\Product\CatalogProvider->ship()
transferprovider.php:31, Bitrix\Sale\Internals\TransferProvider->callProviderMethod()
transferprovider.php:64, Bitrix\Sale\Internals\TransferProvider->ship()
providerbuilderbase.php:89, Bitrix\Sale\Internals\ProviderBuilderBase->callTransferMethod()
providerbuilderbase.php:312, Bitrix\Sale\Internals\ProviderBuilderBase->ship()
providercreator.php:486, Bitrix\Sale\Internals\ProviderCreator->callBuilderMethod()
providercreator.php:408, Bitrix\Sale\Internals\ProviderCreator->ship()
shipmentrules.php:554, Bitrix\Sale\Internals\ShipmentRules::saveRules()
provider.php:1289, Bitrix\Sale\Internals\Catalog\Provider::save()
order.php:2122, Bitrix\Sale\Order->onBeforeSave()
orderbase.php:1109, Bitrix\Sale\OrderBase->save()
order.php:2176, Bitrix\Sale\Order->save()
order_ajax.php:1591, Bitrix\Sale\AdminPage\AjaxProcessor->updateShipmentStatusAction()
order_ajax.php:182, call_user_func:{/var/www/html/bitrix/modules/sale/admin/order_ajax.php:182}()
order_ajax.php:182, Bitrix\Sale\AdminPage\AjaxProcessor->processRequest()
order_ajax.php:61, require_once()
sale_order_ajax.php:2, {main}()
Списание резерва в модуле Интернет-магазин
При разрешении доставки и отгрузки происходит удаление записи о резервировании в таблице b_sale_basket_reservation и затем в в само конце операций b_sale_basket_reservation_history
datamanager.php:1674, Bitrix\Main\ORM\Data\DataManager::delete()
basketreservationservice.php:92, Bitrix\Sale\Reservation\BasketReservationService->delete()
reservequantitycollection.php:223, Bitrix\Sale\ReserveQuantityCollection::deleteInternal()
reservequantitycollection.php:135, Bitrix\Sale\ReserveQuantityCollection->save()
basketitem.php:62, Bitrix\Sale\BasketItem->save()
basketbase.php:489, Bitrix\Sale\BasketBase->save()
basket.php:228, Bitrix\Sale\Basket->save()
orderbase.php:1466, Bitrix\Sale\OrderBase->saveEntities()
order.php:2056, Bitrix\Sale\Order->saveEntities()
orderbase.php:1151, Bitrix\Sale\OrderBase->save()
order.php:2176, Bitrix\Sale\Order->save()
order_ajax.php:1591, Bitrix\Sale\AdminPage\AjaxProcessor->updateShipmentStatusAction()
order_ajax.php:182, call_user_func:{/var/www/html/bitrix/modules/sale/admin/order_ajax.php:182}()
order_ajax.php:182, Bitrix\Sale\AdminPage\AjaxProcessor->processRequest()
order_ajax.php:61, require_once()
sale_order_ajax.php:2, {main}()
Окончание резервирования по таймауту
Если за фиксированный период ожидания по заказу не произошло никаких изменений, из примера - через 3 дня, то будет произведено восстановление остатков товара.
Ежедневно выполняет агент CSaleOrder::ClearProductReservedQuantity() , который вызывает метод \Bitrix\Sale\Helpers\ReservedProductCleaner::bind(60). Таким образом добавляется отдельный агент который ежеминутно будет выполняться и разбирать все записи в таблице с резервами в модуле интернет-магазина
При разборе списка резервов берутся только те записи для которых:
- Заказ не оплачен
- Заказ не отменен
- Достигнута дата до которой формировался резерв
- Количество в резерве больше 0
Логику можно посмотреть в методе \Bitrix\Sale\Helpers\ReservedProductCleaner::execute()
sql запрос для выборки записей
SELECT `sale_reservation_internals_basket_reservation_order`.`ID` AS `ORDER_ID`,
`sale_reservation_internals_basket_reservation`.`ID` AS `ID`,
`sale_reservation_internals_basket_reservation`.`BASKET_ID` AS `BASKET_ID`,
`sale_reservation_internals_basket_reservation_basket`.`ID` AS `UALIAS_0`
FROM `b_sale_basket_reservation` `sale_reservation_internals_basket_reservation`
INNER JOIN `b_sale_basket` `sale_reservation_internals_basket_reservation_basket`
ON `sale_reservation_internals_basket_reservation`.`BASKET_ID` =
`sale_reservation_internals_basket_reservation_basket`.`ID`
INNER JOIN `b_sale_order` `sale_reservation_internals_basket_reservation_order`
ON `sale_reservation_internals_basket_reservation_basket`.`ORDER_ID` =
`sale_reservation_internals_basket_reservation_order`.`ID`
WHERE `sale_reservation_internals_basket_reservation`.`QUANTITY` > 0
AND `sale_reservation_internals_basket_reservation`.`DATE_RESERVE_END` <= '2023-05-17 10:27:06'
AND `sale_reservation_internals_basket_reservation_order`.`PAYED` = 'N'
AND `sale_reservation_internals_basket_reservation_order`.`CANCELED` = 'N'
LIMIT 0, 100
Найденные записи будут удалены а у связанных с ними товаров будут
- увеличено общее количество
- уменьшено зарезервированное количество
Окончание резерва при отмене
Окончание резерва также происходит при отмене заказа с восстановлением исходных значений. При этом если снять отмену, то резерв будет восстановлен.
Подвисание резервов при оплате
Если заказ оплачен при этом не была разрешена доставка и отгрузка - резерв остается. И соответственно доступное количество товаров к покупке будет меньше реального. Что приводит к вопросам - Почему резервирование работает неправильно, товар есть а заказать его нельзя.
Окончание резерва при смене статуса заказа на финальный
Чтобы при смене статуса заказа на финальный (Выполнен [F]) автоматически разрешалась доставка, необходимо например в файл init.php добавить обработчик события смена статуса заказа
/**
* Аторазрешение доставки при финальном статусе заказа
* (разрешение отгрузки при разрешении доставки
* задается в настрйоках модуль интернет-магазин)
*/
$eventManager = \Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler(
"sale",
"OnSaleBeforeStatusOrderChange",
"sale_OnSaleBeforeStatusOrderChange"
);
function sale_OnSaleBeforeStatusOrderChange(\Bitrix\Main\Event $event)
{
/**
* @var \Bitrix\Sale\Order $order
*/
$order = $event->getParameter('ENTITY');
$value = $event->getParameter('VALUE');
$oldValue = $event->getParameter('OLD_VALUE');
do {
if ($value !== 'F') {
break;
}
if ($value === $oldValue) {
break;
}
$collection = $order->getShipmentCollection();
foreach ($collection as $shipment) {
/**
* @var \Bitrix\Sale\Shipment $shipment
*/
if ($shipment->isSystem()) {
continue;
}
if (!$shipment->isAllowDelivery()) {
$r = $shipment->allowDelivery();
if (!$r->isSuccess()) {
// $r->getErrorMessages() to log
}
}
}
} while (false);
return new \Bitrix\Main\EventResult(\Bitrix\Main\EventResult::SUCCESS, null, null, __METHOD__);
}
Очистка зарезервированного или доступного количества у товаров каталога
Очистить поля товаров Зарезервированное количество и/или Доступное количество можно на странице настроек модуля "Торговый каталог". Главное выбрать верно инфоблок с товарами и/или торговыми предложениями
Проверка наличия агентов
Среди агентов должн быть агент отвечающий за снятие резерва по таймауту.
CSaleOrder::ClearProductReservedQuantity();
Просмотр какие товары и в каком количестве сейчас зарезервированы, какие остатки, какие несоответствия
-- зарезервированы заказами --
select SUM(RESERVER_QUANTITY) as BASKET_RESERVED_QUANTITY,
rb.RESERVED_PRODUCT_ID as BASKET_RESERVED_PRODUCT_ID
from (select r.QUANTITY as RESERVER_QUANTITY,
b.PRODUCT_ID as RESERVED_PRODUCT_ID
from b_sale_basket_reservation r
left join b_sale_basket b on b.ID = r.BASKET_ID
where r.DATE_RESERVE_END < now()) rb
group by BASKET_RESERVED_PRODUCT_ID
-- посомтреть какие товары зарезервирвоаны по данным каталога
select ID,
CATALOG_QUANTITY,
CATALOG_RESERVED_QUANTITY,
(CATALOG_QUANTITY + CATALOG_RESERVED_QUANTITY) AS VALUE,
SUM(STORE_AMOUNT) as STORE_AMOUNT,
SUM(STORE_RESERVED) as STORE_RESERVED,
SUM(STORE_AMOUNT + STORE_RESERVED) AS NEED_VALUE
from (select a.ID,
a.QUANTITY as CATALOG_QUANTITY,
a.QUANTITY_RESERVED as CATALOG_RESERVED_QUANTITY,
IFNULL(b.AMOUNT, 0) as STORE_AMOUNT,
IFNULL(b.QUANTITY_RESERVED, 0) AS STORE_RESERVED
from b_catalog_product a
left join b_catalog_store_product b on b.PRODUCT_ID = a.ID) cs
group by ID
-- отличия
select *
from (select ID,
(CATALOG_QUANTITY + CATALOG_RESERVED_QUANTITY) AS VALUE,
SUM(STORE_AMOUNT + STORE_RESERVED) AS NEED_VALUE,
CATALOG_QUANTITY,
CATALOG_RESERVED_QUANTITY,
SUM(STORE_AMOUNT) as STORE_AMOUNT,
SUM(STORE_RESERVED) as STORE_RESERVED
from (select a.ID,
a.QUANTITY as CATALOG_QUANTITY,
a.QUANTITY_RESERVED as CATALOG_RESERVED_QUANTITY,
IFNULL(b.AMOUNT, 0) as STORE_AMOUNT,
IFNULL(b.QUANTITY_RESERVED, 0) AS STORE_RESERVED
from b_catalog_product a
left join b_catalog_store_product b on b.PRODUCT_ID = a.ID) cs
group by ID) csa
where VALUE != NEED_VALUE