Compare commits

...

10 commits

Author SHA1 Message Date
Bastien Roy
73d5afb34a Merge branch 'feature/166/rendre-telephone-non-obligatoire' into 'develop'
[166] Rendre le telephone non obligatoire

Closes #166

See merge request la-chariotte/la-chariotte!108
2024-12-08 13:46:53 +00:00
xmeunier
10aed1560b Remove useless chariotte-v0-data.sql file 2024-11-05 21:21:13 +01:00
xmeunier
686a31b96a Make aggregated values computed when called instead of stored in DB
- GroupedOrder.total_price
- Order.articles_nb
- Order.price
- Item.ordered_nb
2024-11-05 11:01:02 +00:00
Laetitia
39220a15d7 ref: change phone_mandatory into is_change_mandatory 2024-10-25 11:32:40 +02:00
Laetitia
4c494c8684 feat: move facultatif label 2024-10-25 11:30:59 +02:00
Laetitia
0ad9326d97 ref: remove useless import 2024-10-25 11:30:59 +02:00
Laetitia
b05eeb5a82 feat: affichage telephone dans la liste des commandes sur la page de gestion 2024-10-20 15:25:19 +00:00
Laetitia
8adc700005 feat: le tel n'est plus obligatoire quand l'organisateur a choisi ça
Affichage de "faciltatif" dans le formulaire de commande
2024-10-20 15:25:19 +00:00
Bhorah
6a82102ef0 166 suppression d'un truc qui avait pas sa place là 2024-10-20 15:25:19 +00:00
Bastien Roy
76311065d9 166 téléphone obligatoire (manque css) 2024-10-20 15:25:19 +00:00
15 changed files with 166 additions and 1272 deletions

File diff suppressed because it is too large Load diff

View file

@ -22,22 +22,21 @@ classDiagram
name
deadline : DateTime
delivery_date : Date
delivery_slot
place
description
orga : CustomUser
total_price
}
class Item{
name
grouped_order : GroupedOrder
ordered_nb
total_price
price
max_limit
}
class Order{
grouped_order : GroupedOrder
author : OrderAuthor
price
created_date
note
}

View file

@ -18,6 +18,10 @@ class GroupedOrderForm(forms.ModelForm):
widget=forms.TimeInput(attrs={"type": "time"}),
initial=datetime.time(hour=23, minute=59, second=59),
)
is_phone_mandatory = forms.BooleanField(
label="Numéro de téléphone obligatoire pour les participants",
required=False,
)
class Meta:
model = GroupedOrder
@ -29,6 +33,7 @@ class GroupedOrderForm(forms.ModelForm):
"delivery_slot",
"place",
"description",
"is_phone_mandatory",
]
widgets = {
"name": forms.TextInput(

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.11 on 2024-04-14 08:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("order", "0026_groupedorder_delivery_slot"),
]
operations = [
migrations.AddField(
model_name="groupedorder",
name="is_phone_mandatory",
field=models.BooleanField(
default=False, verbose_name="Numéro de téléphone obligatoire"
),
),
]

View file

@ -0,0 +1,36 @@
# Generated by Django 4.2.16 on 2024-10-31 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("order", "0026_groupedorder_delivery_slot"),
]
operations = [
migrations.RemoveField(
model_name="groupedorder",
name="total_price",
),
migrations.RemoveField(
model_name="item",
name="ordered_nb",
),
migrations.RemoveField(
model_name="order",
name="articles_nb",
),
migrations.RemoveField(
model_name="order",
name="price",
),
migrations.AlterField(
model_name="order",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, verbose_name="Date et heure de commande"
),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 4.2 on 2024-10-20 13:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("order", "0027_groupedorder_phone_mandatory"),
]
operations = [
migrations.AlterField(
model_name="order",
name="created_date",
field=models.DateTimeField(
auto_now_add=True, verbose_name="Date et heure de commande"
),
),
]

View file

@ -25,8 +25,10 @@ class GroupedOrder(models.Model):
max_length=100, null=True, blank=True, verbose_name="Lieu de livraison"
)
description = models.TextField("Description", null=True, blank=True)
total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
code = models.CharField(auto_created=True)
is_phone_mandatory = models.BooleanField(
default=False, verbose_name="Numéro de téléphone obligatoire"
)
def create_code_from_pk(self):
"""When a grouped order is created, a unique code is generated, to be used to
@ -52,16 +54,12 @@ class GroupedOrder(models.Model):
)
self.code = f"{base_36_pk}{random_string}"[:code_length]
def compute_total_price(self):
@property
def total_price(self):
price = 0
for order in self.order_set.all():
price += order.price
self.total_price = price
self.save()
def compute_items_ordered_nb(self):
for item in self.item_set.all():
item.compute_ordered_nb()
return price
def get_total_ordered_items(self):
total_nb = 0
@ -123,26 +121,24 @@ class Order(models.Model):
GroupedOrder, on_delete=models.CASCADE, related_name="order_set"
)
author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE)
articles_nb = models.PositiveIntegerField(default=0)
price = models.DecimalField(max_digits=10, decimal_places=2, default=0)
created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True)
note = models.TextField(max_length=200, null=True, blank=True)
def compute_order_articles_nb(self):
@property
def articles_nb(self):
"""Computes the number of articles in this order"""
articles_nb = 0
for ord_item in self.ordered_items.all():
articles_nb += ord_item.nb
self.articles_nb = articles_nb
self.save()
return articles_nb
def compute_order_price(self):
@property
def price(self):
"""Computes the total price of the order"""
price = 0
for ord_item in self.ordered_items.all():
price += ord_item.get_price()
self.price = price
self.save()
return price
def __str__(self): # pragma: no cover
return (
@ -157,15 +153,13 @@ class Item(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2)
max_limit = models.PositiveSmallIntegerField(null=True, blank=True)
ordered_nb = models.IntegerField(default=0)
def compute_ordered_nb(self):
@property
def ordered_nb(self):
"""Computes the number of times this item has been ordered"""
ordered_nb = 0
for order in self.orders.all():
ordered_nb += order.nb
self.ordered_nb = ordered_nb
self.save()
return ordered_nb
def get_total_price(self):
"""Returns the total price of all orders on this item"""

View file

@ -1,5 +1,7 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Nouvelle commande groupée{% endblock %}
{% block content %}
@ -10,8 +12,9 @@
<p class="title">Nouvelle commande groupée</p>
<div class="columns">
<div class="column is-8">
<form method="post" onsubmit="deadlinePassedCheck(event)">{% csrf_token %}
{{ form.as_p }}
<form method="post" onsubmit="deadlinePassedCheck(event)">
{% csrf_token %}
{{ form | crispy }}
<div class="buttons">
<a class="button is-light" href="{% url 'order:index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant">

View file

@ -158,16 +158,18 @@
value="{{ order_author.last_name }}" required></p>
</div>
<div class="column">
<p><label for="phone">Numéro de téléphone :</label>
<input id="phone" type="tel" pattern="[0-9]{10}" placeholder="0601020304" name="phone"
value="{{ order_author.phone }}" required></p>
<p><label for="phone">Numéro de téléphone {% if not is_phone_mandatory %}<em>(facultatif)</em> {% endif %}:</label>
<input id="phone" type="tel" pattern="[0-9]{10}"
placeholder="0601020304" name="phone"
value="{{ order_author.phone }}"
{% if is_phone_mandatory %}required{% endif %}></p>
<p><label for="email">Adresse mail : </label>
<input id="email" type="email" placeholder="exemple@mail.fr" name="email"
value="{{ order_author.email }}" required></p>
</div>
</div>
<p><label for="note">Note à l'organisateur·ice</label>
<textarea id="note" rows=3 placeholder="(facultatif)" name="note">{{ note }}</textarea></p>
<p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label>
<textarea id="note" rows=3 name="note">{{ note }}</textarea></p>
<div class="buttons">
<button id="submit" type="submit" value="Order" class="button is-primary">
@ -186,7 +188,6 @@
// Compute total price whenever a value in input is modified
document.getElementById("inputs-parent").addEventListener("change", function () {
inputs = [...document.getElementsByTagName("input")].filter(input => input.getAttribute("name").indexOf("quantity_") === 0); //filter the inputs to get the quantity inputs only
prices = {{ prices_dict | safe }};
let total_price = 0;

View file

@ -161,7 +161,7 @@
<tr>
<td>{{ order.author }}</td>
<td>{{ order.price }} €</td>
<td><a href="mailto:{{ order.author.email }}">{{ order.author.email }}</a></td>
<td><a href="mailto:{{ order.author.email }}">{{ order.author.email }}</a>{% if order.author.phone %} / {{ order.author.phone }}{% endif %}</td>
<td>
<button class="button is-info is-small js-modal-trigger" data-target="order-detail-{{ order.id }}">
Voir

View file

@ -1,5 +1,7 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Modifier la commande groupée{% endblock %}
{% block content %}
@ -9,13 +11,14 @@
<div class="box">
<p class="title">{{ grouped_order.name }} - modifier</p>
<form method="post" onsubmit="deadlinePassedCheck(event)">{% csrf_token %}
{{ form.as_p }}
{{ form | crispy }}
<div class="buttons">
<a class="button is-light" href="{% url 'order:index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant">
</div>
</form>
</div>
{% endblock %}
{% block extra_js %}

View file

@ -112,7 +112,8 @@ class GroupedOrderDetailView(generic.DetailView):
return get_object_or_404(GroupedOrder, code=self.kwargs.get("code"))
def get_context_data(self, **kwargs):
items = self.get_object().item_set.all()
grouped_order = self.get_object()
items = grouped_order.item_set.all()
remaining_qty = {item.id: item.get_remaining_nb() for item in items}
prices_dict = {item.id: item.price for item in items}
@ -133,6 +134,8 @@ class GroupedOrderDetailView(generic.DetailView):
"prices_dict": json.dumps(prices_dict, cls=DjangoJSONEncoder),
"remaining_qty": remaining_qty,
"order_author": order_author,
# Used to set if the phone is required in the form
"is_phone_mandatory": grouped_order.is_phone_mandatory,
}
)
return context
@ -150,12 +153,6 @@ class GroupedOrderOverview(UserIsOrgaMixin, generic.DetailView):
# Staff can see but not edit grouped orders
return super().test_func() or self.request.user.is_staff
def get(self, request, *args, **kwargs):
# Compute grouped order total price before display
self.get_object().compute_total_price()
self.get_object().compute_items_ordered_nb()
return super().get(self, request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(GroupedOrderOverview, self).get_context_data(**kwargs)
# Add share link to context
@ -228,7 +225,6 @@ class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
# duplicate each item and add it to new_grouped_order
for item in initial_grouped_order.item_set.all():
item.pk = None
item.ordered_nb = 0
item.save()
new_grouped_order.item_set.add(item)
@ -300,7 +296,11 @@ class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView):
context = super(GroupedOrderExportView, self).get_context_data(**kwargs)
grouped_order = self.get_object()
items = grouped_order.item_set.filter(ordered_nb__gt=0).order_by("name")
items = [
item
for item in grouped_order.item_set.all().order_by("name")
if item.ordered_nb > 0
]
orders = grouped_order.order_set.all().order_by(
"author__last_name", "author__first_name"
)
@ -361,7 +361,7 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView):
response = http.HttpResponse(
content_type="text/csv",
headers={
"Content-Disposition": f'attachment; filename="{ context["object"].name }-commandes"'
"Content-Disposition": f'attachment; filename="{context["object"].name}-commandes"'
},
)
writer = csv.writer(response)

View file

@ -72,7 +72,6 @@ def place_order(request, code):
if error_message:
order.delete()
author.delete()
grouped_order.compute_items_ordered_nb()
return render(
request,
"order/grouped_order_detail.html",
@ -103,8 +102,6 @@ def place_order(request, code):
)
# Send confirmation mail and redirect to confirmation page
order.compute_order_price()
grouped_order.compute_items_ordered_nb()
send_order_confirmation_mail(order)
# Redirect to prevent data from being posted twice when the user hits the Back
@ -123,7 +120,6 @@ def validate_item_ordered_nb(item, ordered_nb):
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

View file

@ -662,6 +662,36 @@ class TestGroupedOrderDetailView:
assert order.price == 4
assert item.get_remaining_nb() == 16
def test_phone_not_required_display(self, client, other_user):
"""a user orders something without entering phone when it is required"""
grouped_order = create_grouped_order(
days_before_delivery_date=5,
days_before_deadline=2,
name="gr order test",
orga_user=other_user,
)
assert grouped_order.is_phone_mandatory == True
item = models.Item.objects.create(
name="test item 1", grouped_order=grouped_order, price=1, max_limit=2
)
detail_url = reverse(
"order:grouped_order_detail",
kwargs={
"code": grouped_order.code,
},
)
response = client.get(detail_url)
assert response.status_code == 200
assert "gr order test" in response.content.decode()
assert (
"Numéro de téléphone <em>(facultatif)</em>" not in response.content.decode()
)
grouped_order.is_phone_mandatory = False
grouped_order.save()
response = client.get(detail_url)
assert "gr order test" in response.content.decode()
assert "Numéro de téléphone <em>(facultatif)</em>" in response.content.decode()
class TestGroupedOrderOverview:
def test_get_overview(self, client_log):
@ -1396,7 +1426,7 @@ class TestGroupedOrderSheetView:
response = client_log.get(generate_sheet_url)
assert response.status_code == 200
assert response.context["grouped_order"] == grouped_order
assert response.context["items"].count() == 0
assert len(response.context["items"]) == 0
assert len(response.context["orders_dict"]) == 0
# we order some items in the grouped order
@ -1404,7 +1434,7 @@ class TestGroupedOrderSheetView:
response = client_log.get(generate_sheet_url)
assert response.status_code == 200
assert response.context["grouped_order"] == grouped_order
assert response.context["items"].count() == 2
assert len(response.context["items"]) == 2
assert response.context["orders_dict"][order] == [3, 2]
assert response.context["grouped_order"].total_price == 35

View file

@ -9,12 +9,20 @@ pytestmark = pytest.mark.django_db
def create_grouped_order(
days_before_delivery_date, days_before_deadline, name, orga_user
days_before_delivery_date,
days_before_deadline,
name,
orga_user,
is_phone_mandatory=True,
):
date = timezone.now().date() + datetime.timedelta(days=days_before_delivery_date)
deadline = timezone.now() + datetime.timedelta(days=days_before_deadline)
grouped_order = models.GroupedOrder.objects.create(
name=name, orga=orga_user, delivery_date=date, deadline=deadline
name=name,
orga=orga_user,
delivery_date=date,
deadline=deadline,
is_phone_mandatory=is_phone_mandatory,
)
grouped_order.create_code_from_pk()
grouped_order.save()
@ -44,11 +52,4 @@ def order_items_in_grouped_order(grouped_order):
models.OrderedItem.objects.create(order=order, item=item_2, nb=2)
models.OrderedItem.objects.create(order=order_2, item=item_1, nb=1)
models.OrderedItem.objects.create(order=order_3, item=item_2, nb=1)
item_1.compute_ordered_nb()
item_2.compute_ordered_nb()
order.compute_order_price()
order_2.compute_order_price()
order_3.compute_order_price()
grouped_order.compute_total_price()
grouped_order.save()
return order