瀏覽代碼

Fix rendering of upsell messages for exclusive offers (#3548)

* Improve `available` method of `LineOfferConsumer` class
* Add `quantity_available_for_offer` method to `AbstractLine` class
* Improve conditions: "Get num matches" methods updated to use `quantity_available_for_offer`
method of `Line` model.
* Improve conditions: `get_upsell_message` methods updated to show upsell message only in
the cases where `delta` > 0.
* Ensure basket upsell messages are not `None`
master
Basil Dubyk 5 年之前
父節點
當前提交
0f3df5fca9
沒有連結到貢獻者的電子郵件帳戶。

+ 3
- 0
src/oscar/apps/basket/abstract_models.py 查看文件

@@ -772,6 +772,9 @@ class AbstractLine(models.Model):
772 772
     def is_available_for_offer_discount(self, offer):
773 773
         return self.consumer.available(offer) > 0
774 774
 
775
+    def quantity_available_for_offer(self, offer):
776
+        return self.quantity_without_offer_discount(offer) + self.quantity_with_offer_discount(offer)
777
+
775 778
     # ==========
776 779
     # Properties
777 780
     # ==========

+ 20
- 7
src/oscar/apps/basket/utils.py 查看文件

@@ -143,7 +143,7 @@ class LineOfferConsumer(object):
143 143
     def consumers(self):
144 144
         return [x for x in self._offers.values() if self.consumed(x)]
145 145
 
146
-    def available(self, offer=None) -> int:
146
+    def available(self, offer=None) -> int: # noqa (too complex (11))
147 147
         """
148 148
         check how many items are available for offer
149 149
 
@@ -158,13 +158,26 @@ class LineOfferConsumer(object):
158 158
 
159 159
             applied = [x for x in self.consumers if x != offer]
160 160
 
161
-            # find any *other* exclusive offers
162
-            if any([x.exclusive for x in applied]):
163
-                return 0
161
+            if offer.exclusive:
162
+                for a in applied:
163
+                    if a.exclusive:
164
+                        if any([
165
+                            a.priority > offer.priority,
166
+                            a.priority == offer.priority and a.id < offer.id
167
+                        ]):
168
+                            # Exclusive offers cannot be applied if any other exclusive
169
+                            # offer with higher priority is active already.
170
+                            max_affected_items = max_affected_items - self.consumed(a)
171
+                            if max_affected_items == 0:
172
+                                return 0
173
+
174
+                    else:
175
+                        # Exclusive offers cannot be applied if any other offers are
176
+                        # active already.
177
+                        return 0
164 178
 
165
-            # exclusive offers cannot be applied if any other
166
-            # offers are active already
167
-            if offer.exclusive and len(applied):
179
+            # find any *other* exclusive offers
180
+            elif any([x.exclusive for x in applied]):
168 181
                 return 0
169 182
 
170 183
             # check for applied offers allowing restricted combinations

+ 25
- 23
src/oscar/apps/offer/conditions.py 查看文件

@@ -57,9 +57,8 @@ class CountCondition(Condition):
57 57
             return getattr(self, '_num_matches')
58 58
         num_matches = 0
59 59
         for line in basket.all_lines():
60
-            if (self.can_apply_condition(line)
61
-                    and line.quantity_without_offer_discount(offer) > 0):
62
-                num_matches += line.quantity_without_offer_discount(offer)
60
+            if self.can_apply_condition(line):
61
+                num_matches += line.quantity_available_for_offer(offer)
63 62
         self._num_matches = num_matches
64 63
         return num_matches
65 64
 
@@ -70,9 +69,12 @@ class CountCondition(Condition):
70 69
     def get_upsell_message(self, offer, basket):
71 70
         num_matches = self._get_num_matches(basket, offer)
72 71
         delta = self.value - num_matches
73
-        return ngettext('Buy %(delta)d more product from %(range)s',
74
-                        'Buy %(delta)d more products from %(range)s', delta) \
75
-            % {'delta': delta, 'range': self.range}
72
+        if delta > 0:
73
+            return ngettext(
74
+                'Buy %(delta)d more product from %(range)s',
75
+                'Buy %(delta)d more products from %(range)s',
76
+                delta
77
+            ) % {'delta': delta, 'range': self.range}
76 78
 
77 79
     def consume_items(self, offer, basket, affected_lines):
78 80
         """
@@ -142,21 +144,21 @@ class CoverageCondition(Condition):
142 144
         return False
143 145
 
144 146
     def _get_num_covered_products(self, basket, offer):
145
-        covered_ids = []
147
+        covered_ids = set()
146 148
         for line in basket.all_lines():
147
-            if not line.is_available_for_offer_discount(offer):
148
-                continue
149 149
             product = line.product
150
-            if (self.can_apply_condition(line) and product.id not in
151
-                    covered_ids):
152
-                covered_ids.append(product.id)
150
+            if self.can_apply_condition(line) and line.quantity_available_for_offer(offer) > 0:
151
+                covered_ids.add(product.id)
153 152
         return len(covered_ids)
154 153
 
155 154
     def get_upsell_message(self, offer, basket):
156 155
         delta = self.value - self._get_num_covered_products(basket, offer)
157
-        return ngettext('Buy %(delta)d more product from %(range)s',
158
-                        'Buy %(delta)d more products from %(range)s', delta) \
159
-            % {'delta': delta, 'range': self.range}
156
+        if delta > 0:
157
+            return ngettext(
158
+                'Buy %(delta)d more product from %(range)s',
159
+                'Buy %(delta)d more products from %(range)s',
160
+                delta
161
+            ) % {'delta': delta, 'range': self.range}
160 162
 
161 163
     def is_partially_satisfied(self, offer, basket):
162 164
         return 0 < self._get_num_covered_products(basket, offer) < self.value
@@ -250,12 +252,9 @@ class ValueCondition(Condition):
250 252
             return getattr(self, '_value_of_matches')
251 253
         value_of_matches = D('0.00')
252 254
         for line in basket.all_lines():
253
-            if (self.can_apply_condition(line)
254
-                    and line.quantity_without_offer_discount(offer) > 0):
255
+            if self.can_apply_condition(line):
255 256
                 price = unit_price(offer, line)
256
-                value_of_matches += price * int(
257
-                    line.quantity_without_offer_discount(offer)
258
-                )
257
+                value_of_matches += price * int(line.quantity_available_for_offer(offer))
259 258
         self._value_of_matches = value_of_matches
260 259
         return value_of_matches
261 260
 
@@ -265,9 +264,12 @@ class ValueCondition(Condition):
265 264
 
266 265
     def get_upsell_message(self, offer, basket):
267 266
         value_of_matches = self._get_value_of_matches(offer, basket)
268
-        return _('Spend %(value)s more from %(range)s') % {
269
-            'value': currency(self.value - value_of_matches, basket.currency),
270
-            'range': self.range}
267
+        delta = self.value - value_of_matches
268
+        if delta > 0:
269
+            return _('Spend %(value)s more from %(range)s') % {
270
+                'value': currency(delta, basket.currency),
271
+                'range': self.range,
272
+            }
271 273
 
272 274
     def consume_items(self, offer, basket, affected_lines):
273 275
         """

+ 140
- 0
tests/integration/offer/test_upsell_messages.py 查看文件

@@ -0,0 +1,140 @@
1
+from decimal import Decimal as D
2
+
3
+from django.test.client import RequestFactory
4
+from django.urls import reverse
5
+from django.utils.timezone import now
6
+
7
+from oscar.apps.basket.views import BasketView
8
+from oscar.apps.offer.applicator import Applicator
9
+from oscar.core.loading import get_class
10
+from oscar.test import factories
11
+from oscar.test.testcases import WebTestCase
12
+
13
+Selector = get_class('partner.strategy', 'Selector')
14
+
15
+
16
+class TestUpsellMessages(WebTestCase):
17
+
18
+    def setUp(self):
19
+        super().setUp()
20
+
21
+        self.basket = factories.create_basket(empty=True)
22
+
23
+        # Create range and add one product to it.
24
+        rng = factories.RangeFactory(name='All products', includes_all_products=True)
25
+        self.product = factories.ProductFactory()
26
+        rng.add_product(self.product)
27
+
28
+        # Create offer #1.
29
+        condition1 = factories.ConditionFactory(
30
+            range=rng, type=factories.ConditionFactory._meta.model.COUNT, value=D('2'),
31
+        )
32
+        benefit1 = factories.BenefitFactory(
33
+            range=rng, type=factories.BenefitFactory._meta.model.MULTIBUY, value=None,
34
+        )
35
+        self.offer1 = factories.ConditionalOfferFactory(
36
+            condition=condition1, benefit=benefit1,
37
+            slug='offer-1',
38
+            start_datetime=now(),
39
+            name='Test offer #1',
40
+            priority=1,
41
+        )
42
+
43
+        # Create offer #2.
44
+        condition2 = factories.ConditionFactory(
45
+            range=rng, type=factories.ConditionFactory._meta.model.VALUE, value=D('1.99'),
46
+        )
47
+        benefit2 = factories.BenefitFactory(
48
+            range=rng, type=factories.BenefitFactory._meta.model.MULTIBUY, value=None,
49
+        )
50
+        self.offer2 = factories.ConditionalOfferFactory(
51
+            condition=condition2,
52
+            benefit=benefit2,
53
+            slug='offer-2',
54
+            start_datetime=now(),
55
+            name='Test offer #2',
56
+        )
57
+
58
+        # Create offer #3.
59
+        condition3 = factories.ConditionFactory(
60
+            range=rng, type=factories.ConditionFactory._meta.model.COVERAGE, value=1,
61
+        )
62
+        benefit3 = factories.BenefitFactory(
63
+            range=rng, type=factories.BenefitFactory._meta.model.MULTIBUY, value=None,
64
+        )
65
+        self.offer3 = factories.ConditionalOfferFactory(
66
+            condition=condition3,
67
+            benefit=benefit3,
68
+            slug='offer-3',
69
+            start_datetime=now(),
70
+            name='Test offer #3',
71
+        )
72
+
73
+        # Prepare `BasketView` to use `get_upsell_messages` method in tests.
74
+        self.view = BasketView()
75
+        self.view.request = RequestFactory().get(reverse('basket:summary'))
76
+        self.view.request.user = factories.UserFactory()
77
+        self.view.args = []
78
+        self.view.kwargs = {}
79
+
80
+    def add_product(self):
81
+        self.basket.add_product(self.product)
82
+        self.basket.strategy = Selector().strategy()
83
+        Applicator().apply(self.basket)
84
+
85
+        self.assertBasketUpsellMessagesAreNotNone()
86
+
87
+    def assertBasketUpsellMessagesAreNotNone(self):
88
+        messages = self.view.get_upsell_messages(self.basket)
89
+        for message_data in messages:
90
+            # E.g. message data:
91
+            # {
92
+            #     'message': 'Buy 1 more product from All products',
93
+            #     'offer': <ConditionalOffer: Test offer #1>
94
+            # }.
95
+            self.assertIsNotNone(message_data['message'])
96
+            self.assertIsNotNone(message_data['offer'])
97
+
98
+    def assertOffersApplied(self, offers):
99
+        applied_offers = self.basket.applied_offers()
100
+        self.assertEqual(len(offers), len(applied_offers))
101
+        for offer in offers:
102
+            self.assertIn(offer.id, applied_offers, msg=offer)
103
+
104
+    def test_upsell_messages(self):
105
+        # The basket is empty. No offers are applied.
106
+        self.assertEqual(self.offer1.get_upsell_message(self.basket), 'Buy 2 more products from All products')
107
+        self.assertEqual(self.offer2.get_upsell_message(self.basket), 'Spend £1.99 more from All products')
108
+        self.assertEqual(self.offer3.get_upsell_message(self.basket), 'Buy 1 more product from All products')
109
+
110
+        self.add_product()
111
+
112
+        # 1 product in the basket. Offer #2 is applied.
113
+        self.assertOffersApplied([self.offer2])
114
+        self.assertEqual(self.offer1.get_upsell_message(self.basket), 'Buy 1 more product from All products')
115
+        self.assertIsNone(self.offer2.get_upsell_message(self.basket))
116
+        self.assertEqual(self.offer3.get_upsell_message(self.basket), 'Buy 1 more product from All products')
117
+
118
+        self.add_product()
119
+
120
+        # 2 products in the basket. Offers #1 is applied.
121
+        self.assertOffersApplied([self.offer1])
122
+        self.assertIsNone(self.offer1.get_upsell_message(self.basket))
123
+        self.assertEqual(self.offer2.get_upsell_message(self.basket), 'Spend £1.99 more from All products')
124
+        self.assertEqual(self.offer3.get_upsell_message(self.basket), 'Buy 1 more product from All products')
125
+
126
+        self.add_product()
127
+
128
+        # 3 products in the basket. Offers #1 and #2 are applied.
129
+        self.assertOffersApplied([self.offer1, self.offer2])
130
+        self.assertIsNone(self.offer1.get_upsell_message(self.basket))
131
+        self.assertIsNone(self.offer2.get_upsell_message(self.basket))
132
+        self.assertEqual(self.offer3.get_upsell_message(self.basket), 'Buy 1 more product from All products')
133
+
134
+        self.add_product()
135
+
136
+        # 4 products in the basket. All offers are applied.
137
+        self.assertOffersApplied([self.offer1, self.offer2, self.offer3])
138
+        self.assertIsNone(self.offer1.get_upsell_message(self.basket))
139
+        self.assertIsNone(self.offer2.get_upsell_message(self.basket))
140
+        self.assertIsNone(self.offer3.get_upsell_message(self.basket))

Loading…
取消
儲存