# 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, 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, Order, OrderedMeal, OrderedFood, SheetOrderTransaction from .tables import SheetTable class SheetListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): model = Sheet table_class = SheetTable ordering = '-date' extra_context = {"title": _("Search note sheet")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["can_create_sheet"] = PermissionBackend.check_perm(self.request, "sheets.add_sheet", Sheet( name="Test", date=timezone.now(), description="Test sheet", )) return context class SheetCreateView(ProtectQuerysetMixin, ProtectedCreateView): model = Sheet form_class = SheetForm extra_context = {"title": _("Create note sheet")} def get_sample_object(self): return Sheet( name="Test", date=timezone.now(), description="Test", ) class SheetUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = Sheet form_class = SheetForm extra_context = {"title": _("Update note sheet")} class SheetDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = Sheet def get_context_data(self, **kwargs): context = super().get_context_data() context['can_change_sheet'] = PermissionBackend.check_perm(self.request, 'sheets.change_sheet', self.object) context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'sheets.add_meal', Meal(sheet=self.object, name="Test", price=500)) context['can_add_food'] = PermissionBackend.check_perm(self.request, 'sheets.add_food', Food(sheet=self.object, name="Test", price=500)) return context class FoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): model = Food form_class = FoodForm extra_context = {"title": _("Create new food")} def get_sample_object(self): return Food( sheet_id=self.kwargs['pk'], name="Test", price=500, ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) form = context['form'] form.helper = FormHelper() # Remove form tag on the generation of the form in the template (already present on the template) form.helper.form_tag = False # The formset handles the set of the products form_set = FoodOptionsFormSet(instance=form.instance) context['formset'] = form_set context['helper'] = FoodOptionFormSetHelper() return context def form_valid(self, form): form.instance.sheet_id = self.kwargs['pk'] # For each product, we save it formset = FoodOptionsFormSet(self.request.POST, instance=form.instance) if formset.is_valid(): for f in formset: # We don't save the product if the designation is not entered, ie. if the line is empty if f.is_valid() and f.instance.name: f.save() f.instance.save() else: f.instance = None return super().form_valid(form) def get_success_url(self): return reverse_lazy('sheets:sheet_detail', args=(self.kwargs['pk'],)) class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = Food form_class = FoodForm extra_context = {"title": _("Update food")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) form = context['form'] form.helper = FormHelper() # Remove form tag on the generation of the form in the template (already present on the template) form.helper.form_tag = False # The formset handles the set of the products form_set = FoodOptionsFormSet(instance=form.instance) context['formset'] = form_set context['helper'] = FoodOptionFormSetHelper() return context def form_valid(self, form): # For each product, we save it formset = FoodOptionsFormSet(self.request.POST, instance=form.instance) if formset.is_valid(): for f in formset: # We don't save the product if the designation is not entered, ie. if the line is empty if f.is_valid() and f.instance.name: f.save() f.instance.save() else: f.instance = None return super().form_valid(form) def get_success_url(self): return reverse_lazy('sheets:sheet_detail', args=(self.object.sheet_id,)) class MealCreateView(ProtectQuerysetMixin, ProtectedCreateView): model = Meal form_class = MealForm extra_context = {"title": _("Create new meal")} def get_sample_object(self): return Meal( sheet_id=self.kwargs['pk'], name="Test", price=500, ) def get_form(self, form_class=None): form = super().get_form(form_class) form.fields['content'].queryset = form.fields['content'].queryset.filter(sheet_id=self.kwargs['pk']) return form def form_valid(self, form): form.instance.sheet_id = self.kwargs['pk'] return super().form_valid(form) def get_success_url(self): return reverse_lazy('sheets:sheet_detail', args=(self.object.sheet_id,)) class MealUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = Meal form_class = MealForm extra_context = {"title": _("Update meal")} def get_form(self, form_class=None): form = super().get_form(form_class) form.fields['content'].queryset = form.fields['content'].queryset.filter(sheet=self.object.sheet) return form 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(), help_text=_("Be careful: this gift will be multiplied for each order."), ) 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(), help_text=_("Be careful: this gift will be multiplied for each order."), ) 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'],)) class WaitingListView(ProtectQuerysetMixin, DetailView): model = Food template_name = 'sheets/waiting_list.html' extra_context = {'title': _("Waiting list")} def get_context_data(self, **kwargs): content = super().get_context_data(**kwargs) content['queue'] = OrderedFood.objects.filter(food_id=self.kwargs['pk'], status='QUEUED')\ .order_by('-priority', 'number', 'order__date').all() content['ready'] = OrderedFood.objects.filter(food_id=self.kwargs['pk'], status='READY')\ .order_by('served_date').all() return content class WaitingListDetailView(ProtectQuerysetMixin, DetailView): model = Food template_name = 'sheets/waiting_list_detail.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) list_type = 'CANCELED' if 'canceled' in self.request.path else \ 'SERVED' if 'served' in self.request.path else \ 'READY' if 'ready' in self.request.path else 'QUEUED' context['list_type'] = list_type context['orders'] = OrderedFood.objects.filter(food_id=self.kwargs['pk'], status=list_type)\ .order_by('served_date', '-priority', 'number', 'order__date').all() context['title'] = self.object.name + " - " + _(list_type.lower()).capitalize() return context