impossible to order above max_limit for items

This commit is contained in:
Laetitia Getti 2023-05-26 17:03:10 +02:00
parent 2087b49fc4
commit a14267368c
5 changed files with 211 additions and 22 deletions

View file

@ -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})

View file

@ -26,7 +26,7 @@
<tr>
<form method="post" action="{{ create_item_url }}">
{% csrf_token %}
<td><input name="name" maxlength="100" placeholder="Nom du produit" required></input></td>
<td><input name="name" maxlength="100" placeholder="Nom du produit" autofocus required></input></td>
<td><input name="price" size="2" placeholder="5,40" required></input></td>
<td><input name="max_limit" size="2" placeholder="42"></input></td>
<td><button type="submit" class="button is-primary">Ajouter</button></td>

View file

@ -64,7 +64,8 @@
{% csrf_token %}
<td>{{ item.name }}</td>
<td>{{ item.price }} €</td>
<td><input name="quantity_{{ item.id }}" size="2" type="number" value="0" min="0"></input></td>
<td><input name="quantity_{{ item.id }}" size="2" type="number" value="0" min="0"></input>
{% if item.get_remaining_nb %}<span class="is-italic"> {{ item.get_remaining_nb }} disponibles</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>

View file

@ -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):

View file

@ -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"""