Skip to content

Order Status State Machine

This document describes the order status state machine implemented in orders/state_machine.py.

Overview

The state machine ensures orders follow valid status transitions based on their order type. This prevents invalid status changes and provides a clear workflow for each order type.

Usage

Basic Usage

from orders.state_machine import OrderStateMachine, can_transition, get_available_transitions

# Check if a transition is valid
if can_transition(order, OrderStatus.READY_TO_SHIP):
    order.status = OrderStatus.READY_TO_SHIP
    order.save()

# Get available next statuses for an order
available = get_available_transitions(order)
# Returns: [(OrderStatus.READY_TO_SHIP, 'Ready to Ship'), ...]

# Full transition with validation
from orders.state_machine import transition, InvalidTransitionError

try:
    transition(order, OrderStatus.SHIPPED)
except InvalidTransitionError as e:
    print(f"Cannot transition: {e}")

Locked States

Some states are "locked" and require explicit unlock before allowing changes:

# Check if order is in a locked state
if OrderStateMachine.is_locked_state(order):
    # Requires force=True to change
    transition(order, new_status, force=True)

# Get transitions including from locked state
available = get_available_transitions(order, include_locked=True)

Order Type Workflows

These can be seen in the state_machine.py code or the shared draw.io file Orders_StateMachine.drawio.

This file Uplink/docs/Orders_StateMachine.drawio.xml can also be downloaded and then uploaded to draw.io if you don't have share access to the file.

Extending the State Machine

Adding a New Locked State

  1. Add the status to LOCKED_STATES in state_machine.py:
LOCKED_STATES = {
    OrderType.FORECAST_ORDER: [OrderStatus.PO_CREATED],
    OrderType.SALES_ORDER: [OrderStatus.SHIPPED],
    OrderType.SUPPLIER_ORDER: [OrderStatus.COMPLETED],
    OrderType.STOCK_TRANSFER_ORDER: [OrderStatus.COMPLETED],
    OrderType.CONVERSION_ORDER: [OrderStatus.PROCESSED],
}
  1. Update the template to show unlock UI when in that state.

Adding Custom Transition Rules

  1. Modify the transitions dict for the order type:
SALES_ORDER_TRANSITIONS = {
    OrderStatus.DRAFT: [
        OrderStatus.WAITING_FOR_PAYMENT,
        OrderStatus.HELD,
        OrderStatus.READY_TO_SHIP,
        OrderStatus.CANCELLED,
        OrderStatus.NEW_STATUS,  # Add new transition
    ],
    # ...
}

Adding a New Order Type

  1. Create the transitions dict:
NEW_ORDER_TYPE_TRANSITIONS = {
    OrderStatus.DRAFT: [OrderStatus.READY_TO_PROCESS],
    OrderStatus.READY_TO_PROCESS: [OrderStatus.COMPLETED],
    # ...
}
  1. Add to TRANSITIONS_BY_TYPE:
TRANSITIONS_BY_TYPE = {
    # ...
    OrderType.NEW_TYPE: NEW_ORDER_TYPE_TRANSITIONS,
}
  1. Update get_initial_status() and get_final_statuses() methods.

Integration Points

Views

The UpdateOrderStatus view in orders/views.py uses the state machine: - Checks for locked states before allowing changes - Validates transitions (warns if non-standard) - Respects force_unlock parameter from frontend

The OrderDetail view uses state machine for forecast orders only: - Shows only valid next transitions in the dropdown (not all statuses) - Includes current status so it appears selected - Other order types still show all valid statuses for that type

Templates

The order_detail.html template: - Shows unlock switch for locked states (e.g., PO_CREATED) - Passes force_unlock value to the backend - Disables status select when locked

Configuration

Strict vs Permissive Mode

Currently, the state machine is permissive - it warns about non-standard transitions but allows them. To make it strict:

# In UpdateOrderStatus view, change:
if not OrderStateMachine.can_transition(order, new_status, force=force_unlock):
    messages.error(request, f"Invalid transition from '{from_label}' to '{to_label}'.")
    return HttpResponseRedirect(reverse("order", kwargs={"reference": order.reference}))

Audit Trail

Could add logging to track state changes:

def transition_with_audit(order, to_status, user, force=False):
    from_status = order.status
    order = transition(order, to_status, force=force)

    # Log the transition
    OrderStatusLog.objects.create(
        order=order,
        from_status=from_status,
        to_status=to_status,
        changed_by=user,
        forced=force
    )
    return order

This helps track the 'journey' an order has been through and identify where issues may have occurred.