diff --git a/la_chariotte/lieu/__init__.py b/la_chariotte/lieu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/la_chariotte/lieu/apps.py b/la_chariotte/lieu/apps.py new file mode 100644 index 0000000..717814c --- /dev/null +++ b/la_chariotte/lieu/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LieuConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "la_chariotte.lieu" diff --git a/la_chariotte/lieu/forms.py b/la_chariotte/lieu/forms.py new file mode 100644 index 0000000..f84cd32 --- /dev/null +++ b/la_chariotte/lieu/forms.py @@ -0,0 +1,35 @@ +import datetime + +from django import forms +from django.contrib.auth import get_user_model +from django.forms.utils import to_current_timezone +from django.utils import timezone + +from la_chariotte.lieu.models import Lieu + + +class LieuForm(forms.ModelForm): + class Meta: + model = Lieu + fields = [ + "name", + "description", + "url", + ] + widgets = { + "name": forms.TextInput( + attrs={"placeholder": "ex : Centre social Kropotkine"} + ), + "description": forms.Textarea( + attrs={"placeholder": "Plus d'infos sur le lieu ? (facultatif)"} + ), + "url": forms.TextInput(attrs={"placeholder": "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) diff --git a/la_chariotte/lieu/migrations/0001_initial.py b/la_chariotte/lieu/migrations/0001_initial.py new file mode 100644 index 0000000..efabf0f --- /dev/null +++ b/la_chariotte/lieu/migrations/0001_initial.py @@ -0,0 +1,28 @@ +from django.conf import settings +from django.db import migrations, models + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Lieu", + 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")), + ("url", models.CharField(max_length=20, verbose_name='Portion du lien pour le lieu', 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")), + ], + ), + ] diff --git a/la_chariotte/lieu/migrations/__init__.py b/la_chariotte/lieu/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/la_chariotte/lieu/models.py b/la_chariotte/lieu/models.py new file mode 100644 index 0000000..173a3f8 --- /dev/null +++ b/la_chariotte/lieu/models.py @@ -0,0 +1,27 @@ +import random + +import base36 +from django.core.exceptions import ValidationError +from django.db import models +from django.urls import reverse +from django.utils import timezone + +from la_chariotte.order.models import GroupedOrder +from la_chariotte.settings import AUTH_USER_MODEL + + +class Lieu(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" + ) + url = models.CharField( + max_length=20, verbose_name="Portion du lien pour le lieu", 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("lieu:lieu_update", kwargs={"url": self.url}) diff --git a/la_chariotte/lieu/templates/lieu/index.html b/la_chariotte/lieu/templates/lieu/index.html new file mode 100644 index 0000000..e07c72c --- /dev/null +++ b/la_chariotte/lieu/templates/lieu/index.html @@ -0,0 +1,62 @@ +{% extends 'base.html' %} + +{% block title %}Mes lieux de livraison{% endblock %} + +{% block content %} +

+ {% block content_title %}Lieux de distribution que vous organisez{% endblock %} +

+
+ + + Créer un nouveau lieu de distribution +
+ {% if lieu_context.lieu_liste %} + + + + + + + + + + + + {% for lieu in lieu_context.lieu_liste %} + + + + + + + {% endfor %} + +
LieuCommandesDescriptionURL
+ {{ lieu }} + + {% if lieu.url in lieu_context.commandes.keys %} + {% for lieu_url, lieu_commandes in lieu_context.commandes.items %} + {% if lieu_url == lieu.url %} + {% for commande in lieu_commandes %} + {% url 'order:grouped_order_detail' code=commande.code as order_url %} + {% if order_url %} + {{commande.name}}
+ {% else %} + {{ commande.name }}
+ {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% else %} + Aucune + {% endif %} +
+ {{ lieu.description }} + + {{ lieu.url }} +
+ {% else %} +

Pas de lieux de distribution pour l'instant

+ {% endif %} +{% endblock %} diff --git a/la_chariotte/lieu/templates/lieu/lieu_create.html b/la_chariotte/lieu/templates/lieu/lieu_create.html new file mode 100644 index 0000000..9c64130 --- /dev/null +++ b/la_chariotte/lieu/templates/lieu/lieu_create.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} + +{% load crispy_forms_tags %} + +{% block title %}Nouveau lieu de distribution{% endblock %} + +{% block content %} +

+ {% block content_title %}Créer un lieu de distribution{% endblock %} +

+
+

Nouvelle lieu

+
+
+
+ {% csrf_token %} + {{ form | crispy }} +
+ Annuler + +
+
+
+
+
+{% endblock %} diff --git a/la_chariotte/lieu/templates/lieu/lieu_detail.html b/la_chariotte/lieu/templates/lieu/lieu_detail.html new file mode 100644 index 0000000..e0ffa2a --- /dev/null +++ b/la_chariotte/lieu/templates/lieu/lieu_detail.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} + +{% block title %}{{ lieu }}{% endblock %} + +{% block content %} +

+ {% block content_title %}Modifier le lieu de livraison{% endblock %} +

+
+

{{ lieu.name }} - modifier

+
{% csrf_token %} + {{ form | crispy }} +
+ Annuler + +
+
+
+ +{% endblock %} + diff --git a/la_chariotte/lieu/templates/lieu/lieu_update.html b/la_chariotte/lieu/templates/lieu/lieu_update.html new file mode 100644 index 0000000..41f1ec2 --- /dev/null +++ b/la_chariotte/lieu/templates/lieu/lieu_update.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% load crispy_forms_tags %} + +{% block title %}Modifier le lieu de livraison {% endblock %} + +{% block content %} +

+ {% block content_title %}Modifier le lieu de livraison{% endblock %} +

+
+

{{ lieu.name }} - modifier

+
{% csrf_token %} + {{ form | crispy }} +
+ Annuler + +
+
+
+ +{% endblock %} diff --git a/la_chariotte/lieu/urls.py b/la_chariotte/lieu/urls.py new file mode 100644 index 0000000..e359279 --- /dev/null +++ b/la_chariotte/lieu/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from . import views + +app_name = "lieu" +urlpatterns = [ + path("", views.IndexView.as_view(), name="index"), + path( + "/", + views.LieuUpdateView.as_view(), + name="lieu_update", + ), + path("creer", views.LieuCreateView.as_view(), name="create_lieu"), +] diff --git a/la_chariotte/lieu/views/__init__.py b/la_chariotte/lieu/views/__init__.py new file mode 100644 index 0000000..c5c9e15 --- /dev/null +++ b/la_chariotte/lieu/views/__init__.py @@ -0,0 +1,6 @@ +from .lieu import ( + IndexView, + LieuCreateView, + LieuDetailView, + LieuUpdateView, +) diff --git a/la_chariotte/lieu/views/lieu.py b/la_chariotte/lieu/views/lieu.py new file mode 100644 index 0000000..57acc96 --- /dev/null +++ b/la_chariotte/lieu/views/lieu.py @@ -0,0 +1,87 @@ +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 LieuForm +from ..models import Lieu + + +class IndexView(LoginRequiredMixin, generic.ListView): + """View showing all the grouped orders managed by the authenticated user""" + + template_name = "lieu/index.html" + context_object_name = "lieu_context" + + def get_queryset(self): + lieux = Lieu.objects.filter(orga=self.request.user) + commandes_par_lieu = dict() + for lieu in lieux: + orders = GroupedOrder.objects.all().filter(placekey=lieu) + if orders: + commandes_par_lieu[lieu.url] = orders + + return { + "lieu_liste": lieux, + "commandes": commandes_par_lieu, + } + + +class LieuDetailView(generic.DetailView): + model = Lieu + template_name = "lieu/lieu_detail.html" + context_object_name = "lieu_context" + form_class = LieuForm + + def get_object(self, queryset=None): + return get_object_or_404(Lieu, url=self.kwargs.get("url")) + + def get_context_data(self, **kwargs): + lieu = self.get_object() + + context = super().get_context_data(**kwargs) + return context + + +class LieuUpdateView(UserIsOrgaMixin, generic.UpdateView): + model = Lieu + template_name = "lieu/lieu_update.html" + context_object_name = "lieu" + form_class = LieuForm + # Can't change URL after creation + form_class.base_fields["url"].disabled = True + + def get_object(self, queryset=None): + return get_object_or_404(Lieu, url=self.kwargs.get("url")) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["user"] = self.request.user + + return kwargs + + +class LieuCreateView(LoginRequiredMixin, generic.CreateView): + model = Lieu + form_class = LieuForm + template_name = "lieu/lieu_create.html" + + 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) diff --git a/la_chariotte/order/forms.py b/la_chariotte/order/forms.py index 2cf2817..2b4335d 100644 --- a/la_chariotte/order/forms.py +++ b/la_chariotte/order/forms.py @@ -1,10 +1,12 @@ import datetime from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth import get_user_model from django.forms.utils import to_current_timezone from django.utils import timezone +from la_chariotte.lieu.models import Lieu from la_chariotte.order.models import GroupedOrder, Item @@ -22,6 +24,12 @@ class GroupedOrderForm(forms.ModelForm): label="Numéro de téléphone obligatoire pour les participants", required=False, ) + placekey = forms.ModelMultipleChoiceField( + label="Lieux de distribution", + queryset=Lieu.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) class Meta: model = GroupedOrder @@ -34,6 +42,7 @@ class GroupedOrderForm(forms.ModelForm): "place", "description", "is_phone_mandatory", + "placekey", ] widgets = { "name": forms.TextInput( diff --git a/la_chariotte/order/migrations/0030_add_lieu.py b/la_chariotte/order/migrations/0030_add_lieu.py new file mode 100644 index 0000000..7464e5b --- /dev/null +++ b/la_chariotte/order/migrations/0030_add_lieu.py @@ -0,0 +1,23 @@ +from django.db import migrations, models + +class Migration(migrations.Migration): + dependencies = [ + ("order", "0029_set_phone_mandatory_for_existing_orders"), + ("lieu", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="groupedorder", + name="placekey", + field=models.ManyToManyField( + verbose_name="Lieu de distribution", + to="lieu.lieu", + ), + ), + migrations.AddField( + model_name="order", + name="placekey", + field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.CASCADE, to='lieu.lieu'), + ), + ] diff --git a/la_chariotte/order/models.py b/la_chariotte/order/models.py index 5450f72..03d060c 100644 --- a/la_chariotte/order/models.py +++ b/la_chariotte/order/models.py @@ -21,9 +21,17 @@ class GroupedOrder(models.Model): max_length=50, null=True, blank=True, verbose_name="Créneau de distribution" ) deadline = models.DateTimeField("Date limite de commande") + + # Try to associate with a saved distribution place in DB + placekey = models.ManyToManyField( + "lieu.Lieu", + verbose_name="Lieu de distribution", + ) + # Alternatively, propose a free-text distribution place field place = models.CharField( max_length=100, null=True, blank=True, verbose_name="Lieu de livraison" ) + description = models.TextField("Description", null=True, blank=True) code = models.CharField(auto_created=True) is_phone_mandatory = models.BooleanField( @@ -123,6 +131,9 @@ class Order(models.Model): author = models.ForeignKey(OrderAuthor, on_delete=models.CASCADE) created_date = models.DateTimeField("Date et heure de commande", auto_now_add=True) note = models.TextField(max_length=200, null=True, blank=True) + placekey = models.ForeignKey( + "lieu.lieu", on_delete=models.CASCADE, null=True, blank=True + ) @property def articles_nb(self): diff --git a/la_chariotte/order/templates/order/grouped_order_detail.html b/la_chariotte/order/templates/order/grouped_order_detail.html index b73d467..96fb8fa 100644 --- a/la_chariotte/order/templates/order/grouped_order_detail.html +++ b/la_chariotte/order/templates/order/grouped_order_detail.html @@ -171,6 +171,17 @@

+ {% if placekey %} + + + {% endif %} + +
+
@@ -370,4 +371,4 @@ }); {% endblock %} - \ No newline at end of file + diff --git a/la_chariotte/order/templates/order/grouped_order_sheet.html b/la_chariotte/order/templates/order/grouped_order_sheet.html index 3b5a276..ba6f871 100644 --- a/la_chariotte/order/templates/order/grouped_order_sheet.html +++ b/la_chariotte/order/templates/order/grouped_order_sheet.html @@ -75,64 +75,13 @@

{{ grouped_order.name }} - {{ grouped_order.delivery_date }}

- {% if items %} - - - - - - {% for item in items %} - - {% endfor %} - - - - - - - - {% for item in items %} - - {% endfor %} - - - - - - {% for item in items %} - - {% endfor %} - - - {% for order, ordered_items in orders_dict.items %} - - - - {% for ordered_item in ordered_items %} - - {% endfor %} - - - {% endfor %} - -
OKNom -
{{ item.name }}
-
Prix
Prix unitaire - {{ item.price }} € -
TOTAL - {{ item.ordered_nb }} - {{ grouped_order.total_price }} €
- {{ order.author.last_name|upper }} {{ order.author.first_name }} - - {% if ordered_item > 0 %} - {{ ordered_item }} - {% endif %} - - {{ order.price }} € -
+ {% if by_lieu %} + {% for lieu_name, ordered_dict in lieux.items %} +

{{ lieu_name }}

+ {% include 'order/grouped_order_sheet_list.html' with orders_dict=lieu_orders items=items grouped_order=grouped_order %} + {% endfor %} {% 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 %} - diff --git a/la_chariotte/order/templates/order/grouped_order_sheet_list.html b/la_chariotte/order/templates/order/grouped_order_sheet_list.html new file mode 100644 index 0000000..8f70d32 --- /dev/null +++ b/la_chariotte/order/templates/order/grouped_order_sheet_list.html @@ -0,0 +1,58 @@ +{% if items %} + + + + + + {% for item in items %} + + {% endfor %} + + + + + + + + {% for item in items %} + + {% endfor %} + + + + + + {% for item in items %} + + {% endfor %} + + + {% for order, ordered_items in orders_dict.items %} + + + + {% for ordered_item in ordered_items %} + + {% endfor %} + + + {% endfor %} + +
OKNom +
{{ item.name }}
+
Prix
Prix unitaire + {{ item.price }} € +
TOTAL + {{ item.ordered_nb }} + {{ grouped_order.total_price }} €
+ {{ order.author.last_name|upper }} {{ order.author.first_name }} + + {% if ordered_item > 0 %} + {{ ordered_item }} + {% endif %} + + {{ order.price }} € +
+{% else %} + Aucun produit n'a été commandé +{% endif %} diff --git a/la_chariotte/order/views/grouped_order.py b/la_chariotte/order/views/grouped_order.py index 55d9cca..9c30d34 100644 --- a/la_chariotte/order/views/grouped_order.py +++ b/la_chariotte/order/views/grouped_order.py @@ -11,6 +11,8 @@ from django.views import generic from django_weasyprint import WeasyTemplateResponseMixin from icalendar import Calendar, Event, vCalAddress, vText +from la_chariotte.lieu.models import Lieu + from ..forms import GroupedOrderForm, Item, JoinGroupedOrderForm from ..models import GroupedOrder, OrderAuthor from .mixins import UserIsOrgaMixin @@ -136,6 +138,7 @@ class GroupedOrderDetailView(generic.DetailView): "order_author": order_author, # Used to set if the phone is required in the form "is_phone_mandatory": grouped_order.is_phone_mandatory, + "placekey": grouped_order.placekey.all(), } ) return context @@ -198,6 +201,11 @@ class GroupedOrderUpdateView(UserIsOrgaMixin, generic.UpdateView): kwargs["user"] = self.request.user return kwargs + def get_context_data(self, **kwargs): + context = super(GroupedOrderUpdateView, self).get_context_data(**kwargs) + context["places"] = Lieu.objects.filter(orga=self.request.user) + return context + class GroupedOrderDuplicateView(UserIsOrgaMixin, generic.RedirectView): def get_object(self, queryset=None): @@ -296,6 +304,8 @@ class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView): context = super(GroupedOrderExportView, self).get_context_data(**kwargs) grouped_order = self.get_object() + by_lieu = len(grouped_order.placekey.all()) > 0 + items = [ item for item in grouped_order.item_set.all().order_by("name") @@ -316,6 +326,14 @@ class GroupedOrderExportView(UserIsOrgaMixin, generic.DetailView): context["items"] = items context["orders_dict"] = orders_dict + context["by_lieu"] = by_lieu + if by_lieu: + lieux = dict() + for order, order_items in orders_dict.items(): + if order.placekey.name not in lieux: + lieux[order.placekey.name] = dict() + lieux[order.placekey.name][order] = order_items + context["lieux"] = lieux return context @@ -355,6 +373,8 @@ class ExportGroupOrderEmailAdressesToDownloadView(UserPassesTestMixin, generic.V class ExportGroupedOrderToCSVView(GroupedOrderExportView): def get(self, request, *args, **kwargs): + grouped_order = self.get_object() + enableLieu = len(grouped_order.placekey.all()) > 0 super(ExportGroupedOrderToCSVView, self).get(self, request, *args, **kwargs) context = self.get_context_data() @@ -376,6 +396,8 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView): row.append("Note") row.append("Date") row.append("Heure") + if enableLieu: + row.append("Lieu") writer.writerow(row) row = ["", "Prix unitaire TTC (€)"] @@ -398,6 +420,8 @@ class ExportGroupedOrderToCSVView(GroupedOrderExportView): row.append(order.note) row.append(order.created_date.strftime("%d/%m/%Y")) row.append(order.created_date.strftime("%H:%M")) + if enableLieu: + row.append(order.placekey.name) writer.writerow(row) # write total row diff --git a/la_chariotte/order/views/order.py b/la_chariotte/order/views/order.py index 032b219..7246129 100644 --- a/la_chariotte/order/views/order.py +++ b/la_chariotte/order/views/order.py @@ -5,6 +5,7 @@ from django.urls import reverse, reverse_lazy from django.utils import timezone from django.views import generic +from la_chariotte.lieu.models import Lieu from la_chariotte.mail.utils import send_order_confirmation_mail from ..models import GroupedOrder, Order, OrderAuthor, OrderedItem @@ -13,6 +14,7 @@ from ..models import GroupedOrder, Order, OrderAuthor, OrderedItem def place_order(request, code): # Creates an AnonymousUser and an Order with related OrderedItems grouped_order = get_object_or_404(GroupedOrder, code=code) + places = grouped_order.placekey.all() # Handle permissions user_is_orga = request.user == grouped_order.orga @@ -46,6 +48,19 @@ def place_order(request, code): phone = request.POST["phone"] email = request.POST["email"] note = request.POST["note"] + + # Make sure requested location is valid in this group order (and exists at all) + # If no placekey is enabled for this group order, placekey is always null + if len(places) == 0: + placekey = None + else: + placekey = get_object_or_404(Lieu, url=request.POST["placekey"]) + if placekey not in places: + raise http.Http404( + "Le lieu demandé n'est pas valide pour cette commande: %s" + % requested_placekey + ) + author = OrderAuthor.objects.create( first_name=first_name, last_name=last_name, email=email, phone=phone ) @@ -54,6 +69,7 @@ def place_order(request, code): grouped_order=grouped_order, note=note, created_date=timezone.now(), + placekey=placekey, ) # add items to the order @@ -80,6 +96,7 @@ def place_order(request, code): "error_message": error_message, "note": order.note, "author": author, + "placekey": placekey, }, ) @@ -98,6 +115,7 @@ def place_order(request, code): "error_message": error_message, "note": order.note, "author": author, + "placekey": placekey, }, ) diff --git a/la_chariotte/settings.py b/la_chariotte/settings.py index b9ef0da..669da34 100644 --- a/la_chariotte/settings.py +++ b/la_chariotte/settings.py @@ -27,6 +27,7 @@ if os.getenv("ALLOWED_HOSTS"): # Applications & middlewares INSTALLED_APPS = [ + "la_chariotte.lieu", "la_chariotte.order", "la_chariotte.accounts", "la_chariotte.mail", diff --git a/la_chariotte/urls.py b/la_chariotte/urls.py index a10cf50..6754f41 100644 --- a/la_chariotte/urls.py +++ b/la_chariotte/urls.py @@ -30,6 +30,7 @@ from la_chariotte.order.views.stats import stats urlpatterns = [ path("admin/", admin.site.urls), path("commande/", include("la_chariotte.order.urls")), + path("lieu/", include("la_chariotte.lieu.urls")), path("comptes/", include("la_chariotte.accounts.urls")), # Some paths for accounts are easier to leave here # - PasswordResetView sends the mail