1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-23 19:06:37 +02:00

Add order interface

Signed-off-by: Emmy D'ANELLO <ynerant@crans.org>
This commit is contained in:
2022-08-18 17:27:59 +02:00
parent 5174c84b33
commit 45334e4e02
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())
for ordered_food in self.ordered_food.meal.orderedfood_set.exclude(status='CANCELED').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'],))