Статусы и события
Статусы показывают, на каком этапе находятся заказ и его отгрузки. События модуля sale позволяют выполнить дополнительное действие после сохранения, оплаты, отмены или изменения статуса.
Заказ, оплаты и отгрузки меняют через связанные PHP-объекты. Подробнее в статье Схема работы интернет-магазина и основные объекты.
Чем статус заказа отличается от статуса отгрузки
Заказ и отгрузка проходят разные жизненные циклы. Поэтому модуль хранит для них разные типы статусов.
|
Состояние |
Где хранится |
Класс списка статусов |
Начальный и финальный статусы |
|
Заказ |
Поле |
|
|
|
Отгрузка |
Поле |
|
|
У одного заказа может быть несколько отгрузок, и у каждой отгрузки свой статус.
-
Статус отгрузки не означает, что заказ оплачен.
-
Статус заказа не заменяет признаки оплаты, отмены, разрешения доставки и списания товаров.
Эти состояния меняются через соответствующие объекты и методы. Подробнее об отгрузках читайте в статье Доставка и отгрузки, об оплатах — в статье Оплаты и платежные системы.
Основные объекты
Для операций с заказом или отгрузкой используйте соответствующий объект и класс статусов: OrderStatus для заказа или DeliveryStatus для отгрузки. Поля статуса, его локализованные название и описание можно получить через ORM-классы StatusTable и StatusLangTable.
Чтобы создать или изменить статус, используйте сценарии из статьи Базовые настройки интернет-магазина.
|
API |
Роль |
|
|
Возвращает:
|
|
|
Возвращает:
|
|
|
Хранит общие поля статуса: код, тип, сортировку, признак уведомления, цвет и внешний идентификатор |
|
|
Хранит название и описание статуса для конкретного языка |
|
|
Собирает изменения заказа и связанных объектов во время сохранения |
|
|
Регистрирует обработчики событий модуля |
Поля статуса
Класс StatusTable хранит данные статусов заказа и отгрузки. Поле TYPE разделяет записи:
-
O— заказ, -
D— отгрузка.
Используйте константы StatusTable::TYPE_ORDER и StatusTable::TYPE_SHIPMENT, чтобы не передавать эти значения строками.
|
Поле |
Назначение |
|
|
Код статуса из одной или двух латинских букв |
|
|
Тип статуса: заказа или отгрузки. По умолчанию — статус заказа |
|
|
Порядок статуса в списке. Значение по умолчанию — |
|
|
Признак штатного уведомления о смене статуса: |
|
|
Цвет статуса в интерфейсе |
|
|
Внешний идентификатор статуса |
Получить локализованные статусы
Составной ключ класса StatusLangTable образуют код статуса STATUS_ID и двухбуквенный код языка LID. Поле NAME обязательно, поле DESCRIPTION содержит необязательное описание.
В StatusTable определена ORM-связь STATUS_LANG со StatusLangTable по коду статуса: StatusTable.ID соответствует StatusLangTable.STATUS_ID. Поэтому локализованные поля можно получить через StatusTable::getList() по путям STATUS_LANG.NAME, STATUS_LANG.DESCRIPTION и STATUS_LANG.LID. ORM автоматически присоединит языковые записи к данным статусов.
$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.
Получить статус отгрузки
Статусы отгрузок получают из коллекции отгрузок заказа. Системную отгрузку нужно пропустить: она служит для внутреннего учета товаров, которые еще не распределены по обычным отгрузкам.
$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() возвращает доступные целевые статусы для пользователя и текущего статуса заказа.
Проверяйте переход до вызова setField(). Переменная $userId содержит идентификатор пользователя, от имени которого выполняется операция. Метод учитывает группы этого пользователя и возвращает разрешенные целевые статусы.
Метод getAllowedUserStatuses() проверяет только разрешенный переход между статусами. До загрузки и изменения заказа проверьте, что пользователь может просматривать или изменять этот заказ в текущем сценарии проекта.
// $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()));
}
После успешного сохранения модуль обновляет дату смены статуса, записывает изменение в историю и вызывает события смены статуса. Для пользователя из группы администраторов или с правом на запись W в модуле sale метод возвращает статусы соответствующего типа, у которых есть название на текущем языке.
Для отгрузки используйте тот же порядок:
-
Получите разрешенные переходы через
DeliveryStatus. -
Измените поле отгрузки.
-
Сохраните заказ.
$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()));
}
Не проверяйте доступ к статусу только по наличию записи в StatusTable. Наличие статуса и право пользователя перейти в него — разные условия.
Получить историю изменений заказа
Класс Bitrix\Sale\OrderHistory собирает изменения полей и действия над заказом, оплатами и отгрузками. Во время сохранения система записывает изменения в журнал заказа.
Для чтения журнала используйте методы класса классического API — CSaleOrderChange.
-
GetList($arOrder, $arFilter, $arGroupBy, $arNavStartParams, $arSelectFields)— возвращает записи из текущей таблицы изменений заказа.-
$arOrderзадает сортировку, -
$arFilter— условия выборки, -
$arGroupBy— поля группировки илиfalseдля выборки без группировки, -
$arNavStartParams— параметры постраничной выборки илиfalseдля получения всех подходящих записей, -
$arSelectFields— поля результата.
-
-
GetRecordDescription($type, $data)— преобразует запись в название и текстовое описание.-
$typeсодержит тип изменения, -
$data— сохраненные для него сериализованные данные.
-
$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', 'ENTITY', 'ENTITY_ID']
);
Обработайте $statusHistoryResult тем же циклом, что и общий журнал. Поле ENTITY показывает тип связанного объекта, а ENTITY_ID — его идентификатор.
История предназначена для аудита изменений, а не для восстановления текущего состояния объекта. Текущее состояние всегда получайте из загруженного заказа, оплаты или отгрузки.
Подписаться на события заказа и связанных объектов
Чтобы зарегистрировать обработчик, используйте класс Bitrix\Main\EventManager. Размещайте постоянную регистрацию в /local/php_interface/init.php или в подключаемом из него файле, чтобы код выполнялся на каждом запросе. Общий механизм регистрации описан в статье События.
Основные события передают измененный объект в параметре ENTITY.
Для оплаты или отгрузки выбирайте событие по бизнес-смыслу. Например, OnSaleOrderPaid сообщает об изменении полной оплаты заказа, а OnSalePaymentEntitySaved — о сохранении конкретной оплаты.
Выбирайте событие по моменту жизненного цикла. События изменения поля срабатывают до сохранения. События финального расчета относятся к Order::doFinalAction(). События *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. Используйте их, когда обработчику нужен результат сохранения конкретной позиции корзины, отгрузки, позиции отгрузки, оплаты или значения свойства.
При сохранении заказа сначала срабатывают события отдельных записей и коллекций, затем OnSaleOrderSaved. После него модуль отправляет отложенные события состояния: смены статуса, полной оплаты, отмены и изменений отгрузки.
Результаты обработчиков OnSaleOrderSaved не изменяют результат завершенного сохранения. Отложенные события состояния также не подходят для отмены Order::save(). Чтобы запретить сохранение, используйте OnSaleOrderBeforeSaved или подходящее событие OnBefore...SetField.
Обработать создание заказа
Для действия после первого полного сохранения заказа используйте 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() еще выполняется, поэтому повторный вызов создает рекурсивное сохранение.
Если значение должно попасть в заказ при текущем сохранении, измените объект в OnSaleOrderBeforeSaved и не вызывайте save() повторно. Проверка текущего значения предотвращает повторное изменение поля.
\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 должен поставить отдельную задачу в очередь и завершиться без изменения заказа. Отложенный процесс заново загружает заказ и выполняет собственное сохранение. Храните уникальный идентификатор операции в очереди или другом общем хранилище проекта, чтобы повторная постановка задачи не создала дубликат.
Обработчик не должен обходить права на смену статуса. Если он меняет STATUS_ID, сначала вызовите OrderStatus::getAllowedUserStatuses() или DeliveryStatus::getAllowedUserStatuses() для пользователя, от имени которого выполняется операция.
Как статусы связаны с уведомлениями
Поле NOTIFY в StatusTable указывает, участвует ли статус в отправке штатного уведомления о смене статуса:
-
Y— включает уведомление, -
Nотключает.
После сохранения нового статуса модуль формирует событие смены статуса и передает его подсистеме уведомлений.
Не отправляйте то же штатное письмо повторно из события OnSaleStatusOrderChange. Пользовательский обработчик нужен для отдельного канала или дополнительного действия: сообщения во внутреннюю систему, постановки задачи или интеграции с внешним сервисом. Шаблоны, получатели и правила отправки относятся к настройке уведомлений, а не к логике переходов между статусами.