Repository: django-oscar/django-oscar

Issue:
Offer Consumed Many Times on Same Product
### Issue Summary
The Offer Incentive type "Discount is percentage off of the products value" will multiply the discount value by the max affected items. For example, an Offer for 10% discount with max affected 5 items in the basket will result in a 50% discount on 1 product, instead of the expected 10% discount. 

### Steps to Reproduce
1. Create a new offer with incentive 10% discount and max affected items 5
2. The offer will apply itself 5 * 10% resulting in a 50% discount

![image](https://user-images.githubusercontent.com/674282/64397229-c3c45900-d08a-11e9-894f-df225f43a336.png)
![image](https://user-images.githubusercontent.com/674282/64397235-ccb52a80-d08a-11e9-92b8-205e60f7e481.png)
![image](https://user-images.githubusercontent.com/674282/64397257-d9d21980-d08a-11e9-8be8-46eb87462b6c.png)

![image](https://user-images.githubusercontent.com/674282/64397312-10a82f80-d08b-11e9-83fc-158b07c1f9a5.png)


### Technical details

Can reproduce on the Sandbox site with Oscar 2.0 
https://latest.oscarcommerce.com/
Example Offer - https://latest.oscarcommerce.com/en-gb/dashboard/offers/4/



Discussion:

Resolving patch:
diff --git a/src/oscar/apps/basket/utils.py b/src/oscar/apps/basket/utils.py
--- a/src/oscar/apps/basket/utils.py
+++ b/src/oscar/apps/basket/utils.py
@@ -162,6 +162,6 @@ def available(self, offer=None) -> int:
 
             # respect max_affected_items
             if offer.benefit.max_affected_items:
-                max_affected_items = offer.benefit.max_affected_items
+                max_affected_items = min(offer.benefit.max_affected_items, max_affected_items)
 
         return max_affected_items - self.consumed(offer)
diff --git a/src/oscar/apps/customer/abstract_models.py b/src/oscar/apps/customer/abstract_models.py
--- a/src/oscar/apps/customer/abstract_models.py
+++ b/src/oscar/apps/customer/abstract_models.py
@@ -1,5 +1,6 @@
 from django.conf import settings
 from django.contrib.auth import models as auth_models
+from django.core.mail import send_mail
 from django.core.validators import RegexValidator
 from django.db import models
 from django.template import TemplateDoesNotExist, engines
@@ -79,13 +80,29 @@ class Meta:
         verbose_name = _('User')
         verbose_name_plural = _('Users')
 
+    def clean(self):
+        super().clean()
+        self.email = self.__class__.objects.normalize_email(self.email)
+
     def get_full_name(self):
+        """
+        Return the first_name plus the last_name, with a space in between.
+        """
         full_name = '%s %s' % (self.first_name, self.last_name)
         return full_name.strip()
 
     def get_short_name(self):
+        """
+        Return the short name for the user.
+        """
         return self.first_name
 
+    def email_user(self, subject, message, from_email=None, **kwargs):
+        """
+        Send an email to this user.
+        """
+        send_mail(subject, message, from_email, [self.email], **kwargs)
+
     def _migrate_alerts_to_user(self):
         """
         Transfer any active alerts linked to a user's email address to the
diff --git a/src/oscar/templatetags/currency_filters.py b/src/oscar/templatetags/currency_filters.py
--- a/src/oscar/templatetags/currency_filters.py
+++ b/src/oscar/templatetags/currency_filters.py
@@ -14,6 +14,9 @@ def currency(value, currency=None):
     """
     Format decimal value as currency
     """
+    if currency is None:
+        currency = settings.OSCAR_DEFAULT_CURRENCY
+
     try:
         value = D(value)
     except (TypeError, InvalidOperation):
@@ -22,7 +25,7 @@ def currency(value, currency=None):
     # http://babel.pocoo.org/en/latest/api/numbers.html#babel.numbers.format_currency
     OSCAR_CURRENCY_FORMAT = getattr(settings, 'OSCAR_CURRENCY_FORMAT', None)
     kwargs = {
-        'currency': currency or settings.OSCAR_DEFAULT_CURRENCY,
+        'currency': currency,
         'locale': to_locale(get_language() or settings.LANGUAGE_CODE)
     }
     if isinstance(OSCAR_CURRENCY_FORMAT, dict):


Test patch:
diff --git a/tests/integration/basket/test_utils.py b/tests/integration/basket/test_utils.py
--- a/tests/integration/basket/test_utils.py
+++ b/tests/integration/basket/test_utils.py
@@ -1,6 +1,7 @@
 
 import pytest
 
+from oscar.apps.offer import models
 from oscar.apps.offer.applicator import Applicator
 from oscar.test.factories import (
     BasketFactory, ConditionalOfferFactory, ProductFactory)
@@ -58,6 +59,25 @@ def test_consumed_no_offer(self, filled_basket):
         for line in filled_basket.all_lines():
             assert line.consumer.consumed() == 0
 
+    def test_available_with_offer(self):
+        basket = BasketFactory()
+        product1 = ProductFactory()
+        product2 = ProductFactory()
+        basket.add_product(product1, quantity=1)
+        basket.add_product(product2, quantity=10)
+
+        benefit = models.Benefit(
+            type=models.Benefit.PERCENTAGE,
+            value=10,
+            max_affected_items=5,
+        )
+        benefit.save()
+
+        offer1 = ConditionalOfferFactory(name='offer1', benefit=benefit)
+        lines = basket.all_lines()
+        assert lines[0].consumer.available(offer1) == 1
+        assert lines[1].consumer.available(offer1) == 5
+
     def test_consumed_with_offer(self, filled_basket):
         offer1 = ConditionalOfferFactory(name='offer1')
         offer2 = ConditionalOfferFactory(name='offer2')