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/

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)


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')