Developing custom logic for changing 1C-Bitrix order statuses

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    745
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Custom Order Status Transition Logic for 1C-Bitrix

The standard Bitrix status mechanism allows a manager to manually move an order to any status given the appropriate access rights. However, real-world order processing is more complex: transitioning to "In Delivery" should only be possible from "Assembly", moving to "Cancelled" only when the order is unpaid, and reverting from "Completed" should be restricted to administrators. All of this requires custom transition logic that cannot be implemented through standard settings.

Transition Validation: Event-Driven Model

Bitrix provides the OnSaleOrderBeforeStatusChange event — a handler can block the transition and return an error:

// /local/php_interface/init.php
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'sale',
    'OnSaleOrderBeforeStatusChange',
    ['\App\Order\StatusValidator', 'validate']
);
// /local/lib/Order/StatusValidator.php
namespace App\Order;

use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use Bitrix\Sale\Order;

class StatusValidator
{
    // Allowed transition matrix
    private static array $allowedTransitions = [
        'N' => ['P', 'A'],           // New → Accepted or Cancelled
        'P' => ['W', 'ASSEMBLY', 'A'], // Accepted → Awaiting Payment, Assembly, Cancelled
        'W' => ['P', 'ASSEMBLY', 'A'], // Awaiting Payment → Accepted, Assembly, Cancelled
        'ASSEMBLY' => ['D', 'A'],     // Assembly → In Delivery, Cancelled
        'D' => ['F'],                 // In Delivery → Completed
        'F' => [],                    // Completed — final
        'A' => [],                    // Cancelled — final
    ];

    public static function validate(Event $event): EventResult
    {
        /** @var Order $order */
        $order = $event->getParameter('ENTITY');
        $newStatus = $event->getParameter('VALUE');
        $currentStatus = $order->getField('STATUS_ID');

        $allowed = self::$allowedTransitions[$currentStatus] ?? [];

        if (!in_array($newStatus, $allowed, true)) {
            return new EventResult(
                EventResult::ERROR,
                [
                    'message' => sprintf(
                        'Transition from status "%s" to "%s" is not allowed',
                        $currentStatus,
                        $newStatus
                    ),
                ],
                'sale'
            );
        }

        // Additional business rule: cannot cancel a paid order
        if ($newStatus === 'A' && $order->isPaid()) {
            return new EventResult(
                EventResult::ERROR,
                ['message' => 'Cannot cancel a paid order. Please initiate a refund.'],
                'sale'
            );
        }

        return new EventResult(EventResult::SUCCESS);
    }
}

Role-Based Additional Validation

Administrators can perform actions unavailable to regular managers:

public static function validate(Event $event): EventResult
{
    global $USER;

    $order = $event->getParameter('ENTITY');
    $newStatus = $event->getParameter('VALUE');
    $currentStatus = $order->getField('STATUS_ID');

    // Reverting from a final status — administrators only
    if ($currentStatus === 'F' && !$USER->IsAdmin()) {
        return new EventResult(
            EventResult::ERROR,
            ['message' => 'Modifying a completed order is only available to administrators'],
            'sale'
        );
    }

    // ... additional checks
}

Reacting to Transitions: Post-Processing

After a successful transition, the OnSaleOrderStatusChange event fires:

\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'sale',
    'OnSaleOrderStatusChange',
    function(Event $event) {
        $order = $event->getParameter('ENTITY');
        $newStatus = $event->getParameter('VALUE');
        $oldStatus = $event->getParameter('OLD_VALUE');

        switch ($newStatus) {
            case 'ASSEMBLY':
                // Create a warehouse picking task
                WarehouseIntegration::createPickingTask($order);
                break;

            case 'D':
                // Hand off to the delivery service
                DeliveryService::registerShipment($order);
                // Send the tracking number to the customer
                Notifications::sendTrackingNumber($order);
                break;

            case 'F':
                // Credit loyalty points
                LoyaltyProgram::creditPoints($order);
                // Request a review in 3 days
                ReviewRequest::schedule($order->getUserId(), 3);
                break;

            case 'A':
                // Release warehouse reservation
                if ($oldStatus !== 'N') {
                    StockManager::releaseReservation($order);
                }
                // Initiate refund if payment was made
                if ($order->isPaid()) {
                    RefundManager::initiate($order);
                }
                break;
        }
    }
);

Programmatic Status Change

When changing status from code — not directly via setField, but through the proper method:

$order = \Bitrix\Sale\Order::load($orderId);

// Correct approach — triggers all events
$result = $order->setField('STATUS_ID', 'D');

if ($result->isSuccess()) {
    $saveResult = $order->save();
    if (!$saveResult->isSuccess()) {
        // Handle save errors
        $errors = $saveResult->getErrorMessages();
    }
} else {
    // OnSaleOrderBeforeStatusChange returned an error
    $errors = $result->getErrorMessages();
}

Transition Log

All status changes are automatically logged to b_sale_order_change. For custom auditing — maintain a dedicated table with transition history and reasons:

// On each status change
\App\Order\StatusLog::record([
    'order_id'   => $order->getId(),
    'from_status' => $oldStatus,
    'to_status'   => $newStatus,
    'user_id'     => $GLOBALS['USER']->GetID(),
    'comment'     => $comment,
    'timestamp'   => new \Bitrix\Main\Type\DateTime(),
]);

Status Change Interface with Comment

The standard interface does not provide a comment field when changing status. This is addressed with a custom AJAX handler on the order detail page in the admin panel:

// /local/ajax/change_order_status.php
$orderId = (int)$_POST['order_id'];
$newStatus = htmlspecialchars($_POST['status']);
$comment = htmlspecialchars($_POST['comment']);

$order = \Bitrix\Sale\Order::load($orderId);
// Store comment in session/global for the event handler
$_SESSION['order_status_comment'][$orderId] = $comment;

$order->setField('STATUS_ID', $newStatus);
$result = $order->save();

Timeline

Transition matrix with validation plus reaction handlers for 3–5 statuses — 1–2 business days. A full-featured system with an audit log, comment interface, warehouse and delivery service integration — 3–5 business days.