Prices and availability

This page explains how prices and availability are determined in Oscar. In short, it seems quite complicated at first as there are several parts to it, but what this buys is flexibility: buckets of it.

Overview

Simpler e-commerce frameworks often tie prices to the product model directly:

>>> product = Product.objects.get(id=1)
>>> product.price
Decimal('17.99')

Oscar, on the other hand, distinguishes products from stockrecords and provides a swappable ‘strategy’ component for selecting the appropriate stockrecord, calculating prices and availability information.

>>> from oscar.apps.partner.strategy import Selector
>>> product = Product.objects.get(id=1)
>>> strategy = Selector().strategy()
>>> info = strategy.fetch_for_product(product)

# Availability information
>>> info.availability.is_available_to_buy
True
>>> msg = info.availability.message
>>> str(msg)
"In stock (58 available)"
>>> info.availability.is_purchase_permitted(59)
(False, "A maximum of 58 can be bought")

# Price information
>>> info.price.excl_tax
Decimal('17.99')
>>> info.price.is_tax_known
True
>>> info.price.incl_tax
Decimal('21.59')
>>> info.price.tax
Decimal('3.60')
>>> info.price.currency
'GBP'

The Product model captures the core data about the product (title, description, images) while the StockRecord model represents fulfilment information for one particular partner (number in stock, base price). A product can have multiple stockrecords although only one is selected by the strategy to determine pricing and availability.

By using your own custom strategy class, a wide range of pricing, tax and availability problems can be easily solved.

The strategy class

Oscar uses a ‘strategy’ object to determine product availability and pricing. A new strategy instance is assigned to the request by the basket middleware. A Selector class determines the appropriate strategy for the request. By modifying the Selector class, it’s possible to return different strategies for different customers.

Given a product, the strategy class is responsible for:

  • Selecting a “pricing policy”, an object detailing the prices of the product and whether tax is known.

  • Selecting an “availability policy”, an object responsible for availability logic (i.e. is the product available to buy) and customer messaging.

  • Selecting the appropriate stockrecord to use for fulfilment. If a product can be fulfilled by several fulfilment partners, then each will have their own stockrecord.

These three entities are wrapped up in a PurchaseInfo object, which is a simple named tuple. The strategy class provides fetch_for_product and fetch_for_parent methods which takes a product and returns a PurchaseInfo instance:

The strategy class is used in several places in Oscar, for example, the purchase_info_for_product template tag which is used to load the price and availability information into the template context:

{% load purchase_info_tags %}
{% load currency_filters %}

{% purchase_info_for_product request product as session %}

<p>
{% if session.price.is_tax_known %}
    Price is {{ session.price.incl_tax|currency:session.price.currency }}
{% else %}
    Price is {{ session.price.excl_tax|currency:session.price.currency }} +
    tax
{% endif %}
</p>

Note that the currency template tag accepts a currency parameter from the pricing policy.

Also, basket instances have a strategy instance assigned so they can calculate prices including taxes. This is done automatically in the basket middleware.

This seems quite complicated…

While this probably seems like quite an involved way of looking up a product’s price, it gives the developer an immense amount of flexibility. Here’s a few examples of things you can do with a strategy class:

  • Transact in multiple currencies. The strategy class can use the customer’s location to select a stockrecord from a local distribution partner which will be in the local currency of the customer.

  • Elegantly handle different tax models. A strategy can return prices including tax for a UK or European visitor, but without tax for US visitors where tax is only determined once shipping details are confirmed.

  • Charge different prices to different customers. A strategy can return a different pricing policy depending on the user/session.

  • Use a chain of preferred partners for fulfilment. A site could have many stockrecords for the same product, each from a different fulfilment partner. The strategy class could select the partner with the best margin and stock available. When stock runs out with that partner, the strategy could seamlessly switch to the next best partner.

These are the kinds of problems that other e-commerce frameworks would struggle with.

API

All strategies subclass a common Base class:

class oscar.apps.partner.strategy.Base(request=None)[source]

The base strategy class

Given a product, strategies are responsible for returning a PurchaseInfo instance which contains:

  • The appropriate stockrecord for this customer

  • A pricing policy instance

  • An availability policy instance

fetch_for_line(line, stockrecord=None)[source]

Given a basket line instance, fetch a PurchaseInfo instance.

This method is provided to allow purchase info to be determined using a basket line’s attributes. For instance, “bundle” products often use basket line attributes to store SKUs of contained products. For such products, we need to look at the availability of each contained product to determine overall availability.

fetch_for_parent(product)[source]

Given a parent product, fetch a StockInfo instance

fetch_for_product(product, stockrecord=None)[source]

Given a product, return a PurchaseInfo instance.

The PurchaseInfo class is a named tuple with attributes:

  • price: a pricing policy object.

  • availability: an availability policy object.

  • stockrecord: the stockrecord that is being used

If a stockrecord is passed, return the appropriate PurchaseInfo instance for that product and stockrecord is returned.

Oscar also provides a “structured” strategy class which provides overridable methods for selecting the stockrecord, and determining pricing and availability policies:

class oscar.apps.partner.strategy.Structured(request=None)[source]

A strategy class which provides separate, overridable methods for determining the 3 things that a PurchaseInfo instance requires:

  1. A stockrecord

  2. A pricing policy

  3. An availability policy

availability_policy(product, stockrecord)[source]

Return the appropriate availability policy

fetch_for_parent(product)[source]

Given a parent product, fetch a StockInfo instance

fetch_for_product(product, stockrecord=None)[source]

Return the appropriate PurchaseInfo instance.

This method is not intended to be overridden.

pricing_policy(product, stockrecord)[source]

Return the appropriate pricing policy

select_children_stockrecords(product)[source]

Select appropriate stock record for all children of a product

select_stockrecord(product)[source]

Select the appropriate stockrecord

For most projects, subclassing and overriding the Structured base class should be sufficient. However, Oscar also provides mixins to easily compose the appropriate strategy class for your domain.

Currency

Oscar allows you to define a currency code for each stock - a text field that defaults to settings.OSCAR_DEFAULT_CURRENCY.

By default, Oscar expects all products added to a single basket to have the same currency. It does not however do any logic to select the appropriate stock record to achieve this - you must implement this yourself in the select_stockrecord() method. Oscar does not determine or store user currency and uses it only for formatting product price. More complex logic, like currency switch or conversion can be implemented additionally.

More about currency formatting configuration - OSCAR_CURRENCY_FORMAT.

Loading a strategy

Strategy instances are determined by the Selector class:

class oscar.apps.partner.strategy.Selector[source]

Responsible for returning the appropriate strategy class for a given user/session.

This can be called in three ways:

  1. Passing a request and user. This is for determining prices/availability for a normal user browsing the site.

  2. Passing just the user. This is for offline processes that don’t have a request instance but do know which user to determine prices for.

  3. Passing nothing. This is for offline processes that don’t correspond to a specific user, e.g., determining a price to store in a search index.

strategy(request=None, user=None, **kwargs)[source]

Return an instantiated strategy instance

It’s common to override this class so a custom strategy class can be returned.

Pricing policies

A pricing policy is a simple class with several properties Its job is to contain all price and tax information about a product.

There is a base class that defines the interface a pricing policy should have:

class oscar.apps.partner.prices.Base[source]

The interface that any pricing policy must support

currency = None

Price currency (3 char code)

excl_tax = None

Price excluding tax

exists = False

Whether any prices exist

incl_tax = None

Price including tax

is_tax_known = False

Whether tax is known

retail = None

Retail price

tax = None

Price tax

There are also several policies that accommodate common scenarios:

class oscar.apps.partner.prices.Unavailable[source]

This should be used as a pricing policy when a product is unavailable and no prices are known.

class oscar.apps.partner.prices.FixedPrice(currency, excl_tax, tax=None)[source]

This should be used for when the price of a product is known in advance.

It can work for when tax isn’t known (like in the US).

Note that this price class uses the tax-exclusive price for offers, even if the tax is known. This may not be what you want. Use the TaxInclusiveFixedPrice class if you want offers to use tax-inclusive prices.

property is_tax_known

Test whether the tax is known or not

Availability policies

Like pricing policies, availability policies are simple classes with several properties and methods. The job of an availability policy is to provide availability messaging to show to the customer as well as methods to determine if the product is available to buy.

The base class defines the interface:

class oscar.apps.partner.availability.Base[source]

Base availability policy.

code = ''

Availability code. This is used for HTML classes

dispatch_date = None

When this item should be dispatched

property is_available_to_buy

Test if this product is available to be bought. This is used for validation when a product is added to a user’s basket.

is_purchase_permitted(quantity)[source]

Test whether a proposed purchase is allowed

Should return a boolean and a reason

message = ''

A description of the availability of a product. This is shown on the product detail page, e.g., “In stock”, “Out of stock” etc

property short_message

A shorter version of the availability message, suitable for showing on browsing pages.

There are also several predefined availability policies:

class oscar.apps.partner.availability.Unavailable[source]

Policy for when a product is unavailable

class oscar.apps.partner.availability.Available[source]

For when a product is always available, irrespective of stock level.

This might be appropriate for digital products where stock doesn’t need to be tracked and the product is always available to buy.

is_purchase_permitted(quantity)[source]

Test whether a proposed purchase is allowed

Should return a boolean and a reason

class oscar.apps.partner.availability.StockRequired(num_available)[source]

Allow a product to be bought while there is stock. This policy is instantiated with a stock number (num_available). It ensures that the product is only available to buy while there is stock available.

This is suitable for physical products where back orders (e.g. allowing purchases when there isn’t stock available) are not permitted.

property code

Code indicating availability status.

is_purchase_permitted(quantity)[source]

Test whether a proposed purchase is allowed

Should return a boolean and a reason

property message

Full availability text, suitable for detail pages.

property short_message

A shorter version of the availability message, suitable for showing on browsing pages.

Strategy mixins

Oscar also ships with several mixins which implement one method of the Structured strategy. These allow strategies to be easily composed from re-usable parts:

class oscar.apps.partner.strategy.UseFirstStockRecord[source]

Stockrecord selection mixin for use with the Structured base strategy. This mixin picks the first (normally only) stockrecord to fulfil a product.

This is backwards compatible with Oscar<0.6 where only one stockrecord per product was permitted.

class oscar.apps.partner.strategy.StockRequired[source]

Availability policy mixin for use with the Structured base strategy. This mixin ensures that a product can only be bought if it has stock available (if stock is being tracked).

class oscar.apps.partner.strategy.NoTax[source]

Pricing policy mixin for use with the Structured base strategy. This mixin specifies zero tax and uses the price from the stockrecord.

class oscar.apps.partner.strategy.FixedRateTax[source]

Pricing policy mixin for use with the Structured base strategy. This mixin applies a fixed rate tax to the base price from the product’s stockrecord. The price_incl_tax is quantized to two decimal places. Rounding behaviour is Decimal’s default

get_exponent(stockrecord)[source]

This method serves as hook to be able to plug in support for a varying exponent based on the currency.

TODO: Needs tests.

get_rate(product, stockrecord)[source]

This method serves as hook to be able to plug in support for varying tax rates based on the product.

TODO: Needs tests.

class oscar.apps.partner.strategy.DeferredTax[source]

Pricing policy mixin for use with the Structured base strategy. This mixin does not specify the product tax and is suitable to territories where tax isn’t known until late in the checkout process.

Default strategy

Oscar’s default Selector class returns a Default strategy built from the strategy mixins:

class Default(UseFirstStockRecord, StockRequired, NoTax, Structured):
    pass

The behaviour of this strategy is:

  • Always picks the first stockrecord (this is backwards compatible with Oscar<0.6 where a product could only have one stockrecord).

  • Charge no tax.

  • Only allow purchases where there is appropriate stock (e.g. no back-orders).

How to use

There’s lots of ways to use strategies, pricing and availability policies to handle your domain’s requirements.

The normal first step is provide your own Selector class which returns a custom strategy class. Your custom strategy class can be composed of the above mixins or your own custom logic.

Example 1: UK VAT

Here’s an example strategy.py module which is used to charge VAT on prices.

# myproject/partner/strategy.py
from decimal import Decimal as D
from oscar.apps.partner import strategy, prices


class Selector(object):
    """
    Custom selector to return a UK-specific strategy that charges VAT
    """

    def strategy(self, request=None, user=None, **kwargs):
        return UKStrategy()


class IncludingVAT(strategy.FixedRateTax):
    """
    Price policy to charge VAT on the base price
    """
    # We can simply override the tax rate on the core FixedRateTax.  Note
    # this is a simplification: in reality, you might want to store tax
    # rates and the date ranges they apply in a database table.  Your
    # pricing policy could simply look up the appropriate rate.
    rate = D('0.20')


class UKStrategy(strategy.UseFirstStockRecord, IncludingVAT,
                 strategy.StockRequired, strategy.Structured):
    """
    Typical UK strategy for physical goods.

    - There's only one warehouse/partner so we use the first and only stockrecord
    - Enforce stock level.  Don't allow purchases when we don't have stock.
    - Charge UK VAT on prices.  Assume everything is standard-rated.
    """

Example 2: US sales tax

Here’s an example strategy.py module which is suitable for use in the US where taxes can’t be calculated until the shipping address is known. You normally need to use a 3rd party service to determine taxes - details omitted here.

from oscar.apps.partner import strategy, prices


class Selector(object):
    """
    Custom selector class to returns a US strategy
    """

    def strategy(self, request=None, user=None, **kwargs):
        return USStrategy()


class USStrategy(strategy.UseFirstStockRecord, strategy.DeferredTax,
                 strategy.StockRequired, strategy.Structured):
    """
    Typical US strategy for physical goods.  Note we use the ``DeferredTax``
    mixin to ensure prices are returned without tax.

    - Use first stockrecord
    - Enforce stock level
    - Taxes aren't known for prices at this stage
    """