Переглянути джерело

[FEAT] Absolute discount benefit applied on each product (#4168)

* feat :star: add benefit of fixed discount per product

* feat :star: check range and value in absolute product discount benefit form

* test :ballot_box_with_check: add tests for the new product level absolute discount benefit

* fix :wrench: resolve lint errors

* refactor :package: rename FIXED_PRODUCT to FIXED_UNIT

* refactor :package: rename AbsoluteProductDiscountBenefit to FixedUnitDiscountBenefit

* refactor :package: rename fixed unit benefit test file

* fix :wrench: clean fixed unit benefit form

* refactor :package: rename Fixed unit to Fixed

* feat :star: apply discount on all the product items

* refactor :package: rename test class names to FixedUnit

* fix :wrench: update tests for fixed unit benefit
master
Samar Hassan 2 роки тому
джерело
коміт
609e573617
Аккаунт користувача з таким Email не знайдено

+ 16
- 1
src/oscar/apps/offer/abstract_models.py Переглянути файл

@@ -537,9 +537,10 @@ class AbstractBenefit(BaseOfferMixin, models.Model):
537 537
     )
538 538
 
539 539
     # Benefit types
540
-    PERCENTAGE, FIXED, MULTIBUY, FIXED_PRICE = (
540
+    PERCENTAGE, FIXED, FIXED_UNIT, MULTIBUY, FIXED_PRICE = (
541 541
         "Percentage",
542 542
         "Absolute",
543
+        "Fixed",
543 544
         "Multibuy",
544 545
         "Fixed price",
545 546
     )
@@ -551,6 +552,7 @@ class AbstractBenefit(BaseOfferMixin, models.Model):
551 552
     TYPE_CHOICES = (
552 553
         (PERCENTAGE, _("Discount is a percentage off of the product's value")),
553 554
         (FIXED, _("Discount is a fixed amount off of the basket's total")),
555
+        (FIXED_UNIT, _("Discount is a fixed amount off of the product's value")),
554 556
         (MULTIBUY, _("Discount is to give the cheapest product for free")),
555 557
         (FIXED_PRICE, _("Get the products that meet the condition for a fixed price")),
556 558
         (SHIPPING_ABSOLUTE, _("Discount is a fixed amount of the shipping cost")),
@@ -596,6 +598,7 @@ class AbstractBenefit(BaseOfferMixin, models.Model):
596 598
         return {
597 599
             self.PERCENTAGE: get_class("offer.benefits", "PercentageDiscountBenefit"),
598 600
             self.FIXED: get_class("offer.benefits", "AbsoluteDiscountBenefit"),
601
+            self.FIXED_UNIT: get_class("offer.benefits", "FixedUnitDiscountBenefit"),
599 602
             self.MULTIBUY: get_class("offer.benefits", "MultibuyDiscountBenefit"),
600 603
             self.FIXED_PRICE: get_class("offer.benefits", "FixedPriceBenefit"),
601 604
             self.SHIPPING_ABSOLUTE: get_class(
@@ -737,6 +740,18 @@ class AbstractBenefit(BaseOfferMixin, models.Model):
737 740
         if errors:
738 741
             raise exceptions.ValidationError(errors)
739 742
 
743
+    def clean_fixed(self):
744
+        errors = []
745
+        if not self.range:
746
+            errors.append(
747
+                _("Fixed product level discount benefits require a product range")
748
+            )
749
+        if not self.value:
750
+            errors.append(_("Fixed product level discount benefits require a value"))
751
+
752
+        if errors:
753
+            raise exceptions.ValidationError(errors)
754
+
740 755
     def round(self, amount, currency=None):
741 756
         """
742 757
         Apply rounding to discount amount

+ 51
- 0
src/oscar/apps/offer/benefits.py Переглянути файл

@@ -20,6 +20,7 @@ range_anchor = get_class("offer.utils", "range_anchor")
20 20
 __all__ = [
21 21
     "PercentageDiscountBenefit",
22 22
     "AbsoluteDiscountBenefit",
23
+    "FixedUnitDiscountBenefit",
23 24
     "FixedPriceBenefit",
24 25
     "ShippingBenefit",
25 26
     "MultibuyDiscountBenefit",
@@ -203,6 +204,56 @@ class AbsoluteDiscountBenefit(Benefit):
203 204
         return BasketDiscount(discount)
204 205
 
205 206
 
207
+class FixedUnitDiscountBenefit(AbsoluteDiscountBenefit):
208
+    """
209
+    An offer benefit that gives an absolute discount on each applicable product.
210
+    """
211
+
212
+    class Meta:
213
+        app_label = "offer"
214
+        proxy = True
215
+        verbose_name = _("Fixed unit discount benefit")
216
+        verbose_name_plural = _("Fixed unit discount benefits")
217
+
218
+    def get_lines_to_discount(self, offer, line_tuples):
219
+        # Determine which lines can have the discount applied to them
220
+        max_affected_items = self._effective_max_affected_items()
221
+        num_affected_items = 0
222
+        lines_to_discount = []
223
+        for price, line in line_tuples:
224
+            if num_affected_items >= max_affected_items:
225
+                break
226
+            qty = min(
227
+                line.quantity_without_offer_discount(offer),
228
+                max_affected_items - num_affected_items,
229
+            )
230
+            lines_to_discount.append((line, price, qty))
231
+            num_affected_items += qty
232
+        return lines_to_discount
233
+
234
+    def apply(
235
+        self,
236
+        basket,
237
+        condition,
238
+        offer,
239
+        discount_amount=None,
240
+        max_total_discount=None,
241
+        **kwargs
242
+    ):
243
+        # Fetch basket lines that are in the range and available to be used in an offer.
244
+        line_tuples = self.get_applicable_lines(offer, basket)
245
+        lines_to_discount = self.get_lines_to_discount(offer, line_tuples)
246
+
247
+        applied_discount = D("0.00")
248
+        for line, price, qty in lines_to_discount:
249
+            # If price is less than the fixed discount, then it will be free.
250
+            line_discount = min(price * qty, self.value * qty)
251
+            apply_discount(line, line_discount, qty, offer)
252
+            applied_discount += line_discount
253
+
254
+        return BasketDiscount(applied_discount)
255
+
256
+
206 257
 class FixedPriceBenefit(Benefit):
207 258
     """
208 259
     An offer benefit that gives the items in the condition for a

+ 58
- 0
src/oscar/apps/offer/migrations/0012_fixedunitdiscountbenefit_alter_benefit_type.py Переглянути файл

@@ -0,0 +1,58 @@
1
+# Generated by Django 4.2.5 on 2023-09-15 11:09
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+    dependencies = [
8
+        ("offer", "0011_rangeproductfileupload_included"),
9
+    ]
10
+
11
+    operations = [
12
+        migrations.CreateModel(
13
+            name="FixedUnitDiscountBenefit",
14
+            fields=[],
15
+            options={
16
+                "verbose_name": "Fixed unit discount benefit",
17
+                "verbose_name_plural": "Fixed unit discount benefits",
18
+                "proxy": True,
19
+                "indexes": [],
20
+                "constraints": [],
21
+            },
22
+            bases=("offer.absolutediscountbenefit",),
23
+        ),
24
+        migrations.AlterField(
25
+            model_name="benefit",
26
+            name="type",
27
+            field=models.CharField(
28
+                blank=True,
29
+                choices=[
30
+                    (
31
+                        "Percentage",
32
+                        "Discount is a percentage off of the product's value",
33
+                    ),
34
+                    (
35
+                        "Absolute",
36
+                        "Discount is a fixed amount off of the basket's total",
37
+                    ),
38
+                    ("Fixed", "Discount is a fixed amount off of the product's value"),
39
+                    ("Multibuy", "Discount is to give the cheapest product for free"),
40
+                    (
41
+                        "Fixed price",
42
+                        "Get the products that meet the condition for a fixed price",
43
+                    ),
44
+                    (
45
+                        "Shipping absolute",
46
+                        "Discount is a fixed amount of the shipping cost",
47
+                    ),
48
+                    ("Shipping fixed price", "Get shipping for a fixed price"),
49
+                    (
50
+                        "Shipping percentage",
51
+                        "Discount is a percentage off of the shipping cost",
52
+                    ),
53
+                ],
54
+                max_length=128,
55
+                verbose_name="Type",
56
+            ),
57
+        ),
58
+    ]

+ 368
- 0
tests/integration/offer/test_fixed_unit_benefit.py Переглянути файл

@@ -0,0 +1,368 @@
1
+from decimal import Decimal as D
2
+from unittest import mock
3
+
4
+from django.core.exceptions import ValidationError
5
+from django.test import TestCase
6
+
7
+from oscar.apps.offer import models
8
+from oscar.apps.offer.utils import Applicator
9
+from oscar.test import factories
10
+from oscar.test.basket import add_product, add_products
11
+
12
+
13
+class TestFixedUnitDiscountAppliedWithCountConditionOnDifferentRange(TestCase):
14
+    def setUp(self):
15
+        self.condition_product = factories.ProductFactory()
16
+        condition_range = factories.RangeFactory()
17
+        condition_range.add_product(self.condition_product)
18
+        self.condition = models.CountCondition.objects.create(
19
+            range=condition_range, type=models.Condition.COUNT, value=2
20
+        )
21
+
22
+        self.benefit_product = factories.ProductFactory()
23
+        benefit_range = factories.RangeFactory()
24
+        benefit_range.add_product(self.benefit_product)
25
+        self.benefit = models.FixedUnitDiscountBenefit.objects.create(
26
+            range=benefit_range, type=models.Benefit.FIXED_UNIT, value=D("3.00")
27
+        )
28
+
29
+        self.offer = models.ConditionalOffer(
30
+            id=1, condition=self.condition, benefit=self.benefit
31
+        )
32
+        self.basket = factories.create_basket(empty=True)
33
+
34
+        self.applicator = Applicator()
35
+
36
+    def test_succcessful_application_consumes_correctly(self):
37
+        add_product(self.basket, product=self.condition_product, quantity=2)
38
+        add_product(self.basket, product=self.benefit_product, quantity=1)
39
+
40
+        self.applicator.apply_offers(self.basket, [self.offer])
41
+
42
+        discounts = self.basket.offer_applications.offer_discounts
43
+        self.assertEqual(len(discounts), 1)
44
+        self.assertEqual(discounts[0]["freq"], 1)
45
+
46
+    def test_condition_is_consumed_correctly(self):
47
+        # Testing an error case reported on the mailing list
48
+        add_product(self.basket, product=self.condition_product, quantity=3)
49
+        add_product(self.basket, product=self.benefit_product, quantity=2)
50
+
51
+        self.applicator.apply_offers(self.basket, [self.offer])
52
+
53
+        discounts = self.basket.offer_applications.offer_discounts
54
+        self.assertEqual(len(discounts), 1)
55
+        self.assertEqual(discounts[0]["freq"], 1)
56
+
57
+
58
+class TestFixedUnitDiscountAppliedWithCountCondition(TestCase):
59
+    def setUp(self):
60
+        product_range = models.Range.objects.create(
61
+            name="All products", includes_all_products=True
62
+        )
63
+        self.condition = models.CountCondition.objects.create(
64
+            range=product_range, type=models.Condition.COUNT, value=2
65
+        )
66
+        self.offer = mock.Mock()
67
+        self.benefit = models.FixedUnitDiscountBenefit.objects.create(
68
+            range=product_range, type=models.Benefit.FIXED_UNIT, value=D("3.00")
69
+        )
70
+        self.basket = factories.create_basket(empty=True)
71
+
72
+    def test_applies_correctly_to_empty_basket(self):
73
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
74
+        self.assertEqual(D("0.00"), result.discount)
75
+        self.assertEqual(0, self.basket.num_items_with_discount)
76
+        self.assertEqual(0, self.basket.num_items_without_discount)
77
+
78
+    def test_applies_correctly_to_basket_which_matches_condition_with_one_line(self):
79
+        add_product(self.basket, price=D("12.00"), quantity=2)
80
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
81
+        self.assertEqual(D("6.00"), result.discount)
82
+        self.assertEqual(2, self.basket.num_items_with_discount)
83
+        self.assertEqual(0, self.basket.num_items_without_discount)
84
+
85
+    def test_applies_correctly_to_basket_which_matches_condition_with_multiple_lines(
86
+        self,
87
+    ):
88
+        # Use a basket with 2 lines
89
+        add_products(self.basket, [(D("12.00"), 1), (D("12.00"), 1)])
90
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
91
+
92
+        self.assertTrue(result.is_successful)
93
+        self.assertFalse(result.is_final)
94
+        self.assertEqual(D("6.00"), result.discount)
95
+        self.assertEqual(2, self.basket.num_items_with_discount)
96
+        self.assertEqual(0, self.basket.num_items_without_discount)
97
+
98
+    def test_applies_correctly_to_basket_which_matches_condition_with_multiple_lines_and_lower_total_value(
99
+        self,
100
+    ):
101
+        # Use a basket with 2 lines
102
+        add_products(self.basket, [(D("1.00"), 1), (D("1.50"), 1)])
103
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
104
+
105
+        self.assertTrue(result.is_successful)
106
+        self.assertFalse(result.is_final)
107
+        self.assertEqual(D("2.50"), result.discount)
108
+        self.assertEqual(2, self.basket.num_items_with_discount)
109
+        self.assertEqual(0, self.basket.num_items_without_discount)
110
+
111
+    def test_applies_correctly_to_basket_which_exceeds_condition(self):
112
+        add_products(self.basket, [(D("12.00"), 2), (D("10.00"), 2)])
113
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
114
+        self.assertEqual(D("12.00"), result.discount)
115
+        self.assertEqual(4, self.basket.num_items_with_discount)
116
+        self.assertEqual(0, self.basket.num_items_without_discount)
117
+
118
+    def test_applies_correctly_to_basket_which_exceeds_condition_with_smaller_prices_than_discount(
119
+        self,
120
+    ):
121
+        add_products(self.basket, [(D("2.00"), 2), (D("4.00"), 1)])
122
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
123
+        self.assertEqual(D("7.00"), result.discount)
124
+        self.assertEqual(3, self.basket.num_items_with_discount)
125
+        self.assertEqual(0, self.basket.num_items_without_discount)
126
+
127
+    def test_applies_basket_exceeding_condition_smaller_prices_than_discount_higher_prices_first(
128
+        self,
129
+    ):
130
+        add_products(self.basket, [(D("2.00"), 2), (D("4.00"), 2)])
131
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
132
+        self.assertEqual(D("10.00"), result.discount)
133
+        self.assertEqual(4, self.basket.num_items_with_discount)
134
+        self.assertEqual(0, self.basket.num_items_without_discount)
135
+
136
+
137
+class TestFixedUnitDiscount(TestCase):
138
+    def setUp(self):
139
+        product_range = models.Range.objects.create(
140
+            name="All products", includes_all_products=True
141
+        )
142
+        self.condition = models.CountCondition.objects.create(
143
+            range=product_range, type=models.Condition.COUNT, value=2
144
+        )
145
+        self.benefit = models.FixedUnitDiscountBenefit.objects.create(
146
+            range=product_range, type=models.Benefit.FIXED_UNIT, value=D("4.00")
147
+        )
148
+        self.offer = mock.Mock()
149
+        self.basket = factories.create_basket(empty=True)
150
+
151
+    def test_applies_correctly_when_discounts_need_rounding(self):
152
+        # Split discount across 3 lines
153
+        for price in [D("2.00"), D("2.00"), D("2.00")]:
154
+            add_product(self.basket, price)
155
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
156
+
157
+        self.assertEqual(D("6.00"), result.discount)
158
+
159
+
160
+class TestFixedUnitDiscountWithMaxItemsSetAppliedWithCountCondition(TestCase):
161
+    def setUp(self):
162
+        product_range = models.Range.objects.create(
163
+            name="All products", includes_all_products=True
164
+        )
165
+        self.condition = models.CountCondition.objects.create(
166
+            range=product_range, type=models.Condition.COUNT, value=2
167
+        )
168
+        self.benefit = models.FixedUnitDiscountBenefit.objects.create(
169
+            range=product_range,
170
+            type=models.Benefit.FIXED_UNIT,
171
+            value=D("3.00"),
172
+            max_affected_items=1,
173
+        )
174
+        self.offer = mock.Mock()
175
+        self.basket = factories.create_basket(empty=True)
176
+
177
+    def test_applies_correctly_to_empty_basket(self):
178
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
179
+        self.assertEqual(D("0.00"), result.discount)
180
+        self.assertEqual(0, self.basket.num_items_with_discount)
181
+        self.assertEqual(0, self.basket.num_items_without_discount)
182
+
183
+    def test_applies_correctly_to_basket_which_matches_condition(self):
184
+        add_product(self.basket, D("12.00"), 2)
185
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
186
+        self.assertEqual(D("3.00"), result.discount)
187
+        self.assertEqual(1, self.basket.num_items_with_discount)
188
+        self.assertEqual(1, self.basket.num_items_without_discount)
189
+
190
+    def test_applies_correctly_to_basket_which_exceeds_condition(self):
191
+        add_products(self.basket, [(D("12.00"), 2), (D("10.00"), 2)])
192
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
193
+        self.assertEqual(D("3.00"), result.discount)
194
+        self.assertEqual(1, self.basket.num_items_with_discount)
195
+        self.assertEqual(3, self.basket.num_items_without_discount)
196
+
197
+    def test_applies_correctly_to_basket_which_exceeds_condition_but_with_smaller_prices_than_discount(
198
+        self,
199
+    ):
200
+        add_products(self.basket, [(D("2.00"), 2), (D("1.00"), 2)])
201
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
202
+        self.assertEqual(D("1.00"), result.discount)
203
+        self.assertEqual(1, self.basket.num_items_with_discount)
204
+        self.assertEqual(3, self.basket.num_items_without_discount)
205
+
206
+
207
+class TestFixedUnitDiscountAppliedWithValueCondition(TestCase):
208
+    def setUp(self):
209
+        product_range = models.Range.objects.create(
210
+            name="All products", includes_all_products=True
211
+        )
212
+        self.condition = models.ValueCondition.objects.create(
213
+            range=product_range, type=models.Condition.VALUE, value=D("10.00")
214
+        )
215
+        self.benefit = models.FixedUnitDiscountBenefit.objects.create(
216
+            range=product_range, type=models.Benefit.FIXED_UNIT, value=D("3.00")
217
+        )
218
+        self.offer = mock.Mock()
219
+        self.basket = factories.create_basket(empty=True)
220
+
221
+    def test_applies_correctly_to_empty_basket(self):
222
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
223
+        self.assertEqual(D("0.00"), result.discount)
224
+        self.assertEqual(0, self.basket.num_items_with_discount)
225
+        self.assertEqual(0, self.basket.num_items_without_discount)
226
+
227
+    def test_applies_correctly_to_single_item_basket_which_matches_condition(self):
228
+        add_products(self.basket, [(D("10.00"), 1)])
229
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
230
+        self.assertEqual(D("3.00"), result.discount)
231
+        self.assertEqual(1, self.basket.num_items_with_discount)
232
+        self.assertEqual(0, self.basket.num_items_without_discount)
233
+
234
+    def test_applies_correctly_to_multi_item_basket_which_matches_condition(self):
235
+        add_products(self.basket, [(D("5.00"), 2)])
236
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
237
+        self.assertEqual(D("6.00"), result.discount)
238
+        self.assertEqual(2, self.basket.num_items_with_discount)
239
+        self.assertEqual(0, self.basket.num_items_without_discount)
240
+
241
+    def test_applies_correctly_to_multi_item_basket_which_exceeds_condition(self):
242
+        add_products(self.basket, [(D("4.00"), 3)])
243
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
244
+        self.assertEqual(D("9.00"), result.discount)
245
+        self.assertEqual(3, self.basket.num_items_with_discount)
246
+        self.assertEqual(0, self.basket.num_items_without_discount)
247
+
248
+    def test_applies_correctly_to_multi_item_basket_which_exceeds_condition_but_matches_boundary(
249
+        self,
250
+    ):
251
+        add_products(self.basket, [(D("5.00"), 3)])
252
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
253
+        self.assertEqual(D("9.00"), result.discount)
254
+        self.assertEqual(3, self.basket.num_items_with_discount)
255
+        self.assertEqual(0, self.basket.num_items_without_discount)
256
+
257
+
258
+class TestFixedUnitDiscountWithMaxItemsSetAppliedWithValueCondition(TestCase):
259
+    def setUp(self):
260
+        product_range = models.Range.objects.create(
261
+            name="All products", includes_all_products=True
262
+        )
263
+        self.condition = models.ValueCondition.objects.create(
264
+            range=product_range, type=models.Condition.VALUE, value=D("10.00")
265
+        )
266
+        self.benefit = models.FixedUnitDiscountBenefit.objects.create(
267
+            range=product_range,
268
+            type=models.Benefit.FIXED_UNIT,
269
+            value=D("3.00"),
270
+            max_affected_items=1,
271
+        )
272
+        self.offer = mock.Mock()
273
+        self.basket = factories.create_basket(empty=True)
274
+
275
+    def test_applies_correctly_to_empty_basket(self):
276
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
277
+        self.assertEqual(D("0.00"), result.discount)
278
+        self.assertEqual(0, self.basket.num_items_with_discount)
279
+        self.assertEqual(0, self.basket.num_items_without_discount)
280
+
281
+    def test_applies_correctly_to_single_item_basket_which_matches_condition(self):
282
+        add_products(self.basket, [(D("10.00"), 1)])
283
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
284
+        self.assertEqual(D("3.00"), result.discount)
285
+        self.assertEqual(1, self.basket.num_items_with_discount)
286
+        self.assertEqual(0, self.basket.num_items_without_discount)
287
+
288
+    def test_applies_correctly_to_multi_item_basket_which_matches_condition(self):
289
+        add_products(self.basket, [(D("5.00"), 2)])
290
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
291
+        self.assertEqual(D("3.00"), result.discount)
292
+        self.assertEqual(1, self.basket.num_items_with_discount)
293
+        self.assertEqual(1, self.basket.num_items_without_discount)
294
+
295
+    def test_applies_correctly_to_multi_item_basket_which_exceeds_condition(self):
296
+        add_products(self.basket, [(D("4.00"), 3)])
297
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
298
+        self.assertEqual(D("3.00"), result.discount)
299
+        self.assertEqual(1, self.basket.num_items_with_discount)
300
+        self.assertEqual(2, self.basket.num_items_without_discount)
301
+
302
+    def test_applies_correctly_to_multi_item_basket_which_exceeds_condition_but_matches_boundary(
303
+        self,
304
+    ):
305
+        add_products(self.basket, [(D("5.00"), 3)])
306
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
307
+        self.assertEqual(D("3.00"), result.discount)
308
+        self.assertEqual(1, self.basket.num_items_with_discount)
309
+        self.assertEqual(2, self.basket.num_items_without_discount)
310
+
311
+    def test_applies_correctly_to_multi_item_basket_which_matches_condition_but_with_lower_prices_than_discount(
312
+        self,
313
+    ):
314
+        add_products(self.basket, [(D("2.00"), 6)])
315
+        result = self.benefit.apply(self.basket, self.condition, self.offer)
316
+        self.assertEqual(D("2.00"), result.discount)
317
+        self.assertEqual(1, self.basket.num_items_with_discount)
318
+        self.assertEqual(5, self.basket.num_items_without_discount)
319
+
320
+
321
+class TestFixedUnitDiscountBenefit(TestCase):
322
+    def test_requires_a_benefit_value(self):
323
+        rng = models.Range.objects.create(name="", includes_all_products=True)
324
+        benefit = models.Benefit(type=models.Benefit.FIXED_UNIT, range=rng)
325
+        with self.assertRaises(ValidationError):
326
+            benefit.clean()
327
+
328
+    def test_requires_a_range(self):
329
+        benefit = models.Benefit(type=models.Benefit.FIXED_UNIT, value=10)
330
+        with self.assertRaises(ValidationError):
331
+            benefit.clean()
332
+
333
+    def test_non_negative_basket_lines_values(self):
334
+        # absolute product benefit is larger than the line price
335
+        rng = models.Range.objects.create(name="", includes_all_products=True)
336
+        benefit1 = models.Benefit.objects.create(
337
+            type=models.Benefit.FIXED_UNIT, range=rng, value=D("100")
338
+        )
339
+        benefit2 = models.Benefit.objects.create(
340
+            type=models.Benefit.FIXED_UNIT, range=rng, value=D("100")
341
+        )
342
+        condition = models.ValueCondition.objects.create(
343
+            range=rng, type=models.Condition.VALUE, value=D("10")
344
+        )
345
+        models.ConditionalOffer.objects.create(
346
+            name="offer1",
347
+            offer_type=models.ConditionalOffer.SITE,
348
+            benefit=benefit1,
349
+            condition=condition,
350
+            exclusive=False,
351
+        )
352
+        models.ConditionalOffer.objects.create(
353
+            name="offer2",
354
+            offer_type=models.ConditionalOffer.SITE,
355
+            benefit=benefit2,
356
+            condition=condition,
357
+            exclusive=False,
358
+        )
359
+
360
+        basket = factories.create_basket(empty=True)
361
+        add_products(basket, [(D("20"), 1)])
362
+
363
+        Applicator().apply(basket)
364
+        assert len(basket.offer_applications) == 2
365
+        line = basket.all_lines().first()
366
+        assert line.line_price_excl_tax_incl_discounts == D(0)
367
+        assert line.line_price_incl_tax_incl_discounts == D(0)
368
+        assert basket.total_incl_tax == 0

Завантаження…
Відмінити
Зберегти