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
Add support for file and image attributes
Handle fuzz data within search facets

TIL Haystack passes "selected_facets" data unfiltered to the search
form, and so you have to be careful in interpretting the data passed.
This change avoids sequence errors when creating a dict from the passed
facet data.
# Last 5 commit messages
# ----------------------
# 07eff7b Add support for file and image attributes [Izidor Matušov]
# ec12c47 Merge pull request #1076 from tangentlabs/issue/1070/offer-prices [David Winterbottom]
# 41c7edc Merge pull request #1020 [David Winterbottom]
# a9eb4f2 Ensure get_discount uses the correct parent charge for discount [David Winterbottom]
# 2de69cc Adjust sample strategies for UK and US [David Winterbottom]
Ensure basket line prices are calculated to correct precision

Use D instead of Decimal everywhere.
Remove dead offers section from product update form

Based on 8617a75, we remove the offers section and correct the loading
of the ranges section within the product dashboard template.
Show spelling suggestion even if no results
Introduce new API method for strategies (for basket lines)

This allows availability to be correctly determined for complex basket
lines which use basket attributes to hold information vital for
availability decisions.  An example is bundle products that store bundle
product IDs in the basket line attributes.

A new template tag is also introduced, which has an inconsistent name
with the product version.  This will be fixed in a later commit.
s/session_strategy/purchase_info_for_product

Rename product templatetag to be inline with the basket line one.
Rename strategy fetch method to fetch_for_product

More explicit innit.
Rename StockInfo to PurchaseInfo

The word "stock info" was bugging me as it's not quite correct for the
combination of availability and price information.  The problem is the
word "stock" which implies physical widgets in a warehouse somewhere.
It sounds weird for ask for stock info for a digital product.

Hence, I came up with PurchaseInfo, which is a bit more vague but
captures the information better.
Extend docstring for new fetch_for_line strategy method
Rename stockrecord_tags to purchase_info_tags
Adjust purchase_info template tags

- Use simpler decorator
- Tighten validation check
Merge pull request #1008 from tangentlabs/issue/1008

Need to determine stock info of basket lines which have attributes
Pin requests version

This avoids a pesky UnicodeDecodeError that occurs with Haystack when
trying to rebuild the index.