Order customization

This page documents different order customization methods.

Custom statuses

Order statuses are defined using django’s django.db.models.TextChoices() enum class with the default set to salesman.orders.status.OrderStatus class.

You can change it by providing a dotted path in SALESMAN_ORDER_STATUS setting that points to a class extending salesman.orders.status.BaseOrderStatus class.

class OrderStatus(BaseOrderStatus):
    """
    Default order choices enum. Can be overriden by providing a
    dotted path to a class in ``SALESMAN_ORDER_STATUS`` setting.
    Required choices are NEW, CREATED, COMPLETED and REFUNDED.
    """

    NEW = "NEW", _("New")  # Order with reference created, items are in the basket.
    CREATED = "CREATED", _("Created")  # Created with items and pending payment.
    HOLD = "HOLD", _("Hold")  # Stock reduced but still awaiting payment.
    FAILED = "FAILED", _("Failed")  # Payment failed, retry is available.
    CANCELLED = "CANCELLED", _("Cancelled")  # Cancelled by seller, stock increased.
    PROCESSING = "PROCESSING", _("Processing")  # Payment confirmed, processing order.
    SHIPPED = "SHIPPED", _("Shipped")  # Shipped to customer.
    COMPLETED = "COMPLETED", _("Completed")  # Completed and received by customer.
    REFUNDED = "REFUNDED", _("Refunded")  # Fully refunded by seller.

    @classmethod
    def get_payable(cls) -> list[Any]:
        """
        Returns default payable statuses.
        """
        return [cls.CREATED, cls.HOLD, cls.FAILED]

    @classmethod
    def get_transitions(cls) -> dict[str, list[Any]]:
        """
        Returns default status transitions.
        """
        return {
            "NEW": [cls.CREATED],
            "CREATED": [cls.HOLD, cls.FAILED, cls.CANCELLED, cls.PROCESSING],
            "HOLD": [cls.FAILED, cls.CANCELLED, cls.PROCESSING],
            "FAILED": [cls.CANCELLED, cls.PROCESSING],
            "CANCELLED": [],
            "PROCESSING": [cls.SHIPPED, cls.COMPLETED, cls.REFUNDED],
            "SHIPPED": [cls.COMPLETED, cls.REFUNDED],
            "COMPLETED": [cls.REFUNDED],
            "REFUNDED": [],
        }

As seen in the default status class above, you must define statuses with names and values NEW, CREATED, COMPLETED and REFUNDED while others are customizable to your needs.

You can also optionally override salesman.orders.status.BaseOrderStatus.get_payable() method that returns a list of statuses from which an order is eligible for payment, and salesman.orders.status.BaseOrderStatus.get_transitions() method which limits status transitions for an order.

Order Status enum can be access using salesman.orders.models.BaseOrder.Status class property.

Validate status transitions

You might want to validate certain status transitions for your orders. This is possible by overriding the salesman.orders.status.BaseOrderStatus.validate_transition() method.

Tip

Eg. you could enforce a check that the order has been fully paid for before it can be marked as Completed, or even return and force a different status entirely.

A default status transition validation is provided with salesman.orders.status.BaseOrderStatus class which checks that transition is available for the current order status:

    @classmethod
    def validate_transition(cls, status: str, order: BaseOrder) -> str:
        """
        Validate a given status transition for the order.
        By default check status is defined in transitions.

        Args:
            status (str): New status
            order (Order): Order instance

        Raises:
            ValidationError: If transition not valid

        Returns:
            str: Validated status
        """
        transitions = cls.get_transitions().get(order.status, [status])
        transitions.append(order.status)
        if status not in transitions:
            current, new = cls[order.status].label, cls[status].label  # type: ignore
            msg = _(f"Can't change order with status '{current}' to '{new}'.")
            raise ValidationError(msg)
        return status

Note

To include base validation be sure to call super() when overriding validation.

Custom reference generator

Order reference is generated when a new order get’s created and represents a unique identifier for that order. By default reference is generated in a {year}-{5-digit-increment} format using salesman.orders.utils.generate_ref() function.

You can change it by providing a dotted path in SALESMAN_ORDER_REFERENCE_GENERATOR setting that points to a function that returns a unique reference string.

def generate_ref(request: HttpRequest) -> str:
    """
    Default order reference generator function. Can be overriden by providing a
    dotted path to a function in ``SALESMAN_ORDER_REFERENCE_GENERATOR`` setting.

    Default format is ``{year}-{5-digit-increment}`` (eg. `2020-00001`).

    Args:
        request (HttpRequest): Django request

    Returns:
        str: New order reference
    """
    year = timezone.now().year
    Order = get_salesman_model("Order")
    last = Order.objects.filter(date_created__year=year, ref__isnull=False).first()
    increment = int(last.ref.split("-")[1]) + 1 if last and last.ref else 1
    return f"{year}-{increment:05d}"

Your custom function should accept Django’s request object as a parameter.

Custom order serializer

You can override order serializer by providing a dotted path in SALESMAN_ORDER_SERIALIZER setting. Optionally you can configure a separate order “summary” serializer with SALESMAN_ORDER_SUMMARY_SERIALIZER setting, otherwise the same order serializer is used. Default serializer for is set to salesman.orders.serializers.OrderSerializer.

You can also override the prefetch_related_fields and select_related_fields in OrderSerializer.Meta properties to optimize for any added relations to your custom serializer.