Bläddra i källkod

If a product has a option it cannot be oversold (#4096)

* if a product has a option it cannot be oversold

* Improve error message

---------

Co-authored-by: wessel <wessel@highbiza.nl>
Co-authored-by: Lars van de Kerkhof <lars@permanentmarkers.nl>
master
Joey 2 år sedan
förälder
incheckning
b39920547d
Inget konto är kopplat till bidragsgivarens mejladress

+ 1
- 1
.github/workflows/test.yml Visa fil

@@ -52,7 +52,7 @@ jobs:
52 52
       run: |
53 53
         coverage run --parallel -m pytest -x
54 54
     - name: Upload coverage to Codecov
55
-      uses: codecov/codecov-action@v1
55
+      uses: codecov/codecov-action@v1.5.2
56 56
       with:
57 57
         fail_ci_if_error: true
58 58
   lint_python:

+ 28
- 1
src/oscar/apps/basket/abstract_models.py Visa fil

@@ -148,20 +148,47 @@ class AbstractBasket(models.Model):
148 148
             return max_allowed, basket_threshold
149 149
         return None, None
150 150
 
151
-    def is_quantity_allowed(self, qty):
151
+    def is_quantity_allowed(self, qty, line=None):
152 152
         """
153 153
         Test whether the passed quantity of items can be added to the basket
154 154
         """
155 155
         # We enforce a max threshold to prevent a DOS attack via the offers
156 156
         # system.
157 157
         max_allowed, basket_threshold = self.max_allowed_quantity()
158
+
159
+        if line is not None:
160
+            line_purchase_permitted, reason = line.purchase_info.availability.is_purchase_permitted(qty)
161
+
162
+            if not line_purchase_permitted:
163
+                return line_purchase_permitted, reason
164
+
165
+            # Also check if it's permitted with potentional other lines of the same product & stocrecord
166
+            total_lines_quantity = self.basket_quantity(line) + qty
167
+            line_purchase_permitted, reason = line.purchase_info.availability.is_purchase_permitted(
168
+                total_lines_quantity)
169
+
170
+            if not line_purchase_permitted:
171
+                return line_purchase_permitted, _(
172
+                    "Available stock is only %(max)d, which has been exceeded because "
173
+                    "multiple lines contain the same product."
174
+                ) % {'max': line.purchase_info.availability.num_available}
175
+
158 176
         if max_allowed is not None and qty > max_allowed:
159 177
             return False, _(
160 178
                 "Due to technical limitations we are not able "
161 179
                 "to ship more than %(threshold)d items in one order.") \
162 180
                 % {'threshold': basket_threshold}
181
+
163 182
         return True, None
164 183
 
184
+    def basket_quantity(self, line):
185
+        """Return the quantity of similar lines in the basket.
186
+        The basket can contain multiple lines with the same product and
187
+        stockrecord, but different options. Those quantities are summed up.
188
+        """
189
+        matching_lines = self.lines.filter(stockrecord=line.stockrecord)
190
+        quantity = matching_lines.aggregate(Sum('quantity'))['quantity__sum']
191
+        return quantity or 0
165 192
     # ============
166 193
     # Manipulation
167 194
     # ============

+ 1
- 1
src/oscar/apps/basket/forms.py Visa fil

@@ -126,7 +126,7 @@ class BasketLineForm(forms.ModelForm):
126 126
         # number and updated. Thus, product already in the basket and we don't
127 127
         # add second time, just updating number of items.
128 128
         qty_delta = qty - self.instance.quantity
129
-        is_allowed, reason = self.instance.basket.is_quantity_allowed(qty_delta)
129
+        is_allowed, reason = self.instance.basket.is_quantity_allowed(qty_delta, line=self.instance)
130 130
         if not is_allowed:
131 131
             raise forms.ValidationError(reason)
132 132
 

+ 37
- 1
tests/functional/test_basket.py Visa fil

@@ -16,7 +16,7 @@ from oscar.apps.partner import strategy
16 16
 from oscar.core.compat import get_user_model
17 17
 from oscar.test import factories
18 18
 from oscar.test.basket import add_product
19
-from oscar.test.factories import create_product
19
+from oscar.test.factories import OptionFactory, create_product
20 20
 from oscar.test.testcases import WebTestCase
21 21
 
22 22
 User = get_user_model()
@@ -482,3 +482,39 @@ class BasketFormSetTests(WebTestCase):
482 482
         self.assertFalse(response.context['formset'].forms[0].is_valid())
483 483
         self.assertEqual(basket.lines.count(), 1)
484 484
         self.assertEqual(basket.lines.all()[0].quantity, 1)
485
+
486
+    def test_formset_quantity_update_with_options(self):
487
+        product = create_product(num_in_stock=2)
488
+        option = OptionFactory()
489
+        # Add the option to the product class
490
+        product.get_product_class().options.add(option)
491
+
492
+        basket = factories.create_basket(empty=True)
493
+        basket.owner = self.user
494
+        basket.save()
495
+        basket.add_product(product, options=[{"option": option, "value": "Test 1"}])
496
+        basket.add_product(product, options=[{"option": option, "value": "Test 2"}])
497
+
498
+        response = self.get(reverse('basket:summary'))
499
+        formset = response.context['formset']
500
+        self.assertEqual(len(formset.forms), 2)
501
+
502
+        # Now update one of the quantities to 2
503
+        data = {
504
+            formset.add_prefix('TOTAL_FORMS'): formset.management_form.initial['TOTAL_FORMS'],
505
+            formset.add_prefix('INITIAL_FORMS'): formset.management_form.initial['INITIAL_FORMS'],
506
+            formset.add_prefix('MIN_NUM_FORMS'): formset.management_form.initial['MIN_NUM_FORMS'],
507
+            formset.add_prefix('MAX_NUM_FORMS'): formset.management_form.initial['MAX_NUM_FORMS'],
508
+            formset.forms[0].add_prefix('id'): formset.forms[0].instance.pk,
509
+            formset.forms[0].add_prefix('quantity'): 2,
510
+            formset.forms[1].add_prefix('id'): formset.forms[1].instance.pk,
511
+            formset.forms[1].add_prefix('quantity'): formset.forms[1].instance.quantity,
512
+        }
513
+        response = self.post(reverse('basket:summary'), params=data, xhr=True)
514
+
515
+        self.assertEqual(response.status_code, 200)
516
+        self.assertFalse(response.context['formset'].forms[0].is_valid())
517
+        self.assertIn(
518
+            "can be bought which has been exceeded because you have multiple lines of the same product.",
519
+            str(response.context['formset'].forms[0].errors)
520
+        )

+ 20
- 0
tests/integration/basket/test_forms.py Visa fil

@@ -138,6 +138,26 @@ class TestBasketLineForm(TestCase):
138 138
             queryset=self.basket.all_lines())
139 139
         self.assertTrue(formset.forms[0].instance.has_discount)
140 140
 
141
+    def test_max_allowed_quantity_with_options(self):
142
+        self.basket.flush()
143
+
144
+        option = OptionFactory(required=False)
145
+        product = factories.create_product(num_in_stock=2)
146
+        product.get_product_class().options.add(option)
147
+        self.basket.add_product(product, options=[{"option": option, "value": "Test 1"}])
148
+        self.basket.add_product(product, options=[{"option": option, "value": "Test 2"}])
149
+
150
+        form = forms.BasketLineForm(
151
+            strategy=self.basket.strategy,
152
+            data={'quantity': 2},
153
+            instance=self.basket.all_lines()[0]
154
+        )
155
+        self.assertFalse(form.is_valid())
156
+        self.assertIn(
157
+            "can be bought which has been exceeded because you have multiple lines of the same product.",
158
+            str(form.errors)
159
+        )
160
+
141 161
 
142 162
 class TestAddToBasketForm(TestCase):
143 163
 

Laddar…
Avbryt
Spara