mirror of
https://framagit.org/la-chariotte/la-chariotte.git
synced 2025-05-03 12:21:52 +02:00
impossible to order above max_limit for items
This commit is contained in:
parent
2087b49fc4
commit
a14267368c
5 changed files with 211 additions and 22 deletions
|
@ -31,6 +31,11 @@ class GroupedOrder(models.Model):
|
||||||
self.total_price = price
|
self.total_price = price
|
||||||
self.save()
|
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):
|
def is_ongoing(self):
|
||||||
"""Returns True if the grouped order is open for new Orders - False if it's too late"""
|
"""Returns True if the grouped order is open for new Orders - False if it's too late"""
|
||||||
return self.deadline >= timezone.now()
|
return self.deadline >= timezone.now()
|
||||||
|
@ -130,6 +135,13 @@ class Item(models.Model):
|
||||||
"""Returns the price of all orders on this item"""
|
"""Returns the price of all orders on this item"""
|
||||||
return self.price * self.ordered_nb
|
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):
|
def get_absolute_url(self):
|
||||||
return reverse("order:manage_items", kwargs={"pk": self.grouped_order.pk})
|
return reverse("order:manage_items", kwargs={"pk": self.grouped_order.pk})
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<form method="post" action="{{ create_item_url }}">
|
<form method="post" action="{{ create_item_url }}">
|
||||||
{% csrf_token %}
|
{% 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="price" size="2" placeholder="5,40" required></input> €</td>
|
||||||
<td><input name="max_limit" size="2" placeholder="42"></input></td>
|
<td><input name="max_limit" size="2" placeholder="42"></input></td>
|
||||||
<td><button type="submit" class="button is-primary">Ajouter</button></td>
|
<td><button type="submit" class="button is-primary">Ajouter</button></td>
|
||||||
|
|
|
@ -64,7 +64,8 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<td>{{ item.name }}</td>
|
<td>{{ item.name }}</td>
|
||||||
<td>{{ item.price }} €</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -232,7 +232,7 @@ class TestGroupedOrderDetailView:
|
||||||
orga_user=other_user,
|
orga_user=other_user,
|
||||||
)
|
)
|
||||||
item = models.Item.objects.create(
|
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(
|
item2 = models.Item.objects.create(
|
||||||
name="test item 2", grouped_order=grouped_order, price=5
|
name="test item 2", grouped_order=grouped_order, price=5
|
||||||
|
@ -247,6 +247,7 @@ class TestGroupedOrderDetailView:
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "test item" in response.content.decode()
|
assert "test item" in response.content.decode()
|
||||||
assert "gr order test" 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 item.ordered_nb == 0
|
||||||
assert item2.ordered_nb == 0
|
assert item2.ordered_nb == 0
|
||||||
order_url = reverse(
|
order_url = reverse(
|
||||||
|
@ -286,11 +287,13 @@ class TestGroupedOrderDetailView:
|
||||||
assert order.ordered_items.count() == 2
|
assert order.ordered_items.count() == 2
|
||||||
assert order.articles_nb == 5
|
assert order.articles_nb == 5
|
||||||
assert order.price == 9
|
assert order.price == 9
|
||||||
|
assert item.get_remaining_nb() == 16
|
||||||
|
|
||||||
def test_order_item__no_articles_ordered(self, client, other_user):
|
def test_order_item__no_articles_ordered(self, client, other_user):
|
||||||
"""
|
"""
|
||||||
From the OrderDetailView, we order without having changed any item quantity.
|
From the OrderDetailView, we order without having changed any item quantity.
|
||||||
An error is raised.
|
An error is raised.
|
||||||
|
The order is deleted
|
||||||
"""
|
"""
|
||||||
grouped_order = create_grouped_order(
|
grouped_order = create_grouped_order(
|
||||||
days_before_delivery_date=5,
|
days_before_delivery_date=5,
|
||||||
|
@ -328,13 +331,13 @@ class TestGroupedOrderDetailView:
|
||||||
"email": "test@mail.fr",
|
"email": "test@mail.fr",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
order = models.Order.objects.first()
|
|
||||||
order.articles_nb == 0
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert (
|
assert (
|
||||||
response.context["error_message"]
|
response.context["error_message"]
|
||||||
== "Veuillez commander au moins un produit"
|
== "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):
|
def test_deadline_passed(self, client, other_user):
|
||||||
"""
|
"""
|
||||||
|
@ -577,6 +580,122 @@ class TestOrder:
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
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:
|
class TestGroupedOrderCreateView:
|
||||||
def test_create_grouped_order(self, client_log):
|
def test_create_grouped_order(self, client_log):
|
||||||
|
|
|
@ -2,6 +2,7 @@ from io import BytesIO
|
||||||
|
|
||||||
from django import http
|
from django import http
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
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.shortcuts import get_object_or_404, render
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
|
@ -62,6 +63,14 @@ class GroupedOrderDetailView(generic.DetailView):
|
||||||
template_name = "order/grouped_order_detail.html"
|
template_name = "order/grouped_order_detail.html"
|
||||||
context_object_name = "grouped_order"
|
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):
|
class GroupedOrderOverview(UserPassesTestMixin, generic.DetailView):
|
||||||
"""Overview of a grouped order, for the organizer"""
|
"""Overview of a grouped order, for the organizer"""
|
||||||
|
@ -144,12 +153,17 @@ class ItemCreateView(UserPassesTestMixin, generic.CreateView):
|
||||||
def order(request, grouped_order_id):
|
def order(request, grouped_order_id):
|
||||||
"""Creates an AnonymousUser, and an Order for this GroupedOrder, with related OrderedItems"""
|
"""Creates an AnonymousUser, and an Order for this GroupedOrder, with related OrderedItems"""
|
||||||
grouped_order = get_object_or_404(GroupedOrder, pk=grouped_order_id)
|
grouped_order = get_object_or_404(GroupedOrder, pk=grouped_order_id)
|
||||||
|
|
||||||
|
# check if the grouped order is ongoing
|
||||||
if not grouped_order.is_ongoing():
|
if not grouped_order.is_ongoing():
|
||||||
return http.HttpResponseForbidden()
|
return http.HttpResponseForbidden()
|
||||||
|
|
||||||
# get a dict with (quantity_{{item_id}}:{{quantity}})
|
# get a dict with (quantity_{{item_id}}:{{quantity}})
|
||||||
orders_dict = {
|
orders_dict = {
|
||||||
key: value for key, value in request.POST.items() if key.startswith("quantity")
|
key: value for key, value in request.POST.items() if key.startswith("quantity")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# create an order
|
||||||
first_name = request.POST["first_name"]
|
first_name = request.POST["first_name"]
|
||||||
last_name = request.POST["last_name"]
|
last_name = request.POST["last_name"]
|
||||||
phone = request.POST["phone"]
|
phone = request.POST["phone"]
|
||||||
|
@ -158,26 +172,51 @@ def order(request, grouped_order_id):
|
||||||
first_name=first_name, last_name=last_name, email=email, phone=phone
|
first_name=first_name, last_name=last_name, email=email, phone=phone
|
||||||
)
|
)
|
||||||
order = Order.objects.create(author=author, grouped_order=grouped_order)
|
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():
|
for key, quantity in orders_dict.items():
|
||||||
if quantity == "":
|
quantity = int(quantity)
|
||||||
quantity = 0
|
|
||||||
if int(quantity) > 0:
|
|
||||||
item = grouped_order.item_set.get(pk=key.split("_")[1])
|
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)
|
OrderedItem.objects.create(nb=quantity, order=order, item=item)
|
||||||
item.compute_ordered_nb()
|
|
||||||
order.compute_order_articles_nb()
|
# Redisplay the form with error messages if there is an error
|
||||||
if order.articles_nb == 0:
|
if error_message:
|
||||||
# Redisplay the order form for this grouped order.
|
order.delete()
|
||||||
|
author.delete()
|
||||||
|
grouped_order.compute_items_ordered_nb()
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"order/grouped_order_detail.html",
|
"order/grouped_order_detail.html",
|
||||||
{
|
{
|
||||||
"grouped_order": grouped_order,
|
"grouped_order": grouped_order,
|
||||||
"error_message": "Veuillez commander au moins un produit",
|
"error_message": error_message,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
|
# 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()
|
order.compute_order_price()
|
||||||
|
grouped_order.compute_items_ordered_nb()
|
||||||
# Always return an http.HttpResponseRedirect after successfully dealing
|
# Always return an http.HttpResponseRedirect after successfully dealing
|
||||||
# with POST data. This prevents data from being posted twice if a
|
# with POST data. This prevents data from being posted twice if a
|
||||||
# user hits the Back button.
|
# user hits the Back button.
|
||||||
|
@ -186,6 +225,24 @@ def order(request, grouped_order_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class OrderDetailView(generic.DetailView):
|
||||||
"""Confirmation page after a user orders"""
|
"""Confirmation page after a user orders"""
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue