Compare commits

...

3 commits

Author SHA1 Message Date
selfhoster1312
4eac4f43b8 feature: Add place relationship for distribution spots 2025-03-03 21:24:43 +01:00
Laetitia
19c4a0b6ed feat: update gitlab links everywhere else 2025-02-16 10:35:00 +00:00
Laetitia
9589bcf48d feat: update gitlab link to framagit 2025-02-16 10:35:00 +00:00
23 changed files with 517 additions and 104 deletions

View file

@ -6,7 +6,7 @@ started.
The first step is to clone the project, you can find more information about that in The first step is to clone the project, you can find more information about that in
the [getting started guide](../install.md). Once that's done, you can: the [getting started guide](../install.md). Once that's done, you can:
- choose a task [on the board](https://gitlab.com/la-chariotte/la_chariotte/-/boards) and assign it to - choose a task [on the board](https://framagit.org/la-chariotte/la-chariotte/-/boards) and assign it to
yourself - if you don't know which task to do, feel free to reach to yourself - if you don't know which task to do, feel free to reach to
us. us.
- create a new branch **from develop** naming it to reflect what you want to do - create a new branch **from develop** naming it to reflect what you want to do

View file

@ -11,7 +11,7 @@
- **docs.chariotte.fr**, the docs you are reading now. It's handled by [readthedocs.org](https://readthedocs.org). - **docs.chariotte.fr**, the docs you are reading now. It's handled by [readthedocs.org](https://readthedocs.org).
- **chariotte.fr**, the main instance. It's deployed on Alwaysdata - **chariotte.fr**, the main instance. It's deployed on Alwaysdata
- **blog.chariotte.fr**, our blog. It's [a static website](https://gitlab.com/la-chariotte/la-chariotte.gitlab.io) deployed on Gitlab pages. - **blog.chariotte.fr**, our blog. It's [a static website](https://framagit.org/la-chariotte/la-chariotte.frama.io) deployed on Gitlab pages.
## The main instance ## The main instance

View file

@ -1,7 +1,7 @@
First, clone the project First, clone the project
```bash ```bash
git clone git@gitlab.com:la-chariotte/la_chariotte.git git clone https://framagit.org/la-chariotte/la-chariotte.git
``` ```
## Virtual environment ## Virtual environment

View file

@ -22,7 +22,7 @@
<p><strong>Rendez-vous pour la distribution</strong> : <p><strong>Rendez-vous pour la distribution</strong> :
le {{ order.grouped_order.delivery_date }}{% if order.grouped_order.delivery_slot %}, {{ order.grouped_order.delivery_slot }}{% endif %} le {{ order.grouped_order.delivery_date }}{% if order.grouped_order.delivery_slot %}, {{ order.grouped_order.delivery_slot }}{% endif %}
{% if order.grouped_order.place %}<br>Lieu : {{ order.grouped_order.place }}{% endif %} {% if order.place %}<br>Lieu : {{ order.place }}{% endif %}
</p> </p>
<p><strong>Une question sur cette commande groupée ?</strong><br>Vous pouvez contacter l'organisateur·ice de la commande, <strong>{{ order.grouped_order.orga }}</strong> : <p><strong>Une question sur cette commande groupée ?</strong><br>Vous pouvez contacter l'organisateur·ice de la commande, <strong>{{ order.grouped_order.orga }}</strong> :

View file

@ -1,11 +1,12 @@
import datetime import datetime
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.forms.utils import to_current_timezone from django.forms.utils import to_current_timezone
from django.utils import timezone from django.utils import timezone
from la_chariotte.order.models import GroupedOrder, Item from la_chariotte.order.models import GroupedOrder, Item, Place
class GroupedOrderForm(forms.ModelForm): class GroupedOrderForm(forms.ModelForm):
@ -22,6 +23,13 @@ class GroupedOrderForm(forms.ModelForm):
label="Numéro de téléphone obligatoire pour les participants", label="Numéro de téléphone obligatoire pour les participants",
required=False, required=False,
) )
places = forms.ModelMultipleChoiceField(
label="Lieux de distribution",
# TODO: filter own places
queryset=Place.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
class Meta: class Meta:
model = GroupedOrder model = GroupedOrder
@ -31,7 +39,7 @@ class GroupedOrderForm(forms.ModelForm):
"deadline_time", "deadline_time",
"delivery_date", "delivery_date",
"delivery_slot", "delivery_slot",
"place", "places",
"description", "description",
"is_phone_mandatory", "is_phone_mandatory",
] ]
@ -46,7 +54,6 @@ class GroupedOrderForm(forms.ModelForm):
"delivery_slot": forms.TextInput( "delivery_slot": forms.TextInput(
attrs={"placeholder": "14h - 17h (facultatif)"} attrs={"placeholder": "14h - 17h (facultatif)"}
), ),
"place": forms.TextInput(attrs={"placeholder": "(facultatif)"}),
"description": forms.Textarea( "description": forms.Textarea(
attrs={ attrs={
"placeholder": "Plus d'infos sur la commande groupée ? (facultatif)" "placeholder": "Plus d'infos sur la commande groupée ? (facultatif)"
@ -103,3 +110,32 @@ class JoinGroupedOrderForm(forms.Form):
"Désolé, nous ne trouvons aucune commande avec ce code" "Désolé, nous ne trouvons aucune commande avec ce code"
) )
return form_code return form_code
class PlaceForm(forms.ModelForm):
class Meta:
model = Place
fields = [
"name",
"description",
"code",
]
widgets = {
"name": forms.TextInput(
attrs={"placeholder": "ex : Centre social Kropotkine"}
),
"description": forms.Textarea(
attrs={"placeholder": "Plus d'infos sur le lieu ? (facultatif)"}
),
"code": forms.TextInput(
attrs={"placeholder": "Identifiant unique du lieu (raccourci)"}
),
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super().__init__(*args, **kwargs)
def save(self, commit=True):
self.instance.orga = get_user_model().objects.get(id=self.user.pk)
return super().save(commit=commit)

View file

@ -0,0 +1,75 @@
from django.conf import settings
from django.db import migrations, models
import base36
import random
def random_code():
return base36.dumps(
random.randint(pow(36, code_length - 2), pow(36, code_length - 1) - 1)
)
# Migrate existing GroupedOrder `place` to the new `place` table
def link_existing_place(apps, schema_editor):
GroupedOrder = apps.get_model('order', 'GroupedOrder')
Place = apps.get_model('order', 'Place')
for grouped_order in GroupedOrder.objects.all():
if grouped_order.place:
# Generate new random code for this existing place
code = random_code()
while Place.objects.all().filter(code=code):
# Random code already exists, try a new random code
code = random_code()
place = Place.objects.create(
name=grouped_order.place,
code=code,
)
grouped_order.places.add(place)
grouped_order.save()
class Migration(migrations.Migration):
dependencies = [
("order", "0029_set_phone_mandatory_for_existing_orders"),
]
operations = [
migrations.CreateModel(
name="Place",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, verbose_name="Nom du lieu de distribution")),
("code", models.CharField(max_length=20, verbose_name="Identifiant unique du lieu (raccourci)", unique=True)),
("orga", models.ForeignKey(on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Organisateur·ice')),
("description", models.TextField(blank=True, null=True, verbose_name="Description")),
],
),
migrations.AddField(
model_name="groupedorder",
name="places",
field=models.ManyToManyField(
to="order.place",
verbose_name="Lieux de distribution",
related_name="orders",
)
),
migrations.RunPython(link_existing_place),
migrations.RemoveField(
model_name='groupedorder',
name='place',
),
migrations.AddField(
model_name="order",
name="place",
field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.CASCADE, to='order.place'),
),
]

View file

@ -21,9 +21,12 @@ class GroupedOrder(models.Model):
max_length=50, null=True, blank=True, verbose_name="Créneau de distribution" max_length=50, null=True, blank=True, verbose_name="Créneau de distribution"
) )
deadline = models.DateTimeField("Date limite de commande") deadline = models.DateTimeField("Date limite de commande")
place = models.CharField(
max_length=100, null=True, blank=True, verbose_name="Lieu de livraison" # Associate with zero/more saved distribution places
places = models.ManyToManyField(
"order.Place", verbose_name="Lieux de distribution", related_name="orders"
) )
description = models.TextField("Description", null=True, blank=True) description = models.TextField("Description", null=True, blank=True)
code = models.CharField(auto_created=True) code = models.CharField(auto_created=True)
is_phone_mandatory = models.BooleanField( is_phone_mandatory = models.BooleanField(
@ -54,6 +57,13 @@ 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 has_places(self):
# Check whether this GroupedOrder has any distribution Place enabled
if len(self.places.all()) > 0:
return True
return False
@property @property
def total_price(self): def total_price(self):
price = 0 price = 0
@ -123,6 +133,9 @@ class Order(models.Model):
author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE) author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE)
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)
place = models.ForeignKey(
"order.place", on_delete=models.CASCADE, null=True, blank=True
)
@property @property
def articles_nb(self): def articles_nb(self):
@ -196,3 +209,22 @@ class OrderedItem(models.Model):
def __str__(self): # pragma: no cover def __str__(self): # pragma: no cover
return f"{self.nb} {self.item}, dans la commande {self.order.pk}" return f"{self.nb} {self.item}, dans la commande {self.order.pk}"
class Place(models.Model):
name = models.CharField(max_length=100, verbose_name="Nom du lieu de distribution")
orga = models.ForeignKey(
AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Organisateur·ice"
)
code = models.CharField(
max_length=20,
verbose_name="Identifiant unique du lieu (raccourci)",
unique=True,
)
description = models.TextField("Description", null=True, blank=True)
def __str__(self): # pragma: no cover
return self.name
def get_absolute_url(self):
return reverse("order:place_update", kwargs={"code": self.code})

View file

@ -27,10 +27,6 @@
{% endif %} {% endif %}
</div> </div>
<div class="column"> <div class="column">
{% if grouped_order.place %}
<p><i class="fa fa-map-pin mr-3" aria-label="Lieu" title="Lieu"
aria-hidden="true"></i>{{ grouped_order.place }}</p>
{% endif %}
<p><i class="fa fa-calendar-check-o mr-3" aria-label="Date limite de commande" <p><i class="fa fa-calendar-check-o mr-3" aria-label="Date limite de commande"
title="Date limite de commande" aria-hidden="true"></i> title="Date limite de commande" aria-hidden="true"></i>
Commandes avant le {{ grouped_order.deadline|date:'d M Y' }} à {{ grouped_order.deadline|date:'H:i' }} Commandes avant le {{ grouped_order.deadline|date:'d M Y' }} à {{ grouped_order.deadline|date:'H:i' }}
@ -171,6 +167,16 @@
<p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label> <p><label for="note">Note à l'organisateur·ice<em> (facultatif)</em> :</label>
<textarea id="note" rows=3 name="note">{{ note }}</textarea></p> <textarea id="note" rows=3 name="note">{{ note }}</textarea></p>
{% if places %}
<label for="place">Point de distribution:</label>
<select name="place" id="place-select">
{% for place in places %}
<option value="{{ place.code }}">{{ place.name }}</option>
{% endfor %}
</select>
{% endif %}
<hr>
<div class="buttons"> <div class="buttons">
<button id="submit" type="submit" value="Order" class="button is-primary"> <button id="submit" type="submit" value="Order" class="button is-primary">
<i class="fa fa-shopping-basket mr-3" aria-hidden="true"></i>Commander <i class="fa fa-shopping-basket mr-3" aria-hidden="true"></i>Commander

View file

@ -184,6 +184,7 @@
<header class="modal-card-head has-background-info"> <header class="modal-card-head has-background-info">
<div class="modal-card-title-container"> <div class="modal-card-title-container">
<p class="modal-card-title mb-2">Commande de {{ order.author }}</p> <p class="modal-card-title mb-2">Commande de {{ order.author }}</p>
{% if order.place %}<p class="has-text-grey-dark">Lieu: {{ order.place }}</p>{% endif %}
<p class="has-text-grey-dark">Le {{ order.created_date|date:'d M Y' }} à {{ order.created_date|date:'H:i' }}</p> <p class="has-text-grey-dark">Le {{ order.created_date|date:'d M Y' }} à {{ order.created_date|date:'H:i' }}</p>
</div> </div>
<button class="delete" aria-label="close"></button> <button class="delete" aria-label="close"></button>

View file

@ -75,64 +75,13 @@
<h2 style="text-align: center"> <h2 style="text-align: center">
{{ grouped_order.name }} - {{ grouped_order.delivery_date }} {{ grouped_order.name }} - {{ grouped_order.delivery_date }}
</h2> </h2>
{% if items %} {% if places %}
<table> {% for place_name, place_orders in places.items %}
<thead> <h3>{{ place_name }}</h3>
<tr> {% include 'order/grouped_order_sheet_list.html' with orders_dict=place_orders items=items grouped_order=grouped_order %}
<th style="font-size: 0.5em; width: 2em">OK</th> {% endfor %}
<th style="text-align: center; width: 20%">Nom</th>
{% for item in items %}
<th class="item_name" style="font-weight: normal;">
<div>{{ item.name }}</div>
</th>
{% endfor %}
<th style="width: 2cm">Prix</th>
</tr>
</thead>
<tbody>
<tr style="background-color: #bababa">
<td></td>
<td style="text-align: left">Prix unitaire</td>
{% for item in items %}
<td>
{{ item.price }} €
</td>
{% endfor %}
<td></td>
</tr>
<tr style="background-color: #bababa">
<th></th>
<th style="text-align: left">TOTAL</th>
{% for item in items %}
<th>
{{ item.ordered_nb }}
</th>
{% endfor %}
<th>{{ grouped_order.total_price }} €</th>
</tr>
{% for order, ordered_items in orders_dict.items %}
<tr>
<td></td>
<td>
{{ order.author.last_name|upper }} {{ order.author.first_name }}
</td>
{% for ordered_item in ordered_items %}
<td>
{% if ordered_item > 0 %}
{{ ordered_item }}
{% endif %}
</td>
{% endfor %}
<td>
{{ order.price }} €
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %} {% else %}
Aucun produit n'a été commandé {% include 'order/grouped_order_sheet_list.html' with orders_dict=orders_dict items=items grouped_order=grouped_order %}
{% endif %} {% endif %}
</body> </body>
</html> </html>

View file

@ -0,0 +1,58 @@
{% if items %}
<table>
<thead>
<tr>
<th style="font-size: 0.5em; width: 2em">OK</th>
<th style="text-align: center; width: 20%">Nom</th>
{% for item in items %}
<th class="item_name" style="font-weight: normal;">
<div>{{ item.name }}</div>
</th>
{% endfor %}
<th style="width: 2cm">Prix</th>
</tr>
</thead>
<tbody>
<tr style="background-color: #bababa">
<td></td>
<td style="text-align: left">Prix unitaire</td>
{% for item in items %}
<td>
{{ item.price }} €
</td>
{% endfor %}
<td></td>
</tr>
<tr style="background-color: #bababa">
<th></th>
<th style="text-align: left">TOTAL</th>
{% for item in items %}
<th>
{{ item.ordered_nb }}
</th>
{% endfor %}
<th>{{ grouped_order.total_price }} €</th>
</tr>
{% for order, ordered_items in orders_dict.items %}
<tr>
<td></td>
<td>
{{ order.author.last_name|upper }} {{ order.author.first_name }}
</td>
{% for ordered_item in ordered_items %}
<td>
{% if ordered_item > 0 %}
{{ ordered_item }}
{% endif %}
</td>
{% endfor %}
<td>
{{ order.price }} €
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
Aucun produit n'a été commandé
{% endif %}

View file

@ -0,0 +1,62 @@
{% extends 'base.html' %}
{% block title %}Mes lieux de livraison{% endblock %}
{% block content %}
<p class="desktop-hidden mobile-content-title">
{% block content_title %}Lieux de distribution que vous organisez{% endblock %}
</p>
<div class="buttons is-pulled-right">
<a class="button is-primary" href="{% url 'order:place_create' %}">
<i class="fa fa-plus-circle mr-3" aria-hidden="true"></i>
Créer un nouveau lieu de distribution</a>
</div>
{% if context.places %}
<table class="table">
<thead>
<tr>
<th>Lieu</th>
<th>Commandes</th>
<th>Description</th>
<th>URL</th>
<th> </th>
</tr>
</thead>
<tbody>
{% for place in context.places %}
<tr>
<td>
<a title="Détail du lieu de distribution" href="{% url 'order:place_update' place.code %}">{{ place }}</a>
</td>
<td>
{% if place.code in context.orders.keys %}
{% for place_url, place_orders in context.orders.items %}
{% if place_url == place.code %}
{% for order in place_orders %}
{% url 'order:grouped_order_detail' code=order.code as order_url %}
{% if order_url %}
<a href="{{ order_url }}">{{order.name}}</a><br>
{% else %}
{{ order.name }}<br>
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% else %}
Aucune
{% endif %}
</td>
<td>
{{ place.description }}
</td>
<td>
{{ place.code }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>Pas de lieux de distribution pour l'instant</p>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Nouveau lieu de distribution{% endblock %}
{% block content %}
<p class="desktop-hidden mobile-content-title">
{% block content_title %}Créer un lieu de distribution{% endblock %}
</p>
<div class="box">
<p class="title">Nouveau lieu</p>
<div class="columns">
<div class="column is-8">
<form method="post" onsubmit="deadlinePassedCheck(event)">
{% csrf_token %}
{{ form | crispy }}
<div class="buttons">
<a class="button is-light" href="{% url 'order:place_index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant">
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title %}Modifier le lieu de livraison {% endblock %}
{% block content %}
<p class="desktop-hidden mobile-content-title">
{% block content_title %}Modifier le lieu de livraison{% endblock %}
</p>
<div class="box">
<p class="title">{{ place.name }} - modifier</p>
<form method="post">{% csrf_token %}
{{ form | crispy }}
<div class="buttons">
<a class="button is-light" href="{% url 'order:place_index' %}">Annuler</a>
<input class="button is-primary" type="submit" value="Suivant">
</div>
</form>
</div>
{% endblock %}

View file

@ -4,77 +4,88 @@ from . import views
app_name = "order" app_name = "order"
urlpatterns = [ urlpatterns = [
path("", views.IndexView.as_view(), name="index"), path("commande/", views.IndexView.as_view(), name="index"),
path( path(
"<str:code>/", "commande/<str:code>/",
views.GroupedOrderDetailView.as_view(), views.GroupedOrderDetailView.as_view(),
name="grouped_order_detail", name="grouped_order_detail",
), ),
path( path(
"<str:code>/ics/", "commande/<str:code>/ics/",
views.GroupedOrderEventView.as_view(), views.GroupedOrderEventView.as_view(),
name="grouped_order_event", name="grouped_order_event",
), ),
path( path(
"<str:code>/gerer", "commande/<str:code>/gerer",
views.GroupedOrderOverview.as_view(), views.GroupedOrderOverview.as_view(),
name="grouped_order_overview", name="grouped_order_overview",
), ),
path("<str:code>/commander/", views.place_order, name="order"), path("commande/<str:code>/commander/", views.place_order, name="order"),
path( path(
"<str:code>/<int:pk>/confirmation/", "commande/<str:code>/<int:pk>/confirmation/",
views.OrderDetailView.as_view(), views.OrderDetailView.as_view(),
name="order_confirm", name="order_confirm",
), ),
path( path(
"<str:code>/gerer/<int:pk>/supprimer", "commande/<str:code>/gerer/<int:pk>/supprimer",
views.OrderDeleteView.as_view(), views.OrderDeleteView.as_view(),
name="order_delete", name="order_delete",
), ),
path("creer", views.GroupedOrderCreateView.as_view(), name="create_grouped_order"),
path( path(
"<str:code>/gerer-produits", "commande/creer",
views.GroupedOrderCreateView.as_view(),
name="create_grouped_order",
),
path(
"commande/<str:code>/gerer-produits",
views.GroupedOrderAddItemsView.as_view(), views.GroupedOrderAddItemsView.as_view(),
name="manage_items", name="manage_items",
), ),
path( path(
"<str:code>/modifier", "commande/<str:code>/modifier",
views.GroupedOrderUpdateView.as_view(), views.GroupedOrderUpdateView.as_view(),
name="update_grouped_order", name="update_grouped_order",
), ),
path( path(
"<str:code>/supprimer", "commande/<str:code>/supprimer",
views.GroupedOrderDeleteView.as_view(), views.GroupedOrderDeleteView.as_view(),
name="delete_grouped_order", name="delete_grouped_order",
), ),
path( path(
"<str:code>/dupliquer", "commande/<str:code>/dupliquer",
views.GroupedOrderDuplicateView.as_view(), views.GroupedOrderDuplicateView.as_view(),
name="duplicate_grouped_order", name="duplicate_grouped_order",
), ),
path( path(
"<str:code>/gerer-produits/nouveau", "commande/<str:code>/gerer-produits/nouveau",
views.ItemCreateView.as_view(), views.ItemCreateView.as_view(),
name="item_create", name="item_create",
), ),
path( path(
"<str:code>/gerer-produits/<int:pk>/supprimer", "commande/<str:code>/gerer-produits/<int:pk>/supprimer",
views.ItemDeleteView.as_view(), views.ItemDeleteView.as_view(),
name="item_delete", name="item_delete",
), ),
path( path(
"<str:code>/gerer/imprimer", "commande/<str:code>/gerer/imprimer",
views.DownloadGroupedOrderSheetView.as_view(), views.DownloadGroupedOrderSheetView.as_view(),
name="grouped_order_sheet", name="grouped_order_sheet",
), ),
path( path(
"<str:code>/gerer/liste-mails", "commande/<str:code>/gerer/liste-mails",
views.ExportGroupOrderEmailAdressesToDownloadView.as_view(), views.ExportGroupOrderEmailAdressesToDownloadView.as_view(),
name="email_list", name="email_list",
), ),
path( path(
"<str:code>/gerer/csv", "commande/<str:code>/gerer/csv",
views.ExportGroupedOrderToCSVView.as_view(), views.ExportGroupedOrderToCSVView.as_view(),
name="grouped_order_csv_export", name="grouped_order_csv_export",
), ),
path("lieu/", views.PlaceIndexView.as_view(), name="place_index"),
path(
"lieu/<str:code>/",
views.PlaceUpdateView.as_view(),
name="place_update",
),
path("lieu/creer", views.PlaceCreateView.as_view(), name="place_create"),
] ]

View file

@ -16,3 +16,4 @@ from .grouped_order import (
) )
from .item import ItemCreateView, ItemDeleteView from .item import ItemCreateView, ItemDeleteView
from .order import OrderDeleteView, OrderDetailView, place_order from .order import OrderDeleteView, OrderDetailView, place_order
from .place import PlaceCreateView, PlaceIndexView, PlaceUpdateView

View file

@ -12,7 +12,7 @@ from django_weasyprint import WeasyTemplateResponseMixin
from icalendar import Calendar, Event, vCalAddress, vText from icalendar import Calendar, Event, vCalAddress, vText
from ..forms import GroupedOrderForm, Item, JoinGroupedOrderForm from ..forms import GroupedOrderForm, Item, JoinGroupedOrderForm
from ..models import GroupedOrder, OrderAuthor from ..models import GroupedOrder, OrderAuthor, Place
from .mixins import UserIsOrgaMixin from .mixins import UserIsOrgaMixin
@ -75,7 +75,8 @@ class GroupedOrderEventView(generic.DetailView):
event.add("dtstart", self.object.delivery_date) event.add("dtstart", self.object.delivery_date)
event.add("dtend", self.object.delivery_date) event.add("dtend", self.object.delivery_date)
event.add("date", self.object.delivery_date) event.add("date", self.object.delivery_date)
event.add("location", vText(self.object.place)) # TODO
# event.add("location", vText(self.object.place))
description = "" description = ""
if self.object.delivery_slot: if self.object.delivery_slot:
@ -136,6 +137,7 @@ class GroupedOrderDetailView(generic.DetailView):
"order_author": order_author, "order_author": order_author,
# Used to set if the phone is required in the form # Used to set if the phone is required in the form
"is_phone_mandatory": grouped_order.is_phone_mandatory, "is_phone_mandatory": grouped_order.is_phone_mandatory,
"places": grouped_order.places.all(),
} }
) )
return context return context
@ -198,6 +200,11 @@ class GroupedOrderUpdateView(UserIsOrgaMixin, generic.UpdateView):
kwargs["user"] = self.request.user kwargs["user"] = self.request.user
return kwargs return kwargs
def get_context_data(self, **kwargs):
context = super(GroupedOrderUpdateView, self).get_context_data(**kwargs)
context["places"] = Place.objects.filter(orga=self.request.user)
return context
class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView): class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
def get_object(self, queryset=None): def get_object(self, queryset=None):
@ -214,10 +221,11 @@ class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView):
orga=self.request.user, orga=self.request.user,
delivery_date=initial_grouped_order.delivery_date, delivery_date=initial_grouped_order.delivery_date,
deadline=initial_grouped_order.deadline, deadline=initial_grouped_order.deadline,
place=initial_grouped_order.place,
description=initial_grouped_order.description, description=initial_grouped_order.description,
) )
# duplicate the places set
new_grouped_order.places.set(initial_grouped_order.places.all())
# create a unique code for the new grouped order # create a unique code for the new grouped order
new_grouped_order.create_code_from_pk() new_grouped_order.create_code_from_pk()
new_grouped_order.save() new_grouped_order.save()
@ -317,6 +325,14 @@ class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView):
context["items"] = items context["items"] = items
context["orders_dict"] = orders_dict context["orders_dict"] = orders_dict
if grouped_order.has_places:
places = dict()
for order, order_items in orders_dict.items():
if order.place.name not in places:
lieux[order.place.name] = dict()
lieux[order.place.name][order] = order_items
context["places"] = places
return context return context
@ -355,6 +371,7 @@ class ExportGroupOrderEmailAdressesToDownloadView(UserPassesTestMixin, generic.V
class ExportGroupedOrderToCSVView(GroupedOrderExportView): class ExportGroupedOrderToCSVView(GroupedOrderExportView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
grouped_order = self.get_object()
super(ExportGroupedOrderToCSVView, self).get(self, request, *args, **kwargs) super(ExportGroupedOrderToCSVView, self).get(self, request, *args, **kwargs)
context = self.get_context_data() context = self.get_context_data()
@ -376,6 +393,8 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView):
row.append("Note") row.append("Note")
row.append("Date") row.append("Date")
row.append("Heure") row.append("Heure")
if grouped_order.has_places:
row.append("Lieu")
writer.writerow(row) writer.writerow(row)
row = ["", "Prix unitaire TTC (€)"] row = ["", "Prix unitaire TTC (€)"]
@ -398,6 +417,8 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView):
row.append(order.note) row.append(order.note)
row.append(order.created_date.strftime("%d/%m/%Y")) row.append(order.created_date.strftime("%d/%m/%Y"))
row.append(order.created_date.strftime("%H:%M")) row.append(order.created_date.strftime("%H:%M"))
if grouped_order.has_places:
row.append(order.place.name)
writer.writerow(row) writer.writerow(row)
# write total row # write total row

View file

@ -7,7 +7,7 @@ from django.views import generic
from la_chariotte.mail.utils import send_order_confirmation_mail from la_chariotte.mail.utils import send_order_confirmation_mail
from ..models import GroupedOrder, Order, OrderAuthor, OrderedItem from ..models import GroupedOrder, Order, OrderAuthor, OrderedItem, Place
def place_order(request, code): def place_order(request, code):
@ -46,6 +46,23 @@ def place_order(request, code):
phone = request.POST["phone"] phone = request.POST["phone"]
email = request.POST["email"] email = request.POST["email"]
note = request.POST["note"] note = request.POST["note"]
# Make sure requested place is valid in this group order (and exists at all)
# If no places are enabled for this group order, chosen place is always None
if grouped_order.has_places:
places = grouped_order.places.all()
# Return 404 if the requested place does not exist at all
place = get_object_or_404(Place, code=request.POST["place"])
if place not in places:
# Return 404 is the requested place exists but is not enabled for this
# GroupedOrder instance
raise http.Http404(
"Le lieu demandé n'est pas valide pour cette commande: %s"
% request.POST["place"]
)
else:
place = None
author = OrderAuthor.objects.create( author = OrderAuthor.objects.create(
first_name=first_name, last_name=last_name, email=email, phone=phone first_name=first_name, last_name=last_name, email=email, phone=phone
) )
@ -54,6 +71,7 @@ def place_order(request, code):
grouped_order=grouped_order, grouped_order=grouped_order,
note=note, note=note,
created_date=timezone.now(), created_date=timezone.now(),
place=place,
) )
# add items to the order # add items to the order
@ -80,6 +98,7 @@ def place_order(request, code):
"error_message": error_message, "error_message": error_message,
"note": order.note, "note": order.note,
"author": author, "author": author,
"place": place,
}, },
) )
@ -98,6 +117,7 @@ def place_order(request, code):
"error_message": error_message, "error_message": error_message,
"note": order.note, "note": order.note,
"author": author, "author": author,
"place": place,
}, },
) )

View file

@ -0,0 +1,83 @@
import csv
import json
from django import http
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views import generic
from django_weasyprint import WeasyTemplateResponseMixin
from icalendar import Calendar, Event, vCalAddress, vText
from la_chariotte.order.models import GroupedOrder
from la_chariotte.order.views.mixins import UserIsOrgaMixin
from ..forms import PlaceForm
from ..models import Place
class PlaceIndexView(LoginRequiredMixin, generic.ListView):
"""View showing all the distribution places managed by the authenticated user"""
template_name = "place/index.html"
context_object_name = "context"
def get_queryset(self):
places = Place.objects.filter(orga=self.request.user)
print(places)
# Let's filter orders by distribution place (for UI grouping)
orders = dict()
for place in places:
# TODO: maybe filter out finished GroupedOrder?
if place.orders.all():
orders[place.code] = place.orders.all()
# orders[place.code] = orders
return {
"places": places,
"orders": orders,
}
class PlaceUpdateView(UserIsOrgaMixin, generic.UpdateView):
"""View showing details and allowing updates to a distribution place"""
model = Place
template_name = "place/place_update.html"
context_object_name = "place"
form_class = PlaceForm
# Prevent URL change after creation
form_class.base_fields["code"].disabled = True
def get_object(self, queryset=None):
return get_object_or_404(Place, code=self.kwargs.get("code"))
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
class PlaceCreateView(LoginRequiredMixin, generic.CreateView):
"""View for creating a new distribution place"""
model = Place
form_class = PlaceForm
template_name = "place/place_create.html"
# Allow setting URL for creation
form_class.base_fields["code"].disabled = False
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form):
self.object = form.save()
return super().form_valid(form)

View file

@ -7,7 +7,7 @@ from sentry_sdk.integrations.django import DjangoIntegration
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
BASE_URL = os.getenv("BASE_URL", "http://127.0.0.1:8000") BASE_URL = os.getenv("BASE_URL", "http://127.0.0.1:8000")
PROJECT_NAME = os.getenv("PROJECT_NAME", "La Chariotte") PROJECT_NAME = os.getenv("PROJECT_NAME", "La Chariotte")
GITLAB_LINK = "https://gitlab.com/la-chariotte/la_chariotte" GITLAB_LINK = "https://framagit.org/la-chariotte/la-chariotte"
CONTACT_MAIL = "contact@chariotte.fr" CONTACT_MAIL = "contact@chariotte.fr"
HELLOASSO_LINK = "https://www.helloasso.com/associations/la-chariotte/" HELLOASSO_LINK = "https://www.helloasso.com/associations/la-chariotte/"
FEEDBACK_LINK = "https://framaforms.org/votre-avis-sur-la-chariotte-1709742328" FEEDBACK_LINK = "https://framaforms.org/votre-avis-sur-la-chariotte-1709742328"

View file

@ -920,7 +920,7 @@ class TestGroupedOrderUpdateView:
grouped_order.save() grouped_order.save()
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test" assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().place == None assert models.GroupedOrder.objects.first().places.count() == 0
# get the update form # get the update form
update_grouped_order_url = reverse( update_grouped_order_url = reverse(
@ -942,7 +942,7 @@ class TestGroupedOrderUpdateView:
) )
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test" assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().place == None assert models.GroupedOrder.objects.first().places.count() == 0
# get the update form # get the update form
update_grouped_order_url = reverse( update_grouped_order_url = reverse(
@ -954,6 +954,9 @@ class TestGroupedOrderUpdateView:
# post the update form # post the update form
date = timezone.now().date() + datetime.timedelta(days=42) date = timezone.now().date() + datetime.timedelta(days=42)
deadline = timezone.now() + datetime.timedelta(days=32) deadline = timezone.now() + datetime.timedelta(days=32)
place = models.Place.objects.create(
code="foobar", name="quelque part", orga=auth.get_user(client_log)
)
response = client_log.post( response = client_log.post(
update_grouped_order_url, update_grouped_order_url,
{ {
@ -961,14 +964,15 @@ class TestGroupedOrderUpdateView:
"deadline_date": deadline.date(), "deadline_date": deadline.date(),
"deadline_time": deadline.time().strftime("%H:%M"), "deadline_time": deadline.time().strftime("%H:%M"),
"delivery_date": date, "delivery_date": date,
"place": "quelque part", "places": place.id,
}, },
) )
# assert response.content.decode() == ""
assert response.status_code == 302 assert response.status_code == 302
assert response.url.endswith("gerer-produits") assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "titre test modifié" assert models.GroupedOrder.objects.first().name == "titre test modifié"
assert models.GroupedOrder.objects.first().place == "quelque part" assert models.GroupedOrder.objects.first().places.first().name == "quelque part"
def test_update_grouped_order__delivery_date_passed(self, client_log): def test_update_grouped_order__delivery_date_passed(self, client_log):
""" """
@ -983,7 +987,7 @@ class TestGroupedOrderUpdateView:
) )
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "gr order test" assert models.GroupedOrder.objects.first().name == "gr order test"
assert models.GroupedOrder.objects.first().place == None assert models.GroupedOrder.objects.first().places.count() == 0
# get the update form # get the update form
update_grouped_order_url = reverse( update_grouped_order_url = reverse(
@ -995,6 +999,9 @@ class TestGroupedOrderUpdateView:
# post the update form # post the update form
date = timezone.now().date() + datetime.timedelta(days=-1) date = timezone.now().date() + datetime.timedelta(days=-1)
deadline = timezone.now() + datetime.timedelta(days=-3) deadline = timezone.now() + datetime.timedelta(days=-3)
place = models.Place.objects.create(
code="foobar", name="quelque part", orga=auth.get_user(client_log)
)
response = client_log.post( response = client_log.post(
update_grouped_order_url, update_grouped_order_url,
{ {
@ -1002,14 +1009,14 @@ class TestGroupedOrderUpdateView:
"deadline_date": deadline.date(), "deadline_date": deadline.date(),
"deadline_time": deadline.time().strftime("%H:%M"), "deadline_time": deadline.time().strftime("%H:%M"),
"delivery_date": date, "delivery_date": date,
"place": "quelque part", "places": place.id,
}, },
) )
assert response.status_code == 302 assert response.status_code == 302
assert response.url.endswith("gerer-produits") assert response.url.endswith("gerer-produits")
assert models.GroupedOrder.objects.count() == 1 assert models.GroupedOrder.objects.count() == 1
assert models.GroupedOrder.objects.first().name == "titre test modifié" assert models.GroupedOrder.objects.first().name == "titre test modifié"
assert models.GroupedOrder.objects.first().place == "quelque part" assert models.GroupedOrder.objects.first().places.first().name == "quelque part"
def test_update_grouped_order__not_orga(self, client_log, other_user): def test_update_grouped_order__not_orga(self, client_log, other_user):
"""A user that is not organiszer of the GO accesses update page. """A user that is not organiszer of the GO accesses update page.
@ -1315,7 +1322,9 @@ class TestGroupedOrderDuplicateView:
assert new_grouped_order.name == "gr order test - copie" assert new_grouped_order.name == "gr order test - copie"
assert new_grouped_order.delivery_date == grouped_order.delivery_date assert new_grouped_order.delivery_date == grouped_order.delivery_date
assert new_grouped_order.deadline == grouped_order.deadline assert new_grouped_order.deadline == grouped_order.deadline
assert new_grouped_order.place == grouped_order.place assert [x.code for x in new_grouped_order.places.all()] == [
x.code for x in grouped_order.places.all()
]
assert new_grouped_order.orga == auth.get_user(client_log) assert new_grouped_order.orga == auth.get_user(client_log)
assert new_grouped_order.description == grouped_order.description assert new_grouped_order.description == grouped_order.description
assert new_grouped_order.item_set.count() == grouped_order.item_set.count() assert new_grouped_order.item_set.count() == grouped_order.item_set.count()

View file

@ -29,7 +29,8 @@ from la_chariotte.order.views.stats import stats
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("commande/", include("la_chariotte.order.urls")), # No route specified because there are multiple subroutes defined in order.urls
path("", include("la_chariotte.order.urls")),
path("comptes/", include("la_chariotte.accounts.urls")), path("comptes/", include("la_chariotte.accounts.urls")),
# Some paths for accounts are easier to leave here # Some paths for accounts are easier to leave here
# - PasswordResetView sends the mail # - PasswordResetView sends the mail

View file

@ -1,7 +1,7 @@
site_name: La chariotte site_name: La chariotte
site_description: An application for grouped-orders site_description: An application for grouped-orders
repo_name: la-chariotte/la_chariotte repo_name: la-chariotte/la_chariotte
repo_url: https://gitlab.com/la-chariotte/la_chariotte repo_url: https://framagit.org/la-chariotte/la-chariotte
nav: nav:
- How-tos: - How-tos:
- Getting started: install.md - Getting started: install.md