Перейти к содержанию

Статусы показывают, на каком этапе находятся заказ и его отгрузки. События модуля sale позволяют выполнить дополнительное действие после сохранения, оплаты, отмены или изменения статуса.

Совет

Заказ, оплаты и отгрузки меняют через связанные PHP-объекты. Подробнее в статье Схема работы интернет-магазина и основные объекты.

Чем статус заказа отличается от статуса отгрузки

Заказ и отгрузка проходят разные жизненные циклы. Поэтому модуль хранит для них разные типы статусов.

|

|| Состояние | Где хранится | Класс списка статусов | Начальный и финальный статусы || || Заказ | Поле STATUS_ID объекта Bitrix\Sale\Order | Bitrix\Sale\OrderStatus | N и F || || Отгрузка | Поле STATUS_ID объекта Bitrix\Sale\Shipment | Bitrix\Sale\DeliveryStatus | DN и DF || |#

У одного заказа может быть несколько отгрузок, и у каждой отгрузки свой статус.

  • Статус отгрузки не означает, что заказ оплачен.

  • Статус заказа не заменяет признаки оплаты, отмены, разрешения доставки и списания товаров.

Эти состояния меняются через соответствующие объекты и методы. Подробнее об отгрузках читайте в статье Доставка и отгрузки, об оплатах — в статье Оплаты и платежные системы.

Основные объекты

Для операций с заказом или отгрузкой используйте соответствующий объект и класс статусов: OrderStatus для заказа или DeliveryStatus для отгрузки. Поля статуса, его локализованные название и описание можно получить через ORM-классы StatusTable и StatusLangTable.

Чтобы создать или изменить статус, используйте сценарии из статьи Базовые настройки интернет-магазина.

|

|| API | Роль || || Bitrix\Sale\OrderStatus | Возвращает:

  • статусы заказа
  • начальный и финальный коды
  • доступные пользователю переходы и статусы, с которых разрешена оплата || || Bitrix\Sale\DeliveryStatus | Возвращает:

  • статусы отгрузки

  • начальный и финальный коды
  • доступные пользователю переходы || || Bitrix\Sale\Internals\StatusTable | Хранит общие поля статуса: код, тип, сортировку, признак уведомления, цвет и внешний идентификатор || || Bitrix\Sale\Internals\StatusLangTable | Хранит название и описание статуса для конкретного языка || || Bitrix\Sale\OrderHistory | Собирает изменения заказа и связанных объектов во время сохранения || || Bitrix\Main\EventManager | Регистрирует обработчики событий модуля sale || |#

Поля статуса

Класс StatusTable хранит данные статусов заказа и отгрузки. Поле TYPE разделяет записи:

  • O — заказ,

  • D — отгрузка.

Используйте константы StatusTable::TYPE_ORDER и StatusTable::TYPE_SHIPMENT, чтобы не передавать эти значения строками.

|

|| Поле | Назначение || || ID | Код статуса из одной или двух латинских букв || || TYPE | Тип статуса: заказа или отгрузки. По умолчанию — статус заказа || || SORT | Порядок статуса в списке. Значение по умолчанию — 100 || || NOTIFY | Признак штатного уведомления о смене статуса: Y или N. Значение по умолчанию — Y || || COLOR | Цвет статуса в интерфейсе || || XML_ID | Внешний идентификатор статуса || |#

Получить локализованные статусы

Составной ключ класса 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 возвращает только записи выбранного языка.

Базовый сценарий

Работа со статусом и событием состоит из последовательных действий:

  1. Загрузите заказ.

  2. Получите текущий STATUS_ID заказа или отгрузки.

  3. Запросите разрешенные целевые статусы для пользователя.

  4. Проверьте, что нужный статус входит в результат.

  5. Измените STATUS_ID через объект заказа или отгрузки.

  6. Сохраните заказ методом Order::save() и проверьте ошибки.

  7. Выполните дополнительное действие в подходящем событии и убедитесь, что повторный вызов обработчика не дублирует результат.

После успешного сохранения новый статус появляется в объекте и истории изменений. Модуль вызывает событие смены статуса и при 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 метод возвращает статусы соответствующего типа, у которых есть название на текущем языке.

Для отгрузки используйте тот же порядок:

  1. Получите разрешенные переходы через DeliveryStatus.

  2. Измените поле отгрузки.

  3. Сохраните заказ.

$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 — о завершении сохранения заказа и связанных коллекций.

|

|| Событие | Когда вызывается | Основные параметры || || OnBeforeSaleOrderSetField | Перед изменением поля заказа. Обработчик может заменить значение или вернуть ошибку и отменить изменение | - ENTITY — изменяемый заказ Bitrix\Sale\Order - NAME — имя поля - VALUE — новое значение || || OnSaleOrderSetField | Непосредственно перед записью нового значения в объект заказа | - ENTITY — изменяемый заказ Bitrix\Sale\Order - NAME — имя поля - VALUE — новое значение - OLD_VALUE — прежнее значение || || OnBeforeSaleOrderFinalAction | Перед расчетом скидок и налогов в Order::doFinalAction() | - ENTITY — рассчитываемый заказ Bitrix\Sale\Order - HAS_MEANINGFUL_FIELD — признак значимых изменений - BASKET — корзина заказа || || OnAfterSaleOrderFinalAction | После завершения финального расчета заказа | - ENTITY — рассчитанный заказ Bitrix\Sale\Order || || OnSaleOrderBeforeSaved | При подготовке заказа к сохранению. Обработчик может вернуть ошибку и остановить сохранение | - ENTITY — заказ Bitrix\Sale\Order, который готовится к сохранению - VALUES — исходные значения полей заказа до текущего сохранения || || OnSaleOrderEntitySaved | После сохранения строки самого заказа. Связанные коллекции в этот момент еще могут сохраняться | - ENTITY — сохраненный объект заказа Bitrix\Sale\Order - VALUES — исходные значения полей заказа || || OnSaleOrderSaved | При завершении сохранения заказа | - ENTITY — сохраненный заказ Bitrix\Sale\Order - IS_NEW — признак создания нового заказа - IS_CHANGED — признак изменения полей заказа - VALUES — исходные значения полей заказа до сохранения || || OnSaleStatusOrderChange | При успешном сохранении нового статуса заказа | - ENTITY — заказ Bitrix\Sale\Order с новым статусом - VALUE — код нового статуса - OLD_VALUE — код предыдущего статуса || || OnSaleOrderPaid | При изменении признака полной оплаты заказа | - ENTITY — заказ Bitrix\Sale\Order с новым состоянием оплаты || || OnSaleOrderCanceled | При изменении признака отмены заказа | - ENTITY — заказ Bitrix\Sale\Order с новым состоянием отмены || || OnSaleStatusShipmentChange | При успешном сохранении нового статуса отгрузки | - ENTITY — отгрузка Bitrix\Sale\Shipment с новым статусом - VALUE — код нового статуса - OLD_VALUE — код предыдущего статуса || || OnSaleShipmentDelivery | При успешной обработке разрешения доставки, до завершения Order::save() | - ENTITY — отгрузка Bitrix\Sale\Shipment, для которой обработано разрешение доставки || || OnShipmentDeducted | При изменении признака списания отгрузки | - ENTITY — отгрузка Bitrix\Sale\Shipment с новым состоянием списания - VALUES — исходные значения полей отгрузки до изменения || || OnSalePaymentEntitySaved | При завершении сохранения оплаты | - ENTITY — сохраненная оплата Bitrix\Sale\Payment - VALUES — исходные значения полей оплаты до сохранения || || OnSaleShipmentEntitySaved | При завершении сохранения отгрузки | - ENTITY — сохраненная отгрузка Bitrix\Sale\Shipment - VALUES — исходные значения полей отгрузки до сохранения - IS_NEW — признак создания новой отгрузки || || OnShipmentTrackingNumberChange | После сохранения нового непустого идентификатора отправления | - ENTITY — измененная отгрузка Bitrix\Sale\Shipment - VALUES — исходные значения полей отгрузки || || OnShipmentAllowDelivery | После сохранения изменения флага разрешения доставки | - ENTITY — измененная отгрузка Bitrix\Sale\Shipment - VALUES — исходные значения полей отгрузки || |#

Для позиций корзины, оплат, отгрузок, позиций отгрузок и значений свойств существуют аналогичные пары событий 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. Пользовательский обработчик нужен для отдельного канала или дополнительного действия: сообщения во внутреннюю систему, постановки задачи или интеграции с внешним сервисом. Шаблоны, получатели и правила отправки относятся к настройке уведомлений, а не к логике переходов между статусами.