mirror of
https://framagit.org/la-chariotte/la-chariotte.git
synced 2025-05-08 14:51:52 +02:00
Compare commits
7 commits
ec84e5d51e
...
39220a15d7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
39220a15d7 | ||
![]() |
4c494c8684 | ||
![]() |
0ad9326d97 | ||
![]() |
b05eeb5a82 | ||
![]() |
8adc700005 | ||
![]() |
6a82102ef0 | ||
![]() |
76311065d9 |
17 changed files with 2279 additions and 434 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -5,23 +5,6 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## 1.3.0 (2024-11-02)
|
|
||||||
|
|
||||||
This is a small release, with a few features and bugfixes. The code is now
|
|
||||||
hosted on https://framagit.org rather than https://gitlab.com.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Changed hour and date format in the generated CSV files
|
|
||||||
- Added the date in the grouped order dashboard
|
|
||||||
- Changed the display of the last possible date and hour to place an order
|
|
||||||
- Added the total number of ordered items in the grouped orders overview
|
|
||||||
- Removed header line and column for grouped order name in the CSV export for emails
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- The "reset password" form is now fixed, it was broken since the last release.
|
|
||||||
|
|
||||||
## 1.2.0 (2024-07-12)
|
## 1.2.0 (2024-07-12)
|
||||||
|
|
||||||
A small release, with a few features, a new footer and a `/stats` endpoint.
|
A small release, with a few features, a new footer and a `/stats` endpoint.
|
||||||
|
|
1213
chariotte-v0-data.sql
Normal file
1213
chariotte-v0-data.sql
Normal file
File diff suppressed because it is too large
Load diff
|
@ -22,21 +22,22 @@ classDiagram
|
||||||
name
|
name
|
||||||
deadline : DateTime
|
deadline : DateTime
|
||||||
delivery_date : Date
|
delivery_date : Date
|
||||||
delivery_slot
|
|
||||||
place
|
place
|
||||||
description
|
description
|
||||||
orga : CustomUser
|
orga : CustomUser
|
||||||
|
total_price
|
||||||
}
|
}
|
||||||
class Item{
|
class Item{
|
||||||
name
|
name
|
||||||
grouped_order : GroupedOrder
|
grouped_order : GroupedOrder
|
||||||
ordered_nb
|
ordered_nb
|
||||||
price
|
total_price
|
||||||
max_limit
|
max_limit
|
||||||
}
|
}
|
||||||
class Order{
|
class Order{
|
||||||
grouped_order : GroupedOrder
|
grouped_order : GroupedOrder
|
||||||
author : OrderAuthor
|
author : OrderAuthor
|
||||||
|
price
|
||||||
created_date
|
created_date
|
||||||
note
|
note
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = "1.3.0"
|
__version__ = "1.2.0"
|
||||||
|
|
|
@ -8,6 +8,6 @@
|
||||||
</p>
|
</p>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<p>Votre mot de passe a été correctement réinitialisé. Vous pouvez maintenant vous connecter.</p>
|
<p>Votre mot de passe a été correctement réinitialisé. Vous pouvez maintenant vous connecter.</p>
|
||||||
<a class="button is-primary" href="{% url 'accounts:login' %}">Se connecter</a>
|
<button class="button is-primary" href="{% url 'accounts:login'%}">Se connecter</button>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -9,8 +9,8 @@
|
||||||
{% if validlink %}
|
{% if validlink %}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<p>Veuillez entrer un nouveau mot de passe tout neuf.</p>
|
<p>Veuillez entrer un nouveau mot de passe tout neuf.</p>
|
||||||
{{ form | crispy }}
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% crispy form %}
|
||||||
<input class="button is-primary"
|
<input class="button is-primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Changer le mot de passe">
|
value="Changer le mot de passe">
|
||||||
|
|
|
@ -8,8 +8,7 @@
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<p>Entrez votre mail pour recevoir les instructions pour le réinitialiser.</p>
|
<p>Entrez votre mail pour recevoir les instructions pour le réinitialiser.</p>
|
||||||
{{ form | crispy }}
|
{% crispy form %}
|
||||||
{% csrf_token %}
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<input class="button is-primary" type="submit" value="Envoyer">
|
<input class="button is-primary" type="submit" value="Envoyer">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 4.2.16 on 2024-12-08 15:15
|
# Generated by Django 4.2.11 on 2024-04-14 08:58
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("order", "0027_remove_groupedorder_total_price_and_more"),
|
("order", "0026_groupedorder_delivery_slot"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -1,36 +0,0 @@
|
||||||
# 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"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,20 +0,0 @@
|
||||||
# Generated by Django 4.2.16 on 2024-12-08 15:41
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
def set_phone_mandatory(apps, schema_editor):
|
|
||||||
"""For continuity, force mandatory phone for the orders that were created so far."""
|
|
||||||
GroupedOrder = apps.get_model("order", "GroupedOrder")
|
|
||||||
for grouped_order in GroupedOrder.objects.all():
|
|
||||||
grouped_order.is_phone_mandatory = True
|
|
||||||
grouped_order.save()
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("order", "0028_groupedorder_is_phone_mandatory"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(set_phone_mandatory),
|
|
||||||
]
|
|
|
@ -25,6 +25,7 @@ class GroupedOrder(models.Model):
|
||||||
max_length=100, null=True, blank=True, verbose_name="Lieu de livraison"
|
max_length=100, null=True, blank=True, verbose_name="Lieu de livraison"
|
||||||
)
|
)
|
||||||
description = models.TextField("Description", null=True, blank=True)
|
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)
|
code = models.CharField(auto_created=True)
|
||||||
is_phone_mandatory = models.BooleanField(
|
is_phone_mandatory = models.BooleanField(
|
||||||
default=False, verbose_name="Numéro de téléphone obligatoire"
|
default=False, verbose_name="Numéro de téléphone obligatoire"
|
||||||
|
@ -54,12 +55,16 @@ class GroupedOrder(models.Model):
|
||||||
)
|
)
|
||||||
self.code = f"{base_36_pk}{random_string}"[:code_length]
|
self.code = f"{base_36_pk}{random_string}"[:code_length]
|
||||||
|
|
||||||
@property
|
def compute_total_price(self):
|
||||||
def total_price(self):
|
|
||||||
price = 0
|
price = 0
|
||||||
for order in self.order_set.all():
|
for order in self.order_set.all():
|
||||||
price += order.price
|
price += order.price
|
||||||
return price
|
self.total_price = price
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def compute_items_ordered_nb(self):
|
||||||
|
for item in self.item_set.all():
|
||||||
|
item.compute_ordered_nb()
|
||||||
|
|
||||||
def get_total_ordered_items(self):
|
def get_total_ordered_items(self):
|
||||||
total_nb = 0
|
total_nb = 0
|
||||||
|
@ -121,24 +126,26 @@ class Order(models.Model):
|
||||||
GroupedOrder, on_delete=models.CASCADE, related_name="order_set"
|
GroupedOrder, on_delete=models.CASCADE, related_name="order_set"
|
||||||
)
|
)
|
||||||
author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE)
|
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)
|
created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True)
|
||||||
note = models.TextField(max_length=200, null=True, blank=True)
|
note = models.TextField(max_length=200, null=True, blank=True)
|
||||||
|
|
||||||
@property
|
def compute_order_articles_nb(self):
|
||||||
def articles_nb(self):
|
|
||||||
"""Computes the number of articles in this order"""
|
"""Computes the number of articles in this order"""
|
||||||
articles_nb = 0
|
articles_nb = 0
|
||||||
for ord_item in self.ordered_items.all():
|
for ord_item in self.ordered_items.all():
|
||||||
articles_nb += ord_item.nb
|
articles_nb += ord_item.nb
|
||||||
return articles_nb
|
self.articles_nb = articles_nb
|
||||||
|
self.save()
|
||||||
|
|
||||||
@property
|
def compute_order_price(self):
|
||||||
def price(self):
|
|
||||||
"""Computes the total price of the order"""
|
"""Computes the total price of the order"""
|
||||||
price = 0
|
price = 0
|
||||||
for ord_item in self.ordered_items.all():
|
for ord_item in self.ordered_items.all():
|
||||||
price += ord_item.get_price()
|
price += ord_item.get_price()
|
||||||
return price
|
self.price = price
|
||||||
|
self.save()
|
||||||
|
|
||||||
def __str__(self): # pragma: no cover
|
def __str__(self): # pragma: no cover
|
||||||
return (
|
return (
|
||||||
|
@ -153,13 +160,15 @@ class Item(models.Model):
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
max_limit = models.PositiveSmallIntegerField(null=True, blank=True)
|
max_limit = models.PositiveSmallIntegerField(null=True, blank=True)
|
||||||
|
|
||||||
@property
|
ordered_nb = models.IntegerField(default=0)
|
||||||
def ordered_nb(self):
|
|
||||||
|
def compute_ordered_nb(self):
|
||||||
"""Computes the number of times this item has been ordered"""
|
"""Computes the number of times this item has been ordered"""
|
||||||
ordered_nb = 0
|
ordered_nb = 0
|
||||||
for order in self.orders.all():
|
for order in self.orders.all():
|
||||||
ordered_nb += order.nb
|
ordered_nb += order.nb
|
||||||
return ordered_nb
|
self.ordered_nb = ordered_nb
|
||||||
|
self.save()
|
||||||
|
|
||||||
def get_total_price(self):
|
def get_total_price(self):
|
||||||
"""Returns the total price of all orders on this item"""
|
"""Returns the total price of all orders on this item"""
|
||||||
|
|
|
@ -119,11 +119,12 @@ class GroupedOrderDetailView(generic.DetailView):
|
||||||
prices_dict = {item.id: item.price for item in items}
|
prices_dict = {item.id: item.price for item in items}
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
order_author = OrderAuthor(
|
order_author = OrderAuthor.objects.create(
|
||||||
first_name=self.request.user.first_name,
|
first_name=self.request.user.first_name,
|
||||||
last_name=self.request.user.last_name,
|
last_name=self.request.user.last_name,
|
||||||
email=self.request.user.username,
|
email=self.request.user.username,
|
||||||
)
|
)
|
||||||
|
order_author.save()
|
||||||
else:
|
else:
|
||||||
order_author = None
|
order_author = None
|
||||||
|
|
||||||
|
@ -153,6 +154,12 @@ class GroupedOrderOverview(UserIsOrgaMixin, generic.DetailView):
|
||||||
# Staff can see but not edit grouped orders
|
# Staff can see but not edit grouped orders
|
||||||
return super().test_func() or self.request.user.is_staff
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(GroupedOrderOverview, self).get_context_data(**kwargs)
|
context = super(GroupedOrderOverview, self).get_context_data(**kwargs)
|
||||||
# Add share link to context
|
# Add share link to context
|
||||||
|
@ -225,6 +232,7 @@ class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
|
||||||
# duplicate each item and add it to new_grouped_order
|
# duplicate each item and add it to new_grouped_order
|
||||||
for item in initial_grouped_order.item_set.all():
|
for item in initial_grouped_order.item_set.all():
|
||||||
item.pk = None
|
item.pk = None
|
||||||
|
item.ordered_nb = 0
|
||||||
item.save()
|
item.save()
|
||||||
new_grouped_order.item_set.add(item)
|
new_grouped_order.item_set.add(item)
|
||||||
|
|
||||||
|
@ -296,11 +304,7 @@ class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView):
|
||||||
context = super(GroupedOrderExportView, self).get_context_data(**kwargs)
|
context = super(GroupedOrderExportView, self).get_context_data(**kwargs)
|
||||||
grouped_order = self.get_object()
|
grouped_order = self.get_object()
|
||||||
|
|
||||||
items = [
|
items = grouped_order.item_set.filter(ordered_nb__gt=0).order_by("name")
|
||||||
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(
|
orders = grouped_order.order_set.all().order_by(
|
||||||
"author__last_name", "author__first_name"
|
"author__last_name", "author__first_name"
|
||||||
)
|
)
|
||||||
|
|
|
@ -72,6 +72,7 @@ def place_order(request, code):
|
||||||
if error_message:
|
if error_message:
|
||||||
order.delete()
|
order.delete()
|
||||||
author.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",
|
||||||
|
@ -102,6 +103,8 @@ def place_order(request, code):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send confirmation mail and redirect to confirmation page
|
# Send confirmation mail and redirect to confirmation page
|
||||||
|
order.compute_order_price()
|
||||||
|
grouped_order.compute_items_ordered_nb()
|
||||||
send_order_confirmation_mail(order)
|
send_order_confirmation_mail(order)
|
||||||
|
|
||||||
# Redirect to prevent data from being posted twice when the user hits the Back
|
# Redirect to prevent data from being posted twice when the user hits the Back
|
||||||
|
@ -120,6 +123,7 @@ def validate_item_ordered_nb(item, ordered_nb):
|
||||||
|
|
||||||
def validate_articles_ordered_nb(order):
|
def validate_articles_ordered_nb(order):
|
||||||
"""Return an error if no items are ordered"""
|
"""Return an error if no items are ordered"""
|
||||||
|
order.compute_order_articles_nb()
|
||||||
if order.articles_nb == 0:
|
if order.articles_nb == 0:
|
||||||
return "Veuillez commander au moins un produit"
|
return "Veuillez commander au moins un produit"
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1426,7 +1426,7 @@ class TestGroupedOrderSheetView:
|
||||||
response = client_log.get(generate_sheet_url)
|
response = client_log.get(generate_sheet_url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.context["grouped_order"] == grouped_order
|
assert response.context["grouped_order"] == grouped_order
|
||||||
assert len(response.context["items"]) == 0
|
assert response.context["items"].count() == 0
|
||||||
assert len(response.context["orders_dict"]) == 0
|
assert len(response.context["orders_dict"]) == 0
|
||||||
|
|
||||||
# we order some items in the grouped order
|
# we order some items in the grouped order
|
||||||
|
@ -1434,7 +1434,7 @@ class TestGroupedOrderSheetView:
|
||||||
response = client_log.get(generate_sheet_url)
|
response = client_log.get(generate_sheet_url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.context["grouped_order"] == grouped_order
|
assert response.context["grouped_order"] == grouped_order
|
||||||
assert len(response.context["items"]) == 2
|
assert response.context["items"].count() == 2
|
||||||
assert response.context["orders_dict"][order] == [3, 2]
|
assert response.context["orders_dict"][order] == [3, 2]
|
||||||
assert response.context["grouped_order"].total_price == 35
|
assert response.context["grouped_order"].total_price == 35
|
||||||
|
|
||||||
|
|
|
@ -52,4 +52,11 @@ 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, item=item_2, nb=2)
|
||||||
models.OrderedItem.objects.create(order=order_2, item=item_1, nb=1)
|
models.OrderedItem.objects.create(order=order_2, item=item_1, nb=1)
|
||||||
models.OrderedItem.objects.create(order=order_3, item=item_2, 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
|
return order
|
||||||
|
|
Loading…
Reference in a new issue