Bladeren bron

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 jaren geleden
bovenliggende
commit
b39920547d
No account linked to committer's email address

+ 1
- 1
.github/workflows/test.yml Bestand weergeven

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

+ 28
- 1
src/oscar/apps/basket/abstract_models.py Bestand weergeven

148
             return max_allowed, basket_threshold
148
             return max_allowed, basket_threshold
149
         return None, None
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
         Test whether the passed quantity of items can be added to the basket
153
         Test whether the passed quantity of items can be added to the basket
154
         """
154
         """
155
         # We enforce a max threshold to prevent a DOS attack via the offers
155
         # We enforce a max threshold to prevent a DOS attack via the offers
156
         # system.
156
         # system.
157
         max_allowed, basket_threshold = self.max_allowed_quantity()
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
         if max_allowed is not None and qty > max_allowed:
176
         if max_allowed is not None and qty > max_allowed:
159
             return False, _(
177
             return False, _(
160
                 "Due to technical limitations we are not able "
178
                 "Due to technical limitations we are not able "
161
                 "to ship more than %(threshold)d items in one order.") \
179
                 "to ship more than %(threshold)d items in one order.") \
162
                 % {'threshold': basket_threshold}
180
                 % {'threshold': basket_threshold}
181
+
163
         return True, None
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
     # Manipulation
193
     # Manipulation
167
     # ============
194
     # ============

+ 1
- 1
src/oscar/apps/basket/forms.py Bestand weergeven

126
         # number and updated. Thus, product already in the basket and we don't
126
         # number and updated. Thus, product already in the basket and we don't
127
         # add second time, just updating number of items.
127
         # add second time, just updating number of items.
128
         qty_delta = qty - self.instance.quantity
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
         if not is_allowed:
130
         if not is_allowed:
131
             raise forms.ValidationError(reason)
131
             raise forms.ValidationError(reason)
132
 
132
 

+ 37
- 1
tests/functional/test_basket.py Bestand weergeven

16
 from oscar.core.compat import get_user_model
16
 from oscar.core.compat import get_user_model
17
 from oscar.test import factories
17
 from oscar.test import factories
18
 from oscar.test.basket import add_product
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
 from oscar.test.testcases import WebTestCase
20
 from oscar.test.testcases import WebTestCase
21
 
21
 
22
 User = get_user_model()
22
 User = get_user_model()
482
         self.assertFalse(response.context['formset'].forms[0].is_valid())
482
         self.assertFalse(response.context['formset'].forms[0].is_valid())
483
         self.assertEqual(basket.lines.count(), 1)
483
         self.assertEqual(basket.lines.count(), 1)
484
         self.assertEqual(basket.lines.all()[0].quantity, 1)
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 Bestand weergeven

138
             queryset=self.basket.all_lines())
138
             queryset=self.basket.all_lines())
139
         self.assertTrue(formset.forms[0].instance.has_discount)
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
 class TestAddToBasketForm(TestCase):
162
 class TestAddToBasketForm(TestCase):
143
 
163
 

Laden…
Annuleren
Opslaan