diff --git a/la_chariotte/order/models.py b/la_chariotte/order/models.py
index 31748a3..70bc93b 100644
--- a/la_chariotte/order/models.py
+++ b/la_chariotte/order/models.py
@@ -31,6 +31,11 @@ class GroupedOrder(models.Model):
self.total_price = price
self.save()
+ def compute_items_ordered_nb(self):
+ """Updates the ordered_nb of all items fo the grouped_order"""
+ for item in self.item_set.all():
+ item.compute_ordered_nb()
+
def is_ongoing(self):
"""Returns True if the grouped order is open for new Orders - False if it's too late"""
return self.deadline >= timezone.now()
@@ -130,6 +135,13 @@ class Item(models.Model):
"""Returns the price of all orders on this item"""
return self.price * self.ordered_nb
+ def get_remaining_nb(self):
+ """Returns the number of remaining articles for this item in this grouped order"""
+ if self.max_limit:
+ return self.max_limit - self.ordered_nb
+ else:
+ return None
+
def get_absolute_url(self):
return reverse("order:manage_items", kwargs={"pk": self.grouped_order.pk})
diff --git a/la_chariotte/order/templates/order/grouped_order_add_items.html b/la_chariotte/order/templates/order/grouped_order_add_items.html
index 41739cd..654e8c4 100644
--- a/la_chariotte/order/templates/order/grouped_order_add_items.html
+++ b/la_chariotte/order/templates/order/grouped_order_add_items.html
@@ -26,7 +26,7 @@
{% endfor %}
diff --git a/la_chariotte/order/tests/test_views.py b/la_chariotte/order/tests/test_views.py
index 869bae0..a753116 100644
--- a/la_chariotte/order/tests/test_views.py
+++ b/la_chariotte/order/tests/test_views.py
@@ -232,7 +232,7 @@ class TestGroupedOrderDetailView:
orga_user=other_user,
)
item = models.Item.objects.create(
- name="test item 1", grouped_order=grouped_order, price=1
+ name="test item 1", grouped_order=grouped_order, price=1, max_limit=20
)
item2 = models.Item.objects.create(
name="test item 2", grouped_order=grouped_order, price=5
@@ -247,6 +247,7 @@ class TestGroupedOrderDetailView:
assert response.status_code == 200
assert "test item" in response.content.decode()
assert "gr order test" in response.content.decode()
+ assert "20 disponibles" in response.content.decode()
assert item.ordered_nb == 0
assert item2.ordered_nb == 0
order_url = reverse(
@@ -286,11 +287,13 @@ class TestGroupedOrderDetailView:
assert order.ordered_items.count() == 2
assert order.articles_nb == 5
assert order.price == 9
+ assert item.get_remaining_nb() == 16
def test_order_item__no_articles_ordered(self, client, other_user):
"""
From the OrderDetailView, we order without having changed any item quantity.
An error is raised.
+ The order is deleted
"""
grouped_order = create_grouped_order(
days_before_delivery_date=5,
@@ -328,13 +331,13 @@ class TestGroupedOrderDetailView:
"email": "test@mail.fr",
},
)
- order = models.Order.objects.first()
- order.articles_nb == 0
assert response.status_code == 200
assert (
response.context["error_message"]
== "Veuillez commander au moins un produit"
)
+ assert not models.Order.objects.first()
+ assert not models.OrderAuthor.objects.first()
def test_deadline_passed(self, client, other_user):
"""
@@ -577,6 +580,122 @@ class TestOrder:
)
assert response.status_code == 403
+ def test_order_too_many_items_ordered(self, client, other_user):
+ """If a user orderd more articles than what is available,
+ the form is displayed again with an error.
+ The OrderedItems, OrderAuthor and Order are deleted."""
+ grouped_order = create_grouped_order(
+ days_before_delivery_date=5,
+ days_before_deadline=2,
+ name="gr order test",
+ orga_user=other_user,
+ )
+ item = models.Item.objects.create(
+ name="test item 1", grouped_order=grouped_order, price=1
+ )
+ item2 = models.Item.objects.create(
+ name="test item 2", grouped_order=grouped_order, price=5, max_limit=20
+ )
+ detail_url = reverse(
+ "order:grouped_order_detail",
+ kwargs={
+ "pk": grouped_order.pk,
+ },
+ )
+ response = client.get(detail_url)
+ assert response.status_code == 200
+ assert "test item" in response.content.decode()
+ assert "gr order test" in response.content.decode()
+ assert item.ordered_nb == 0
+ assert item2.ordered_nb == 0
+ order_url = reverse(
+ "order:order",
+ kwargs={
+ "grouped_order_id": grouped_order.pk,
+ },
+ )
+ response = client.post(
+ order_url,
+ {
+ f"quantity_{item.pk}": 4,
+ f"quantity_{item2.pk}": 25,
+ "first_name": "Prénom",
+ "last_name": "Nom",
+ "phone": "0645632569",
+ "email": "test@mail.fr",
+ },
+ )
+ assert response.status_code == 200
+ assert (
+ response.context["error_message"]
+ == "Trop de test item 2 commandés pour la quantité disponible"
+ )
+ item.refresh_from_db()
+ item2.refresh_from_db()
+ assert item.ordered_nb == 0
+ assert item2.ordered_nb == 0
+ assert not models.OrderAuthor.objects.first()
+ assert not models.Order.objects.first()
+ assert not models.OrderedItem.objects.first()
+
+ def test_negative_nb_ordered(self, client, other_user):
+ """If a user orders a negative nb of articles for an item,
+ the form is displayed again with an error.
+ The OrderedItems, OrderAuthor and Order are deleted."""
+ grouped_order = create_grouped_order(
+ days_before_delivery_date=5,
+ days_before_deadline=2,
+ name="gr order test",
+ orga_user=other_user,
+ )
+ item = models.Item.objects.create(
+ name="test item 1", grouped_order=grouped_order, price=1
+ )
+ item2 = models.Item.objects.create(
+ name="test item 2", grouped_order=grouped_order, price=5, max_limit=20
+ )
+ detail_url = reverse(
+ "order:grouped_order_detail",
+ kwargs={
+ "pk": grouped_order.pk,
+ },
+ )
+ response = client.get(detail_url)
+ assert response.status_code == 200
+ assert "test item" in response.content.decode()
+ assert "gr order test" in response.content.decode()
+ assert item.ordered_nb == 0
+ assert item2.ordered_nb == 0
+ order_url = reverse(
+ "order:order",
+ kwargs={
+ "grouped_order_id": grouped_order.pk,
+ },
+ )
+ response = client.post(
+ order_url,
+ {
+ f"quantity_{item.pk}": 4,
+ f"quantity_{item2.pk}": -2,
+ "first_name": "Prénom",
+ "last_name": "Nom",
+ "phone": "0645632569",
+ "email": "test@mail.fr",
+ },
+ )
+ assert response.status_code == 200
+ assert (
+ response.context["error_message"]
+ == "Veuillez commander un nombre positif de test item 2"
+ )
+ item.refresh_from_db()
+ item2.refresh_from_db()
+ assert item.ordered_nb == 0
+ assert item2.ordered_nb == 0
+ assert not models.OrderAuthor.objects.first()
+ assert not models.Order.objects.first()
+ assert not models.OrderedItem.objects.first()
+
class TestGroupedOrderCreateView:
def test_create_grouped_order(self, client_log):
diff --git a/la_chariotte/order/views.py b/la_chariotte/order/views.py
index 59de77f..f65881d 100644
--- a/la_chariotte/order/views.py
+++ b/la_chariotte/order/views.py
@@ -2,6 +2,7 @@ from io import BytesIO
from django import http
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
+from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404, render
from django.template.loader import get_template
from django.urls import reverse, reverse_lazy
@@ -62,6 +63,14 @@ class GroupedOrderDetailView(generic.DetailView):
template_name = "order/grouped_order_detail.html"
context_object_name = "grouped_order"
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ qty_dict = {}
+ for item in self.get_object().item_set.all():
+ qty_dict[item.id] = item.get_remaining_nb()
+ context["remaining_qty"] = qty_dict
+ return context
+
class GroupedOrderOverview(UserPassesTestMixin, generic.DetailView):
"""Overview of a grouped order, for the organizer"""
@@ -144,12 +153,17 @@ class ItemCreateView(UserPassesTestMixin, generic.CreateView):
def order(request, grouped_order_id):
"""Creates an AnonymousUser, and an Order for this GroupedOrder, with related OrderedItems"""
grouped_order = get_object_or_404(GroupedOrder, pk=grouped_order_id)
+
+ # check if the grouped order is ongoing
if not grouped_order.is_ongoing():
return http.HttpResponseForbidden()
+
# get a dict with (quantity_{{item_id}}:{{quantity}})
orders_dict = {
key: value for key, value in request.POST.items() if key.startswith("quantity")
}
+
+ # create an order
first_name = request.POST["first_name"]
last_name = request.POST["last_name"]
phone = request.POST["phone"]
@@ -158,33 +172,76 @@ def order(request, grouped_order_id):
first_name=first_name, last_name=last_name, email=email, phone=phone
)
order = Order.objects.create(author=author, grouped_order=grouped_order)
+
+ # add items to the order
+ error_message = None
for key, quantity in orders_dict.items():
- if quantity == "":
- quantity = 0
- if int(quantity) > 0:
- item = grouped_order.item_set.get(pk=key.split("_")[1])
- OrderedItem.objects.create(nb=quantity, order=order, item=item)
- item.compute_ordered_nb()
- order.compute_order_articles_nb()
- if order.articles_nb == 0:
- # Redisplay the order form for this grouped order.
+ quantity = int(quantity)
+ item = grouped_order.item_set.get(pk=key.split("_")[1])
+ # check if too many items are ordered
+ error_message = validate_item_ordered_nb(item, quantity)
+ if error_message:
+ break # stop creating items if there is an error
+ OrderedItem.objects.create(nb=quantity, order=order, item=item)
+
+ # Redisplay the form with error messages if there is an error
+ if error_message:
+ order.delete()
+ author.delete()
+ grouped_order.compute_items_ordered_nb()
return render(
request,
"order/grouped_order_detail.html",
{
"grouped_order": grouped_order,
- "error_message": "Veuillez commander au moins un produit",
+ "error_message": error_message,
},
)
- else:
- order.compute_order_price()
- # Always return an http.HttpResponseRedirect after successfully dealing
- # with POST data. This prevents data from being posted twice if a
- # user hits the Back button.
- return http.HttpResponseRedirect(
- reverse("order:order_confirm", args=(grouped_order.pk, order.pk))
+
+ # check if the order contains articles
+ error_message = validate_articles_ordered_nb(order)
+
+ # Redisplay the form with error messages if there is an error
+ if error_message:
+ order.delete()
+ author.delete()
+ return render(
+ request,
+ "order/grouped_order_detail.html",
+ {
+ "grouped_order": grouped_order,
+ "error_message": error_message,
+ },
)
+ # Redirect to confirmation page
+ order.compute_order_price()
+ grouped_order.compute_items_ordered_nb()
+ # Always return an http.HttpResponseRedirect after successfully dealing
+ # with POST data. This prevents data from being posted twice if a
+ # user hits the Back button.
+ return http.HttpResponseRedirect(
+ reverse("order:order_confirm", args=(grouped_order.pk, order.pk))
+ )
+
+
+def validate_item_ordered_nb(item, ordered_nb):
+ """Returns an error message if the ordered items are not available
+ or if the ordered nb is negative"""
+ if ordered_nb < 0:
+ return f"Veuillez commander un nombre positif de {item.name}"
+ if item.get_remaining_nb() and item.get_remaining_nb() - ordered_nb < 0:
+ return f"Trop de {item.name} commandés pour la quantité disponible"
+ return None
+
+
+def validate_articles_ordered_nb(order):
+ """Return an error if no items are ordered"""
+ order.compute_order_articles_nb()
+ if order.articles_nb == 0:
+ return "Veuillez commander au moins un produit"
+ return None
+
class OrderDetailView(generic.DetailView):
"""Confirmation page after a user orders"""