Статусы и события
Статусы показывают, на каком этапе находятся заказ и его отгрузки. Обработчики событий модуля sale выполняют дополнительное действие до и после сохранения, оплаты, отмены или смены статуса.
Заказ, оплаты и отгрузки меняют через связанные PHP-объекты. Подробнее в статье Схема работы интернет-магазина и основные объекты.
Чем статус заказа отличается от статуса отгрузки
Заказ и отгрузка проходят разные жизненные циклы. Поэтому модуль хранит для них разные типы статусов.
|
Состояние |
Где хранится |
Класс списка статусов |
Начальный и финальный статусы |
|
Заказ |
Поле |
|
|
|
Отгрузка |
Поле |
|
|
У одного заказа может быть несколько отгрузок, и у каждой отгрузки свой статус. Статус не заменяет другие признаки заказа:
-
Статус отгрузки не означает, что заказ оплачен.
-
Статус заказа не заменяет признаки оплаты, отмены, разрешения доставки и списания товаров.
Оплату меняйте через объект оплаты, отгрузку — через объект отгрузки. Подробнее об отгрузках читайте в статье Доставка и отгрузки, об оплатах — в статье Оплаты и платежные системы.
Основные объекты
Для операций со статусом заказа используйте класс OrderStatus, для статуса отгрузки — DeliveryStatus. Поля статуса, его локализованные название и описание можно получить через ORM-классы StatusTable и StatusLangTable.
Чтобы создать или изменить статус, используйте сценарии из статьи Базовые настройки интернет-магазина.
|
API |
Роль |
|
|
Возвращает:
|
|
|
Возвращает:
|
|
|
Хранит код, тип, сортировку, признак уведомления, цвет и внешний идентификатор статуса |
|
|
Хранит название и описание статуса для конкретного языка |
|
|
Читает журнал изменений заказа и формирует описание записи |
|
|
Регистрирует обработчики событий модуля |
Поля статуса
Класс StatusTable хранит данные статусов заказа и отгрузки. Поле TYPE разделяет записи. Используйте константы StatusTable::TYPE_ORDER и StatusTable::TYPE_SHIPMENT, чтобы не передавать значения строками.
|
Поле |
Назначение |
|
|
Код статуса из одной или двух латинских букв |
|
|
Тип статуса — |
|
|
Порядок статуса в списке. Значение по умолчанию — |
|
|
Признак штатного уведомления о смене статуса, |
|
|
Цвет статуса в интерфейсе |
|
|
Внешний идентификатор статуса |
Получить локализованные статусы
Составной ключ класса StatusLangTable образуют код статуса STATUS_ID и двухбуквенный код языка LID. Поле NAME обязательно, поле DESCRIPTION содержит необязательное описание.
В StatusTable определена ORM-связь STATUS_LANG со StatusLangTable по коду статуса. Поэтому локализованные поля можно получить через StatusTable::getList() по путям STATUS_LANG.NAME, STATUS_LANG.DESCRIPTION и STATUS_LANG.LID.
Пример. Код получает статусы заказа с названием и описанием на выбранном языке, сортирует их по SORT и собирает массив по коду статуса.
$languageId = 'ru';
$statusResult = \Bitrix\Sale\Internals\StatusTable::getList([
'select' => [
'ID',
'TYPE',
'SORT',
'NOTIFY',
'COLOR',
'XML_ID',
'NAME' => 'STATUS_LANG.NAME',
'DESCRIPTION' => 'STATUS_LANG.DESCRIPTION',
],
'filter' => [
'=TYPE' => \Bitrix\Sale\Internals\StatusTable::TYPE_ORDER,
'=STATUS_LANG.LID' => $languageId,
],
'order' => ['SORT' => 'ASC'],
]);
$statuses = [];
while ($status = $statusResult->fetch())
{
$statuses[$status['ID']] = [
'NAME' => $status['NAME'],
'DESCRIPTION' => $status['DESCRIPTION'],
'NOTIFY' => $status['NOTIFY'],
'COLOR' => $status['COLOR'],
];
}
Запрос с фильтром по STATUS_LANG.LID возвращает только записи выбранного языка. Статусы без перевода на этот язык в выборку не попадут.
Базовый сценарий
Чтобы сменить статус и обработать смену в событии, выполните действия по порядку:
-
Загрузите заказ.
-
Получите текущий
STATUS_IDзаказа или отгрузки. -
Запросите разрешенные целевые статусы для пользователя.
-
Проверьте, что нужный статус входит в результат.
-
Измените
STATUS_IDчерез объект заказа или отгрузки. -
Сохраните заказ методом
Order::save()и проверьте ошибки. -
Выполните дополнительное действие в подходящем событии и убедитесь, что повторный вызов обработчика не дублирует результат.
После сохранения статуса модуль записывает историю, вызывает события и при NOTIFY = Y передает изменение штатным уведомлениям, как описано в разделе Изменить статус с учетом прав.
Получить статус заказа
Загрузите заказ методом Bitrix\Sale\Order::load(). Текущий статус заказа находится в поле заказа STATUS_ID. Название статуса можно получить из списка OrderStatus::getAllStatusesNames().
if (!\Bitrix\Main\Loader::includeModule('sale'))
{
throw new \RuntimeException('Модуль sale не установлен');
}
$order = \Bitrix\Sale\Order::load($orderId);
if (!$order)
{
throw new \RuntimeException('Заказ не найден');
}
$orderStatusId = (string) $order->getField('STATUS_ID');
$orderStatusNames = \Bitrix\Sale\OrderStatus::getAllStatusesNames(LANGUAGE_ID);
$orderStatusName = $orderStatusNames[$orderStatusId] ?? $orderStatusId;
Метод getAllStatusesNames() возвращает массив, в котором:
-
ключ — код статуса,
-
значение — название для указанного языка.
Константа LANGUAGE_ID содержит код текущего языка. В фоновом сценарии без языкового контекста передайте код языка строкой, например ru.
Метод кеширует названия по типу статуса и не учитывает язык. Повторный вызов с другим кодом языка в том же запросе вернет названия первого языка. Для нескольких языков используйте запрос к StatusTable::getList() из раздела Получить локализованные статусы.
Получить статус отгрузки
Переберите коллекцию отгрузок заказа и прочитайте поле STATUS_ID у каждой отгрузки. Пропустите системную отгрузку. Она служит для внутреннего учета товаров, которые еще не распределены по пользовательским отгрузкам.
$deliveryStatusNames = \Bitrix\Sale\DeliveryStatus::getAllStatusesNames(LANGUAGE_ID);
$shipmentStatuses = [];
foreach ($order->getShipmentCollection() as $shipment)
{
if ($shipment->isSystem())
{
continue;
}
$statusId = (string) $shipment->getField('STATUS_ID');
$shipmentStatuses[] = [
'SHIPMENT_ID' => $shipment->getId(),
'STATUS_ID' => $statusId,
'STATUS_NAME' => $deliveryStatusNames[$statusId] ?? $statusId,
];
}
Массив $shipmentStatuses содержит отдельную запись для каждой отгрузки.
Изменить статус с учетом прав
Права на статусы задают разрешенные исходные и целевые статусы для групп пользователей. Метод OrderStatus::getAllowedUserStatuses() возвращает доступные целевые статусы для пользователя и текущего статуса заказа. Метод учитывает группы пользователя и возвращает массив, где ключ — код статуса, значение — его название на текущем языке.
Для пользователя из группы администраторов или с уровнем доступа W и выше в модуле sale метод возвращает все статусы соответствующего типа, у которых есть название на текущем языке.
Язык метод берет из константы LANGUAGE_ID и не принимает код языка аргументом. В скрипте без этой константы задайте ее до вызова.
Проверяйте переход до вызова setField(). Переменная $userId содержит идентификатор пользователя, от имени которого выполняется операция.
Метод getAllowedUserStatuses() проверяет только разрешенный переход между статусами. До загрузки и изменения заказа проверьте, что пользователь может просматривать или изменять этот заказ в текущем сценарии проекта.
Пример. Код проверяет, что пользователь $userId может перевести заказ в статус F, меняет статус и сохраняет заказ.
// $order — ранее загруженный заказ
// $userId — идентификатор пользователя, который выполняет переход
$currentStatusId = (string) $order->getField('STATUS_ID');
// Код статуса, в который нужно перевести заказ
$targetStatusId = 'F';
$allowedStatuses = \Bitrix\Sale\OrderStatus::getAllowedUserStatuses(
$userId,
$currentStatusId
);
if (!array_key_exists($targetStatusId, $allowedStatuses))
{
throw new \RuntimeException('Переход в выбранный статус недоступен');
}
$setResult = $order->setField('STATUS_ID', $targetStatusId);
if (!$setResult->isSuccess())
{
throw new \RuntimeException(implode('; ', $setResult->getErrorMessages()));
}
$saveResult = $order->save();
if (!$saveResult->isSuccess())
{
throw new \RuntimeException(implode('; ', $saveResult->getErrorMessages()));
}
Вызов setField('STATUS_ID', ...) сразу заполняет поле DATE_STATUS. Если текущий пользователь $USER авторизован, в EMP_STATUS_ID попадает его идентификатор, а не $userId из примера. После сохранения модуль записывает изменение в историю и вызывает событие OnSaleStatusOrderChange. При NOTIFY = Y модуль передает изменение подсистеме штатных уведомлений.
Для отгрузки используйте тот же порядок:
-
Получите разрешенные переходы через
DeliveryStatus. -
Измените поле отгрузки.
-
Сохраните заказ.
Пример. Код переводит отгрузку $shipmentId в статус DF, если пользователю $userId разрешен этот переход, и сохраняет заказ.
$shipment = $order->getShipmentCollection()->getItemById($shipmentId);
if (!$shipment || $shipment->isSystem())
{
throw new \RuntimeException('Отгрузка не найдена');
}
$currentStatusId = (string) $shipment->getField('STATUS_ID');
$targetStatusId = 'DF';
$allowedStatuses = \Bitrix\Sale\DeliveryStatus::getAllowedUserStatuses(
$userId,
$currentStatusId
);
if (!array_key_exists($targetStatusId, $allowedStatuses))
{
throw new \RuntimeException('Переход отгрузки в выбранный статус недоступен');
}
$setResult = $shipment->setField('STATUS_ID', $targetStatusId);
if (!$setResult->isSuccess())
{
throw new \RuntimeException(implode('; ', $setResult->getErrorMessages()));
}
$saveResult = $order->save();
if (!$saveResult->isSuccess())
{
throw new \RuntimeException(implode('; ', $saveResult->getErrorMessages()));
}
Право перехода проверяйте методом getAllowedUserStatuses(). Запись в StatusTable подтверждает только, что статус существует, но не право перейти в него.
Если обработчик события меняет STATUS_ID, проверьте переход тем же методом для пользователя, от имени которого выполняется операция.
Получить историю изменений заказа
Модуль записывает изменения заказа, оплат и отгрузок в журнал при сохранении заказа. Текстовое описание записи формирует только классический API, поэтому в примерах ниже используются методы CSaleOrderChange.
-
GetList($arOrder, $arFilter, $arGroupBy, $arNavStartParams, $arSelectFields)— возвращает записи журнала изменений заказа.-
$arOrder— сортировка, -
$arFilter— условия выборки, -
$arGroupBy— поля группировки илиfalseдля выборки без группировки, -
$arNavStartParams— параметры постраничной выборки илиfalseдля получения всех подходящих записей, -
$arSelectFields— поля результата.
-
-
GetRecordDescription($type, $data)— возвращает массив с ключамиNAME— название иINFO— описание записи. Для неизвестного типа метод возвращаетfalse.-
$type— тип изменения, -
$data— сохраненные для него сериализованные данные.
-
Пример. Код получает журнал изменений заказа $orderId от новых записей к старым и добавляет к каждой записи название и описание из GetRecordDescription().
$historyResult = \CSaleOrderChange::GetList(
['DATE_CREATE' => 'DESC'],
['ORDER_ID' => $orderId],
false,
false,
['ID', 'DATE_CREATE', 'TYPE', 'DATA', 'USER_ID', 'ENTITY', 'ENTITY_ID']
);
$history = [];
while ($record = $historyResult->Fetch())
{
$description = \CSaleOrderChange::GetRecordDescription(
$record['TYPE'],
$record['DATA']
);
$history[] = [
'ID' => (int) $record['ID'],
'DATE_CREATE' => $record['DATE_CREATE'],
'TYPE' => $record['TYPE'],
'NAME' => $description['NAME'] ?? $record['TYPE'],
'INFO' => $description['INFO'] ?? '',
'USER_ID' => (int) $record['USER_ID'],
];
}
Чтобы получить только смены статусов заказа и отгрузок, добавьте фильтр по типу записи.
$statusHistoryResult = \CSaleOrderChange::GetList(
['DATE_CREATE' => 'DESC'],
[
'ORDER_ID' => $orderId,
'@TYPE' => [
'ORDER_STATUS_CHANGED',
'SHIPMENT_STATUS_CHANGED',
],
],
false,
false,
['ID', 'DATE_CREATE', 'TYPE', 'DATA', 'USER_ID', 'ENTITY', 'ENTITY_ID']
);
Обработайте $statusHistoryResult тем же циклом, что и общий журнал. Поля записи о связанном объекте:
-
ENTITY— тип связанного объекта, -
ENTITY_ID— идентификатор этого объекта.
История предназначена для аудита изменений, а не для восстановления текущего состояния объекта. Текущее состояние всегда получайте из загруженного заказа, оплаты или отгрузки.
Подписаться на события заказа и связанных объектов
Чтобы зарегистрировать обработчик, используйте класс Bitrix\Main\EventManager. Размещайте постоянную регистрацию в /local/php_interface/init.php или в подключаемом из него файле, чтобы код выполнялся на каждом запросе. Общий механизм регистрации описан в статье События.
Основные события передают измененный объект в параметре ENTITY.
Выбирайте событие по моменту жизненного цикла:
-
события изменения поля срабатывают до сохранения,
-
события финального расчета вызывает
Order::doFinalAction(true), с аргументомfalseони не срабатывают, -
события
*EntitySavedвызываются при сохранении отдельного объекта, -
OnSaleOrderSavedвызывается при завершении сохранения заказа и связанных коллекций.
|
Событие |
Когда вызывается |
Основные параметры |
|
|
При вызове |
|
|
|
При изменении поля заказа, непосредственно перед записью нового значения в объект |
|
|
|
При вызове |
|
|
|
При завершении финального расчета заказа |
|
|
|
При подготовке заказа к сохранению. Обработчик может вернуть ошибку и остановить сохранение |
|
|
|
При сохранении записи заказа, до сохранения связанных коллекций. Вызывается, только если у заказа есть измененные поля |
|
|
|
При завершении сохранения заказа |
|
|
|
При успешном сохранении нового статуса заказа |
|
|
|
При сохранении заказа с измененным признаком полной оплаты |
|
|
|
При сохранении заказа с измененным признаком отмены |
|
|
|
При успешном сохранении нового статуса отгрузки |
|
|
|
При изменении флага разрешения доставки отгрузки через |
|
|
|
При сохранении отгрузки с измененным признаком списания |
|
|
|
При завершении сохранения оплаты |
|
|
|
При сохранении записи отгрузки, до сохранения ее позиций. Вызывается, только если в отгрузке есть изменения |
|
|
|
При сохранении отгрузки с новым непустым идентификатором отправления |
|
|
|
При сохранении отгрузки с измененным флагом разрешения доставки |
|
Для вложенных объектов заказа существуют аналогичные пары событий OnBeforeSale...SetField и OnSale...SetField. Правила для таких пар:
-
обработчик события с префиксом
OnBeforeможет вернутьEventResult::SUCCESSс параметромVALUE, чтобы заменить значение, -
тот же обработчик может вернуть
EventResult::ERROR, чтобы отменить изменение, -
событие без префикса
OnBeforeдополнительно получаетOLD_VALUEи также может заменитьVALUE, но его результат с ошибкой не отменяет изменение.
Проверяйте результат setField(), setValue() и setQuantity(). Обработчик события до изменения может отклонить новое значение.
Имена пар для вложенных объектов:
-
позиция корзины —
OnBeforeSaleBasketItemSetFieldиOnSaleBasketItemSetField, -
отгрузка —
OnBeforeSaleShipmentSetFieldиOnSaleShipmentSetField, -
позиция отгрузки —
OnBeforeSaleShipmentItemSetFieldиOnSaleShipmentItemSetField, -
оплата —
OnBeforeSalePaymentSetFieldиOnSalePaymentSetField, -
значение свойства —
OnBeforeSalePropertyValueSetFieldиOnSalePropertyValueSetField.
После записи отдельных объектов вызываются OnSaleBasketItemEntitySaved, OnSaleShipmentEntitySaved, OnSaleShipmentItemEntitySaved, OnSalePaymentEntitySaved и OnSalePropertyValueEntitySaved. Используйте их, когда обработчику нужен результат сохранения конкретной позиции корзины, отгрузки, позиции отгрузки, оплаты или значения свойства.
При сохранении заказа события срабатывают в таком порядке.
-
Модуль вызывает
OnSaleOrderEntitySavedи сохраняет коллекции. -
Во время сохранения каждой отгрузки срабатывают
OnSaleShipmentEntitySavedи события смены состояния отгрузки, напримерOnSaleStatusShipmentChange,OnShipmentAllowDeliveryиOnShipmentTrackingNumberChange. -
Модуль вызывает
OnSaleOrderSaved. -
После него модуль отправляет события смены состояния заказа —
OnSaleStatusOrderChange,OnSaleOrderPaidиOnSaleOrderCanceled.
Результаты обработчиков OnSaleOrderSaved не изменяют результат завершенного сохранения. События смены состояния также не подходят для отмены Order::save(). Чтобы запретить сохранение, используйте OnSaleOrderBeforeSaved или подходящее событие OnBefore...SetField.
Чтобы запретить смену статуса заказа, верните EventResult::ERROR из обработчика OnSaleBeforeStatusOrderChange. Ошибка из этого обработчика приходит в результат setField(). Для отгрузки результат OnSaleBeforeStatusShipmentChange не учитывается. Чтобы запретить смену статуса отгрузки, верните EventResult::ERROR из обработчика OnBeforeSaleShipmentSetField, если параметр NAME равен STATUS_ID.
Обработать создание заказа
Для действия после первого полного сохранения заказа используйте OnSaleOrderSaved и проверяйте параметр IS_NEW. Сохраняйте идентификатор обработанного заказа во внешней системе или хранилище проекта и проверяйте его перед действием, чтобы повторный запуск обработчика не создавал дубли.
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderSaved',
static function (\Bitrix\Main\Event $event): void
{
if (!$event->getParameter('IS_NEW'))
{
return;
}
/** @var \Bitrix\Sale\Order $order */
$order = $event->getParameter('ENTITY');
$orderId = $order->getId();
$orderPrice = $order->getPrice();
// Выполните идемпотентное действие для нового заказа
}
);
Обработать смену статуса заказа
В событиях смены статуса сравнивайте VALUE и OLD_VALUE. Оба кода приходят в параметрах события, поэтому повторно загружать заказ не нужно.
Пример. Обработчик записывает идентификатор заказа в журнал, только когда заказ переходит в статус F. Обработчик не меняет заказ и не запускает повторное сохранение. Каталог для журнала должен существовать, путь задается от корня сайта.
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleStatusOrderChange',
static function (\Bitrix\Main\Event $event): void
{
$newStatusId = (string) $event->getParameter('VALUE');
$oldStatusId = (string) $event->getParameter('OLD_VALUE');
if ($newStatusId !== 'F' || $oldStatusId === $newStatusId)
{
return;
}
/** @var \Bitrix\Sale\Order $order */
$order = $event->getParameter('ENTITY');
\Bitrix\Main\Diag\Debug::writeToFile(
['ORDER_ID' => $order->getId()],
'Order status changed',
'/local/var/log/order-status.log'
);
}
);
Обработать полную оплату заказа
Событие OnSaleOrderPaid вызывается при установке и при снятии признака полной оплаты. Проверяйте текущее состояние заказа, если действие нужно выполнить только после оплаты.
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderPaid',
static function (\Bitrix\Main\Event $event): void
{
/** @var \Bitrix\Sale\Order $order */
$order = $event->getParameter('ENTITY');
if (!$order->isPaid())
{
return;
}
\Bitrix\Main\Diag\Debug::writeToFile(
['ORDER_ID' => $order->getId()],
'Order paid',
'/local/var/log/order-events.log'
);
}
);
Обработчик получает заказ после изменения его признака полной оплаты. Для реакции на одну конкретную оплату используйте OnSalePaymentEntitySaved и получите объект Bitrix\Sale\Payment из параметра ENTITY.
Обработать смену статуса отгрузки
Событие OnSaleStatusShipmentChange передает новый и прежний коды статуса.
Пример. Обработчик пропускает событие, если код статуса не изменился, и записывает в журнал идентификатор отгрузки, прежний и новый статусы.
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleStatusShipmentChange',
static function (\Bitrix\Main\Event $event): void
{
$newStatusId = (string) $event->getParameter('VALUE');
$oldStatusId = (string) $event->getParameter('OLD_VALUE');
if ($newStatusId === $oldStatusId)
{
return;
}
/** @var \Bitrix\Sale\Shipment $shipment */
$shipment = $event->getParameter('ENTITY');
\Bitrix\Main\Diag\Debug::writeToFile(
[
'SHIPMENT_ID' => $shipment->getId(),
'OLD_STATUS_ID' => $oldStatusId,
'STATUS_ID' => $newStatusId,
],
'Shipment status changed',
'/local/var/log/order-events.log'
);
}
);
Как не допустить рекурсию в обработчике
Не вызывайте $order->save() из OnSaleOrderSaved. В этот момент исходный save() еще выполняется, поэтому повторный вызов создает рекурсивное сохранение. Признак рекурсии — предупреждение E_USER_WARNING с текстом «Order saving in recursion».
Если значение должно попасть в заказ при текущем сохранении, измените объект в OnSaleOrderBeforeSaved и не вызывайте save() повторно.
Пример. Обработчик записывает в поле ADDITIONAL_INFO значение export_required, если его там еще нет, и возвращает ошибку, если setField() не принял значение. Проверка текущего значения предотвращает повторное изменение поля.
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderBeforeSaved',
static function (\Bitrix\Main\Event $event): \Bitrix\Main\EventResult
{
/** @var \Bitrix\Sale\Order $order */
$order = $event->getParameter('ENTITY');
if ((string) $order->getField('ADDITIONAL_INFO') === 'export_required')
{
return new \Bitrix\Main\EventResult(
\Bitrix\Main\EventResult::SUCCESS
);
}
$setResult = $order->setField('ADDITIONAL_INFO', 'export_required');
if (!$setResult->isSuccess())
{
return new \Bitrix\Main\EventResult(
\Bitrix\Main\EventResult::ERROR,
new \Bitrix\Sale\ResultError(
implode('; ', $setResult->getErrorMessages())
)
);
}
return new \Bitrix\Main\EventResult(
\Bitrix\Main\EventResult::SUCCESS
);
}
);
Если действие нужно выполнить только после сохранения, в обработчике OnSaleOrderSaved поставьте отдельную задачу в очередь, например агент модуля main, и завершите обработчик, не меняя заказ. В отложенном процессе заново загрузите заказ и сохраните его. Храните уникальный идентификатор операции в очереди или другом общем хранилище проекта, чтобы повторная постановка задачи не создала дубликат.
Как статусы связаны с уведомлениями
Поле NOTIFY в StatusTable указывает, участвует ли статус в отправке штатного уведомления о смене статуса:
-
Y— включает уведомление, -
N— отключает уведомление.
Для отдельного канала или дополнительного действия используйте обработчик OnSaleStatusOrderChange, например для сообщения во внутреннюю систему, постановки задачи или интеграции с внешним сервисом. Штатное письмо из этого обработчика повторно не отправляйте. Шаблоны, получатели и правила отправки настраивают в уведомлениях. Подробнее — в статье Уведомления по заказам.