Extend price object to specify price to use for offers

This removes the need for the "applies_to_tax_exclusive_prices" field on
the offer model.  Now the strategy can specify which price to use for
offers.

This change also introduces a sample UK strategy which specifies that
offers apply to tax-inclusive prices.
Remove applies_to_tax_exclusive_prices field

It's no longer necessary now that the price object can specify which
price to use for offers.
Ensure discounts are correctly applied to shipping methods without tax

We need to wrap the shipping method in a different discount wrapper
depending on whether the shipping method is aware of taxes.
diff --git a/oscar/apps/dashboard/vouchers/forms.py b/oscar/apps/dashboard/vouchers/forms.py
index e648543..ef4fadb 100644
--- a/oscar/apps/dashboard/vouchers/forms.py
+++ b/oscar/apps/dashboard/vouchers/forms.py
@@ -18,9 +18,9 @@ class VoucherForm(forms.Form):
     code = forms.CharField(label=_("Code"))

     start_date = forms.DateField(
-        label=_("Start date"), widget=widgets.DatePickerInput())
+        label=_("Start date"), widget=widgets.DatePickerInput)
     end_date = forms.DateField(
-        label=_("End date"), widget=widgets.DatePickerInput())
+        label=_("End date"), widget=widgets.DatePickerInput)
     usage = forms.ChoiceField(choices=Voucher.USAGE_CHOICES, label=_("Usage"))

     benefit_range = forms.ModelChoiceField(
diff --git a/oscar/apps/offer/models.py b/oscar/apps/offer/models.py
index 479d4ba..10a3770 100644
--- a/oscar/apps/offer/models.py
+++ b/oscar/apps/offer/models.py
@@ -1519,4 +1519,5 @@ class ShippingPercentageDiscountBenefit(ShippingBenefit):
         verbose_name_plural = _("Shipping percentage discount benefits")

     def shipping_discount(self, charge):
-        return charge * self.value / D('100.0')
+        discount = charge * self.value / D('100.0')
+        return discount.quantize(D('0.01'))
diff --git a/oscar/apps/promotions/context_processors.py b/oscar/apps/promotions/context_processors.py
index 151c256..1ec9dc4 100644
--- a/oscar/apps/promotions/context_processors.py
+++ b/oscar/apps/promotions/context_processors.py
@@ -49,8 +49,3 @@ def split_by_position(linked_promotions, context):
         if key not in context:
             context[key] = []
         context[key].append(promotion)
-
-
-
-
-
diff --git a/oscar/apps/shipping/base.py b/oscar/apps/shipping/base.py
index b901d68..93228cb 100644
--- a/oscar/apps/shipping/base.py
+++ b/oscar/apps/shipping/base.py
@@ -49,6 +49,8 @@ class Base(object):
         return self.charge_incl_tax - self.charge_excl_tax

     def _set_tax(self, value):
+        # Tax can be set later on in territories where it is now known up
+        # front.
         self.charge_incl_tax = self.charge_excl_tax + value
         self.is_tax_known = True

diff --git a/oscar/apps/shipping/methods.py b/oscar/apps/shipping/methods.py
index 63d7e32..2c350fa 100644
--- a/oscar/apps/shipping/methods.py
+++ b/oscar/apps/shipping/methods.py
@@ -91,6 +91,33 @@ class OfferDiscount(Base):
         return self.method.is_tax_known

     @property
+    def charge_excl_tax(self):
+        raise NotImplemented()
+
+    @property
+    def charge_incl_tax(self):
+        raise NotImplemented()
+
+
+class TaxExclusiveOfferDiscount(OfferDiscount):
+
+    @property
+    def charge_excl_tax(self):
+        parent_charge = self.method.charge_excl_tax
+        discount = self.offer.shipping_discount(parent_charge)
+        return parent_charge - discount
+
+    @property
+    def charge_incl_tax(self):
+        """
+        Tax needs to be assigned later on
+        """
+        return self.charge_excl_tax + self.tax
+
+
+class TaxInclusiveOfferDiscount(OfferDiscount):
+
+    @property
     def charge_incl_tax(self):
         parent_charge = self.method.charge_incl_tax
         discount = self.offer.shipping_discount(parent_charge)
@@ -105,5 +132,6 @@ class OfferDiscount(Base):
         charge_incl_tax = self.charge_incl_tax
         if parent_charge_incl_tax == 0:
             return D('0.00')
-        return parent_charge_excl_tax * (charge_incl_tax /
-                                         parent_charge_incl_tax)
+        charge = parent_charge_excl_tax * (charge_incl_tax /
+                                           parent_charge_incl_tax)
+        return charge.quantize(D('0.01'))
diff --git a/oscar/apps/shipping/repository.py b/oscar/apps/shipping/repository.py
index 2b51692..debfdd2 100644
--- a/oscar/apps/shipping/repository.py
+++ b/oscar/apps/shipping/repository.py
@@ -63,14 +63,23 @@ class Repository(object):
         Prime an individual method instance
         """
         method.set_basket(basket)
+
         # If the basket has a shipping offer, wrap the shipping method with a
         # decorating class that applies the offer discount to the shipping
         # charge.
         if basket.offer_applications.shipping_discounts:
             # We assume there is only one shipping discount available
             discount = basket.offer_applications.shipping_discounts[0]
-            if method.charge_incl_tax > D('0.00'):
-                return methods.OfferDiscount(method, discount['offer'])
+            if method.charge_excl_tax > D('0.00'):
+                if method.is_tax_known:
+                    return methods.TaxInclusiveOfferDiscount(
+                        method, discount['offer'])
+                else:
+                    # When returning a tax exclusive discount, it is assumed
+                    # that this will be used to calculate taxes which will then
+                    # be assigned directly to the method instance.
+                    return methods.TaxExclusiveOfferDiscount(
+                        method, discount['offer'])
         return method

     def find_by_code(self, code, basket):
diff --git a/oscar/test/factories.py b/oscar/test/factories.py
index 6aec8ba..67cff91 100644
--- a/oscar/test/factories.py
+++ b/oscar/test/factories.py
@@ -60,7 +60,7 @@ def create_stockinfo(record):
 def create_product(upc=None, title=u"Dummy title",
                    product_class=u"Dummy item class",
                    partner=u"Dummy partner", partner_sku=None, price=None,
-                   num_in_stock=None, attributes=None,
+                   num_in_stock=None, attributes=None,
                    partner_users=None, **kwargs):
     """
     Helper method for creating products that are used in tests.
@@ -88,7 +88,7 @@ def create_product(upc=None, title=u"Dummy title",
     return product

-def create_basket(empty=False, strategy=None):
+def create_basket(empty=False):
     basket = Basket.objects.create()
     basket.strategy = strategy.Default()
     if not empty:
diff --git a/tests/integration/offer/shipping_percentage_benefit_tests.py b/tests/integration/offer/shipping_percentage_benefit_tests.py
index 892a0cb..d72e45e 100644
--- a/tests/integration/offer/shipping_percentage_benefit_tests.py
+++ b/tests/integration/offer/shipping_percentage_benefit_tests.py
@@ -2,9 +2,20 @@ from decimal import Decimal as D

 from django.test import TestCase

-from oscar.apps.offer import models
+from oscar.apps.offer import models, utils
 from oscar.test.basket import add_product
 from oscar.test import factories
+from oscar.apps.shipping import repository, methods
+
+
+class ExcludingTax(methods.Base):
+    charge_excl_tax = D('10.00')
+
+
+class IncludingTax(methods.Base):
+    charge_excl_tax = D('10.00')
+    charge_incl_tax = D('12.00')
+    is_tax_known = True

 class TestAShippingPercentageDiscountAppliedWithCountCondition(TestCase):
@@ -44,3 +55,26 @@ class TestAShippingPercentageDiscountAppliedWithCountCondition(TestCase):
         self.assertEqual(2, self.basket.num_items_with_discount)
         self.assertEqual(1, self.basket.num_items_without_discount)
         self.assertTrue(result.affects_shipping)
+
+    def test_applies_correctly_to_shipping_method_without_tax(self):
+        add_product(self.basket, D('12.00'), 3)
+
+        # Apply offers to basket
+        utils.Applicator().apply_offers(self.basket, [self.offer])
+
+        repo = repository.Repository()
+        raw_method = ExcludingTax()
+        method = repo.prime_method(self.basket, raw_method)
+        self.assertEqual(D('5.00'), method.charge_excl_tax)
+
+    def test_applies_correctly_to_shipping_method_with_tax(self):
+        add_product(self.basket, D('12.00'), 3)
+
+        # Apply offers to basket
+        utils.Applicator().apply_offers(self.basket, [self.offer])
+
+        repo = repository.Repository()
+        raw_method = IncludingTax()
+        method = repo.prime_method(self.basket, raw_method)
+        self.assertEqual(D('6.00'), method.charge_incl_tax)
+        self.assertEqual(D('5.00'), method.charge_excl_tax)
Adjust sample strategies for UK and US

- Improve documentation on what they are for
- Move tax rate into UK strategy
Ensure get_discount uses the correct parent charge for discount

Before this, it was wrongly using the tax inclusive charge each time.
Merge pull request #1076 from tangentlabs/issue/1070/offer-prices

Extend offers to work in both US and UK style tax regimes