1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-12-18 13:32:28 +00:00

Add order interface

Signed-off-by: Emmy D'ANELLO <ynerant@crans.org>
This commit is contained in:
Emmy D'ANELLO 2022-08-18 17:27:59 +02:00
parent 5174c84b33
commit 45334e4e02
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
10 changed files with 371 additions and 59 deletions

View File

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body">
{% crispy form %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,34 @@
# Generated by Django 2.2.27 on 2022-08-18 15:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sheets', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='order',
name='gift',
),
migrations.AddField(
model_name='orderedfood',
name='gift',
field=models.IntegerField(default=0, verbose_name='gift'),
preserve_default=False,
),
migrations.AddField(
model_name='orderedmeal',
name='gift',
field=models.IntegerField(default=0, verbose_name='gift'),
preserve_default=False,
),
migrations.AlterField(
model_name='orderedfood',
name='status',
field=models.CharField(choices=[('QUEUED', 'queued'), ('READY', 'ready'), ('SERVED', 'served'), ('CANCELED', 'canceled')], default='QUEUED', max_length=8, verbose_name='status'),
),
]

View File

@ -137,7 +137,7 @@ class Meal(models.Model):
)
def __str__(self):
return self.name
return _("meal").capitalize() + " " + self.name
class Meta:
verbose_name = _("meal")
@ -162,10 +162,6 @@ class Order(models.Model):
auto_now_add=True,
)
gift = models.IntegerField(
verbose_name=_("gift"),
)
class Meta:
verbose_name = _("order")
verbose_name_plural = _("orders")
@ -184,6 +180,10 @@ class OrderedMeal(models.Model):
verbose_name=_("meal"),
)
gift = models.IntegerField(
verbose_name=_("gift"),
)
class Meta:
verbose_name = _("ordered meal")
verbose_name_plural = _("ordered meals")
@ -229,6 +229,10 @@ class OrderedFood(models.Model):
verbose_name=_("priority request"),
)
gift = models.IntegerField(
verbose_name=_("gift"),
)
number = models.IntegerField(
verbose_name=_("number"),
help_text=_("How many times the user ordered this."),
@ -242,6 +246,7 @@ class OrderedFood(models.Model):
('SERVED', _("served")),
('CANCELED', _("canceled")),
],
default='QUEUED',
verbose_name=_("status"),
)
@ -268,14 +273,16 @@ class SheetOrderTransaction(Transaction):
return _("note sheet")
@property
def price(self):
def get_price(self):
if self.ordered_food.meal:
return sum(ordered_food.price + sum(opt.extra_cost for opt in ordered_food.options.all())
return self.ordered_food.meal.meal.price + self.ordered_food.meal.gift + sum(
sum(opt.extra_cost for opt in ordered_food.options.all())
for ordered_food in self.ordered_food.meal.orderedfood_set.exclude(status='CANCELED').all())
elif self.ordered_food.status == 'CANCELED':
return 0
else:
return self.ordered_food.food.price + sum(opt.extra_cost for opt in self.ordered_food.options.all())
return self.ordered_food.food.price + self.ordered_food.gift \
+ sum(opt.extra_cost for opt in self.ordered_food.options.all())
class Meta:
verbose_name = _("sheet order transaction")

View File

@ -1,11 +1,11 @@
{% extends "wei/base.html" %}
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block profile_content %}
{% block content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{{ title }}

View File

@ -1,11 +1,11 @@
{% extends "wei/base.html" %}
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block profile_content %}
{% block content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{{ title }}

View File

@ -75,7 +75,7 @@
</div>
</div>
<div class="card-footer text-center">
<a class="btn btn-success">
<a href="{% url 'sheets:sheet_order' pk=sheet.pk %}" class="btn btn-success">
{% trans "Order now" %}
</a>
</div>

View File

@ -1,11 +1,11 @@
{% extends "wei/base.html" %}
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block profile_content %}
{% block content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{{ title }}

View File

@ -3,7 +3,7 @@
from django.urls import path
from sheets.views import FoodCreateView, FoodUpdateView, MealCreateView, MealUpdateView, \
from sheets.views import FoodCreateView, FoodUpdateView, MealCreateView, MealUpdateView, OrderView, \
SheetCreateView, SheetDetailView, SheetListView, SheetUpdateView
app_name = 'sheets'
@ -17,4 +17,5 @@ urlpatterns = [
path('food/<int:pk>/update/', FoodUpdateView.as_view(), name="food_update"),
path('meal/create/<int:pk>/', MealCreateView.as_view(), name="meal_create"),
path('meal/<int:pk>/update/', MealUpdateView.as_view(), name="meal_update"),
path('order/<int:pk>/', OrderView.as_view(), name="sheet_order"),
]

View File

@ -1,18 +1,28 @@
# Copyright (C) 2018-2022 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from crispy_forms.bootstrap import Accordion, AccordionGroup, FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Fieldset, Submit, Row, Field
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
from django.forms import Form
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView
from django.views.generic import DetailView, UpdateView, FormView
from django_tables2 import SingleTableView
from note.models import Alias, Note
from note.templatetags.pretty_money import pretty_money
from note_kfet.inputs import AmountInput, Autocomplete
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import FoodForm, MealForm, SheetForm, FoodOptionsFormSet, FoodOptionFormSetHelper
from .models import Sheet, Food, Meal
from .models import Sheet, Food, Meal, Order, OrderedMeal, OrderedFood, SheetOrderTransaction
from .tables import SheetTable
@ -45,13 +55,13 @@ class SheetCreateView(ProtectQuerysetMixin, ProtectedCreateView):
)
class SheetUpdateView(ProtectQuerysetMixin, UpdateView):
class SheetUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Sheet
form_class = SheetForm
extra_context = {"title": _("Update note sheet")}
class SheetDetailView(ProtectQuerysetMixin, DetailView):
class SheetDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Sheet
def get_context_data(self, **kwargs):
@ -114,7 +124,7 @@ class FoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('sheets:sheet_detail', args=(self.kwargs['pk'],))
class FoodUpdateView(ProtectQuerysetMixin, UpdateView):
class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Food
form_class = FoodForm
extra_context = {"title": _("Update food")}
@ -176,7 +186,7 @@ class MealCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('sheets:sheet_detail', args=(self.object.sheet_id,))
class MealUpdateView(ProtectQuerysetMixin, UpdateView):
class MealUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Meal
form_class = MealForm
extra_context = {"title": _("Update meal")}
@ -188,3 +198,211 @@ class MealUpdateView(ProtectQuerysetMixin, UpdateView):
def get_success_url(self):
return reverse_lazy('sheets:sheet_detail', args=(self.object.sheet_id,))
class OrderView(LoginRequiredMixin, FormView, DetailView):
model = Sheet
template_name = 'sheets/order.html'
extra_context = {'title': _("Order now")}
def get_form(self, form_class=None):
form = Form()
form.helper = FormHelper()
layout_fields = []
self.object = self.get_object()
form.fields['note'] = forms.ModelChoiceField(
queryset=Note.objects.filter(PermissionBackend.filter_queryset(self.request, Note, 'note.view_note')),
label=_("Orderer"),
initial=self.request.user.note,
widget=Autocomplete(
model=Note,
attrs={
"api_url": "/api/note/note/",
'placeholder': _("Who orders")
},
),
)
layout_fields.append(Field('note', css_class='is-valid'))
for meal in self.object.meal_set.filter(available=True).all():
form.fields[f'meal_{meal.id}_quantity'] = forms.IntegerField(
label=_("Quantity"),
initial=0,
)
form.fields[f'meal_{meal.id}_gift'] = forms.IntegerField(
label=_("gift").capitalize(),
initial=0,
widget=AmountInput(),
)
form.fields[f'meal_{meal.id}_remark'] = forms.CharField(
max_length=255,
required=False,
label=_("remark").capitalize(),
help_text=_("Allergies,…"),
)
form.fields[f'meal_{meal.id}_priority'] = forms.CharField(
max_length=64,
required=False,
label=_("priority request").capitalize(),
help_text=_("Lesson at 13h30,…"),
)
ag = AccordionGroup(f"{meal} ({pretty_money(meal.price)})",
Row(Field(f'meal_{meal.id}_quantity', wrapper_class='col-sm-9'),
Field(f'meal_{meal.id}_gift', wrapper_class='col-sm-3')),
Row(Field(f'meal_{meal.id}_remark', wrapper_class='col-sm-9'),
Field(f'meal_{meal.id}_priority', wrapper_class='col-sm-3')))
for food in meal.content.filter(available=True).all():
if food.foodoption_set.count():
options_fieldset = Fieldset(_("Options for ") + str(food))
options_row = Row(css_class='ml-0')
for option in food.foodoption_set.filter(available=True).all():
form.fields[f'meal_{meal.id}_food_{food.id}_option_{option.id}'] = forms.BooleanField(
label=f"{option}{f' ({pretty_money(option.extra_cost)})' if option.extra_cost else ''}",
required=False,
)
options_row.fields.append(
Field(f'meal_{meal.id}_food_{food.id}_option_{option.id}', wrapper_class='col-sm-12'))
options_fieldset.fields.append(options_row)
ag.fields.append(options_fieldset)
layout_fields.append(ag)
for food in self.object.food_set.filter(available=True).all():
form.fields[f'food_{food.id}_quantity'] = forms.IntegerField(
label=_("quantity").capitalize(),
initial=0,
)
form.fields[f'food_{food.id}_gift'] = forms.IntegerField(
label=_("gift").capitalize(),
initial=0,
widget=AmountInput(),
)
form.fields[f'food_{food.id}_remark'] = forms.CharField(
max_length=255,
required=False,
label=_("remark").capitalize(),
help_text=_("Allergies,…"),
)
form.fields[f'food_{food.id}_priority'] = forms.CharField(
max_length=255,
required=False,
label=_("priority request").capitalize(),
help_text=_("Lesson at 13h30,…"),
)
ag = AccordionGroup(f"{food} ({pretty_money(food.price)})",
Row(Field(f'food_{food.id}_quantity', wrapper_class='col-sm-9'),
Field(f'food_{food.id}_gift', wrapper_class='col-sm-3')),
Row(Field(f'food_{food.id}_remark', wrapper_class='col-sm-9'),
Field(f'food_{food.id}_priority', wrapper_class='col-sm-3')))
if food.foodoption_set.count():
options_fieldset = Fieldset(_("Options"))
options_row = Row(css_class='ml-0')
for option in food.foodoption_set.all():
form.fields[f'food_{food.id}_option_{option.id}'] = forms.BooleanField(
label=f"{option}{f' ({pretty_money(option.extra_cost)})' if option.extra_cost else ''}",
required=False,
)
options_row.fields.append(Field(f'food_{food.id}_option_{option.id}', wrapper_class='col-sm-12'))
options_fieldset.fields.append(options_row)
ag.fields.append(options_fieldset)
layout_fields.append(ag)
layout_fields.append(FormActions(Submit('submit', _("Order now"))))
form.helper.layout = Accordion(*layout_fields)
if self.request.method in ['PUT', 'POST']:
form.data = self.request.POST
form.files = self.request.FILES
form.is_bound = not form.data or not form.files
return form
def form_valid(self, form):
data = form.cleaned_data
sheet = self.get_object()
with transaction.atomic():
order = Order.objects.create(sheet_id=self.kwargs['pk'], note=data['note'])
total_quantity = 0
for meal in sheet.meal_set.filter(available=True).all():
quantity = data[f'meal_{meal.id}_quantity']
if not quantity:
continue
total_quantity += quantity
gift = data[f'meal_{meal.id}_gift']
remark = data[f'meal_{meal.id}_remark'] or ''
priority = data[f'meal_{meal.id}_priority'] or ''
ordered_meal = OrderedMeal.objects.create(order=order, meal=meal, gift=gift)
for ignored in range(quantity):
for food in meal.content.filter(available=True).all():
n = OrderedFood.objects.filter(order__sheet_id=self.kwargs['pk'],
order__note=order.note,
order__date__gte=timezone.now() - timedelta(hours=6),
food=food).exclude(status='CANCELED').count()
of = OrderedFood.objects.create(order=order, meal=ordered_meal, food=food,
remark=remark, priority=priority, number=n + 1, gift=0)
for option in food.foodoption_set.filter(available=True).all():
if data[f'meal_{meal.id}_food_{food.id}_option_{option.id}']:
of.options.add(option)
of.save()
first_food = ordered_meal.orderedfood_set.first()
tr = SheetOrderTransaction(source_id=order.note_id, destination=first_food.food.club.note,
source_alias=str(order.note), destination_alias=first_food.food.club.name,
quantity=quantity, ordered_food=first_food,
reason=f"{meal.name} - {sheet.name}")
tr.amount = tr.get_price / tr.quantity
tr.save()
for food in sheet.food_set.filter(available=True).all():
quantity = data[f'food_{food.id}_quantity']
if not quantity:
continue
total_quantity += quantity
gift = data[f'food_{meal.id}_gift']
remark = data[f'food_{meal.id}_remark'] or ''
priority = data[f'food_{meal.id}_priority'] or ''
for ignored in range(quantity):
n = OrderedFood.objects.filter(order__sheet_id=self.kwargs['pk'],
order__note=order.note,
order__date__gte=timezone.now() - timedelta(hours=6),
food=food).exclude(state='CANCELED').count()
of = OrderedFood.objects.create(order=order, food=food, gift=gift,
remark=remark, priority=priority, number=n + 1)
for option in food.foodoption_set.filter(available=True).all():
if data[f'meal_{meal.id}_food_{food.id}_option_{option.id}']:
of.options.add(option)
of.options.save()
tr = SheetOrderTransaction(source_id=order.note_id, destination_id=first_food.club.note,
source_alias=str(order.note), destination_alias=first_food.club.name,
quantity=quantity, ordered_food=of,
reason=f"{food.name} - {sheet.name}")
tr.amount = tr.get_price / tr.quantity
tr.save()
if total_quantity == 0:
form.add_error(None, _("You didn't select anything."))
transaction.rollback()
return self.form_invalid(form)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('sheets:sheet_detail', args=(self.kwargs['pk'],))

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-08-18 14:49+0200\n"
"POT-Creation-Date: 2022-08-18 17:20+0200\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: elkmaennchen <elkmaennchen@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@ -1476,7 +1476,7 @@ msgstr "modèles de transaction"
msgid "used alias"
msgstr "alias utilisé"
#: apps/note/models/transactions.py:136
#: apps/note/models/transactions.py:136 apps/sheets/views.py:276
msgid "quantity"
msgstr "quantité"
@ -2188,7 +2188,7 @@ msgid "the note sheet will be private until this field is checked."
msgstr "la feuille de note restera privée tant que ce champ n'est pas coché."
#: apps/sheets/models.py:41 apps/sheets/models.py:54 apps/sheets/models.py:116
#: apps/sheets/models.py:151 apps/sheets/models.py:268
#: apps/sheets/models.py:151 apps/sheets/models.py:273
msgid "note sheet"
msgstr "feuille de note"
@ -2231,7 +2231,8 @@ msgstr "options de nourriture"
msgid "content"
msgstr "contenu"
#: apps/sheets/models.py:143 apps/sheets/models.py:184
#: apps/sheets/models.py:140 apps/sheets/models.py:143
#: apps/sheets/models.py:180
msgid "meal"
msgstr "menu"
@ -2243,19 +2244,20 @@ msgstr "menus"
msgid "date"
msgstr "date"
#: apps/sheets/models.py:166
msgid "gift"
msgstr "don"
#: apps/sheets/models.py:170 apps/sheets/models.py:178
#: apps/sheets/models.py:166 apps/sheets/models.py:174
#: apps/sheets/models.py:196
msgid "order"
msgstr "commande"
#: apps/sheets/models.py:171
#: apps/sheets/models.py:167
msgid "orders"
msgstr "commandes"
#: apps/sheets/models.py:184 apps/sheets/models.py:233 apps/sheets/views.py:235
#: apps/sheets/views.py:280
msgid "gift"
msgstr "don"
#: apps/sheets/models.py:188 apps/sheets/models.py:204
msgid "ordered meal"
msgstr "menu commandé"
@ -2268,56 +2270,56 @@ msgstr "menus commandés"
msgid "options"
msgstr "options"
#: apps/sheets/models.py:222
#: apps/sheets/models.py:222 apps/sheets/views.py:242 apps/sheets/views.py:287
msgid "remark"
msgstr "remarques"
#: apps/sheets/models.py:229
#: apps/sheets/models.py:229 apps/sheets/views.py:248 apps/sheets/views.py:293
msgid "priority request"
msgstr "demande de priorité"
#: apps/sheets/models.py:233
#: apps/sheets/models.py:237
msgid "number"
msgstr "numéro"
#: apps/sheets/models.py:234
#: apps/sheets/models.py:238
msgid "How many times the user ordered this."
msgstr "Combien de fois cet⋅te utilisateur⋅rice a commandé ceci."
#: apps/sheets/models.py:240
#: apps/sheets/models.py:244
msgid "queued"
msgstr "en attente"
#: apps/sheets/models.py:241
#: apps/sheets/models.py:245
msgid "ready"
msgstr "prêt"
#: apps/sheets/models.py:242
#: apps/sheets/models.py:246
msgid "served"
msgstr "servi"
#: apps/sheets/models.py:243
#: apps/sheets/models.py:247
msgid "canceled"
msgstr "annulé"
#: apps/sheets/models.py:245
#: apps/sheets/models.py:250
msgid "status"
msgstr "statut"
#: apps/sheets/models.py:251
#: apps/sheets/models.py:256
msgid "served date"
msgstr "date de service"
#: apps/sheets/models.py:255 apps/sheets/models.py:256
#: apps/sheets/models.py:263
#: apps/sheets/models.py:260 apps/sheets/models.py:261
#: apps/sheets/models.py:268
msgid "ordered food"
msgstr "nourriture commandée"
#: apps/sheets/models.py:281
#: apps/sheets/models.py:288
msgid "sheet order transaction"
msgstr "transaction de commande sur feuille de note"
#: apps/sheets/models.py:282
#: apps/sheets/models.py:289
msgid "sheet order transactions"
msgstr "transactions de commande sur feuille de note"
@ -2347,7 +2349,8 @@ msgstr "Ajouter un plat"
msgid "Add new meal"
msgstr "Ajouter un menu"
#: apps/sheets/templates/sheets/sheet_detail.html:79
#: apps/sheets/templates/sheets/sheet_detail.html:79 apps/sheets/views.py:206
#: apps/sheets/views.py:317
msgid "Order now"
msgstr "Commander maintenant"
@ -2359,34 +2362,70 @@ msgstr "Créer une feuille de note"
msgid "Note sheet listing"
msgstr "Liste des feuilles de notes"
#: apps/sheets/views.py:23
#: apps/sheets/views.py:33
msgid "Search note sheet"
msgstr "Chercher une feuille de note"
#: apps/sheets/views.py:38
#: apps/sheets/views.py:48
msgid "Create note sheet"
msgstr "Créer une feuille de note"
#: apps/sheets/views.py:51
#: apps/sheets/views.py:61
msgid "Update note sheet"
msgstr "Modifier une feuille de note"
#: apps/sheets/views.py:74
#: apps/sheets/views.py:84
msgid "Create new food"
msgstr "Créer un plat"
#: apps/sheets/views.py:120
#: apps/sheets/views.py:130
msgid "Update food"
msgstr "Modifier un plat"
#: apps/sheets/views.py:157
#: apps/sheets/views.py:167
msgid "Create new meal"
msgstr "Créer un menu"
#: apps/sheets/views.py:182
#: apps/sheets/views.py:192
msgid "Update meal"
msgstr "Modifier un menu"
#: apps/sheets/views.py:217
#, fuzzy
#| msgid "order"
msgid "Orderer"
msgstr "commande"
#: apps/sheets/views.py:223
#, fuzzy
#| msgid "orders"
msgid "Who orders"
msgstr "commandes"
#: apps/sheets/views.py:231 apps/treasury/models.py:140
msgid "Quantity"
msgstr "Quantité"
#: apps/sheets/views.py:243 apps/sheets/views.py:288
msgid "Allergies,…"
msgstr "Allergies,…"
#: apps/sheets/views.py:249 apps/sheets/views.py:294
msgid "Lesson at 13h30,…"
msgstr "Cours à 13h30,…"
#: apps/sheets/views.py:260
msgid "Options for "
msgstr "Options pour "
#: apps/sheets/views.py:304
msgid "Options"
msgstr "Options"
#: apps/sheets/views.py:401
msgid "You didn't select anything."
msgstr "Vous n'avez rien sélectionné."
#: apps/treasury/apps.py:12 note_kfet/templates/base.html:96
msgid "Treasury"
msgstr "Trésorerie"
@ -2473,10 +2512,6 @@ msgstr "Facture n°{id}"
msgid "Designation"
msgstr "Désignation"
#: apps/treasury/models.py:140
msgid "Quantity"
msgstr "Quantité"
#: apps/treasury/models.py:145
msgid "Unit price"
msgstr "Prix unitaire"