How to handle US taxes

When trading in the US, taxes aren’t known until the customer’s shipping address has been entered. This scenario requires two changes from core Oscar.

Ensure your site strategy returns prices without taxes applied

First, the site strategy should return all prices without tax when the customer is based in the US. Oscar provides a US strategy class that uses the DeferredTax mixin to indicate that prices don’t include taxes.

See the documentation on strategies for further guidance on how to replace strategies.

Adjust checkout views to apply taxes once they are known

Second, the CheckoutSessionMixin should be overridden within your project to apply taxes to the submission.

from oscar.apps.checkout import session

from . import tax

class CheckoutSessionMixin(session.CheckoutSessionMixin):

    def build_submission(self, **kwargs):
        submission = super().build_submission(
            **kwargs)

        if submission['shipping_address'] and submission['shipping_method']:
            tax.apply_to(submission)

            # Recalculate order total to ensure we have a tax-inclusive total
            submission['order_total'] = self.get_order_totals(
                submission['basket'],
                submission['shipping_charge'])

        return submission

An example implementation of the tax.py module is:

from decimal import Decimal as D

def apply_to(submission):
    # Assume 7% sales tax on sales to New Jersey  You could instead use an
    # external service like Avalara to look up the appropriates taxes.
    STATE_TAX_RATES = {
        'NJ': D('0.07')
    }
    shipping_address = submission['shipping_address']
    rate = STATE_TAX_RATES.get(
        shipping_address.state, D('0.00'))
    for line in submission['basket'].all_lines():
        line_tax = calculate_tax(
            line.line_price_excl_tax_incl_discounts, rate)
        unit_tax = (line_tax / line.quantity).quantize(D('0.01'))
        line.purchase_info.price.tax = unit_tax

    # Note, we change the submission in place - we don't need to
    # return anything from this function
    shipping_charge = submission['shipping_charge']
    if shipping_charge is not None:
        shipping_charge.tax = calculate_tax(
            shipping_charge.excl_tax, rate)

def calculate_tax(price, rate):
    tax = price * rate
    return tax.quantize(D('0.01'))