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.







