mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			app_downlo
			...
			note_sheet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 6bf21b103f | 
| @@ -48,5 +48,15 @@ | |||||||
|             "can_invite": true, |             "can_invite": true, | ||||||
|             "guest_entry_fee": 0 |             "guest_entry_fee": 0 | ||||||
|         } |         } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         "model": "activity.activitytype", | ||||||
|  |         "pk": 8, | ||||||
|  |         "fields": { | ||||||
|  |             "name": "Perm bouffe", | ||||||
|  |             "manage_entries": false, | ||||||
|  |             "can_invite": false, | ||||||
|  |             "guest_entry_fee": 0 | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| ] | ] | ||||||
| @@ -66,6 +66,10 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|             <a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a> |             <a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|  |  | ||||||
|  |         {% if activity.activity_type.name == "Perm bouffe" %} | ||||||
|  |             <a class="btn btn-warning btn-sm my-1" href="{% url 'food:dish_list' activity_pk=activity.pk %}"> {% trans "Dish page" %}</a> | ||||||
|  |         {% endif %} | ||||||
|  |  | ||||||
|         {% if request.path_info == activity_detail_url %} |         {% if request.path_info == activity_detail_url %} | ||||||
|             {% if activity.valid and ".change__open"|has_perm:activity %} |             {% if activity.valid and ".change__open"|has_perm:activity %} | ||||||
|                 <a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a> |                 <a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a> | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|  |  | ||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
|  |  | ||||||
| from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode | from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction | ||||||
|  |  | ||||||
|  |  | ||||||
| class AllergenSerializer(serializers.ModelSerializer): | class AllergenSerializer(serializers.ModelSerializer): | ||||||
| @@ -54,3 +54,43 @@ class QRCodeSerializer(serializers.ModelSerializer): | |||||||
|     class Meta: |     class Meta: | ||||||
|         model = QRCode |         model = QRCode | ||||||
|         fields = '__all__' |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishSerializer(serializers.ModelSerializer): | ||||||
|  |     """ | ||||||
|  |     REST API Serializer for Dish. | ||||||
|  |     The djangorestframework plugin will analyse the model `Dish` and parse all fields in the API. | ||||||
|  |     """ | ||||||
|  |     class Meta: | ||||||
|  |         model = Dish | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SupplementSerializer(serializers.ModelSerializer): | ||||||
|  |     """ | ||||||
|  |     REST API Serializer for Supplement. | ||||||
|  |     The djangorestframework plugin will analyse the model `Supplement` and parse all fields in the API. | ||||||
|  |     """ | ||||||
|  |     class Meta: | ||||||
|  |         model = Supplement | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderSerializer(serializers.ModelSerializer): | ||||||
|  |     """ | ||||||
|  |     REST API Serializer for Order. | ||||||
|  |     The djangorestframework plugin will analyse the model `Order` and parse all fields in the API. | ||||||
|  |     """ | ||||||
|  |     class Meta: | ||||||
|  |         model = Order | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodTransactionSerializer(serializers.ModelSerializer): | ||||||
|  |     """ | ||||||
|  |     REST API Serializer for FoodTransaction. | ||||||
|  |     The djangorestframework plugin will analyse the model `FoodTransaction` and parse all fields in the API. | ||||||
|  |     """ | ||||||
|  |     class Meta: | ||||||
|  |         model = FoodTransaction | ||||||
|  |         fields = '__all__' | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
| # SPDX-License-Identifier: GPL-3.0-or-later | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from .views import AllergenViewSet, FoodViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet | from .views import AllergenViewSet, FoodViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet, \ | ||||||
|  |     DishViewSet, SupplementViewSet, OrderViewSet, FoodTransactionViewSet | ||||||
|  |  | ||||||
|  |  | ||||||
| def register_food_urls(router, path): | def register_food_urls(router, path): | ||||||
| @@ -13,3 +14,7 @@ def register_food_urls(router, path): | |||||||
|     router.register(path + '/basicfood', BasicFoodViewSet) |     router.register(path + '/basicfood', BasicFoodViewSet) | ||||||
|     router.register(path + '/transformedfood', TransformedFoodViewSet) |     router.register(path + '/transformedfood', TransformedFoodViewSet) | ||||||
|     router.register(path + '/qrcode', QRCodeViewSet) |     router.register(path + '/qrcode', QRCodeViewSet) | ||||||
|  |     router.register(path + '/dish', DishViewSet) | ||||||
|  |     router.register(path + '/supplement', SupplementViewSet) | ||||||
|  |     router.register(path + '/order', OrderViewSet) | ||||||
|  |     router.register(path + '/foodtransaction', FoodTransactionViewSet) | ||||||
|   | |||||||
| @@ -3,10 +3,12 @@ | |||||||
|  |  | ||||||
| from api.viewsets import ReadProtectedModelViewSet | from api.viewsets import ReadProtectedModelViewSet | ||||||
| from django_filters.rest_framework import DjangoFilterBackend | from django_filters.rest_framework import DjangoFilterBackend | ||||||
|  | from django.utils import timezone | ||||||
| from rest_framework.filters import SearchFilter | from rest_framework.filters import SearchFilter | ||||||
|  |  | ||||||
| from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer | from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer, \ | ||||||
| from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode |     DishSerializer, SupplementSerializer, OrderSerializer, FoodTransactionSerializer | ||||||
|  | from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction | ||||||
|  |  | ||||||
|  |  | ||||||
| class AllergenViewSet(ReadProtectedModelViewSet): | class AllergenViewSet(ReadProtectedModelViewSet): | ||||||
| @@ -72,3 +74,61 @@ class QRCodeViewSet(ReadProtectedModelViewSet): | |||||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|     filterset_fields = ['qr_code_number', ] |     filterset_fields = ['qr_code_number', ] | ||||||
|     search_fields = ['$qr_code_number', ] |     search_fields = ['$qr_code_number', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishViewSet(ReadProtectedModelViewSet): | ||||||
|  |     """ | ||||||
|  |     REST API View set. | ||||||
|  |     The djangorestframework plugin will get all `Dish` objects, serialize it to JSON with the given serializer, | ||||||
|  |     then render it on /api/food/dish/ | ||||||
|  |     """ | ||||||
|  |     queryset = Dish.objects.order_by('id') | ||||||
|  |     serializer_class = DishSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['main__name', 'activity', ] | ||||||
|  |     search_fields = ['$main__name', '$activity', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SupplementViewSet(ReadProtectedModelViewSet): | ||||||
|  |     """ | ||||||
|  |     REST API View set. | ||||||
|  |     The djangorestframework plugin will get all `Supplement` objects, serialize it to JSON with the given serializer, | ||||||
|  |     then render it on /api/food/supplement/ | ||||||
|  |     """ | ||||||
|  |     queryset = Supplement.objects.order_by('id') | ||||||
|  |     serializer_class = SupplementSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['food__name', 'dish__activity', ] | ||||||
|  |     search_fields = ['$food__name', '$dish__activity', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderViewSet(ReadProtectedModelViewSet): | ||||||
|  |     """ | ||||||
|  |     REST API View set. | ||||||
|  |     The djangorestframework plugin will get all `Order` objects, serialize it to JSON with the given serializer, | ||||||
|  |     then render it on /api/food/order/ | ||||||
|  |     """ | ||||||
|  |     queryset = Order.objects.order_by('id') | ||||||
|  |     serializer_class = OrderSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['user', 'activity', 'dish', 'supplements', 'number', ] | ||||||
|  |     search_fields = ['$user', '$activity', '$dish', '$supplements', '$number', ] | ||||||
|  |  | ||||||
|  |     def perform_update(self, serializer): | ||||||
|  |         instance = serializer.save() | ||||||
|  |         if instance.served and not instance.served_at: | ||||||
|  |             instance.served_at = timezone.now() | ||||||
|  |             instance.save() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodTransactionViewSet(ReadProtectedModelViewSet): | ||||||
|  |     """ | ||||||
|  |     REST API View set. | ||||||
|  |     The djangorestframework plugin will get all `FoodTransaction` objects, serialize it to JSON with the given serializer, | ||||||
|  |     then render it on /api/food/foodtransaction/ | ||||||
|  |     """ | ||||||
|  |     queryset = FoodTransaction.objects.order_by('id') | ||||||
|  |     serializer_class = FoodTransactionSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['order', ] | ||||||
|  |     search_fields = ['$order', ] | ||||||
|   | |||||||
| @@ -4,15 +4,16 @@ | |||||||
| from random import shuffle | from random import shuffle | ||||||
|  |  | ||||||
| from bootstrap_datepicker_plus.widgets import DateTimePickerInput | from bootstrap_datepicker_plus.widgets import DateTimePickerInput | ||||||
|  | from crispy_forms.helper import FormHelper | ||||||
| from django import forms | from django import forms | ||||||
| from django.forms.widgets import NumberInput | from django.forms.widgets import NumberInput | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from member.models import Club | from member.models import Club | ||||||
| from note_kfet.inputs import Autocomplete | from note_kfet.inputs import Autocomplete, AmountInput | ||||||
| from note_kfet.middlewares import get_current_request | from note_kfet.middlewares import get_current_request | ||||||
| from permission.backends import PermissionBackend | from permission.backends import PermissionBackend | ||||||
|  |  | ||||||
| from .models import Food, BasicFood, TransformedFood, QRCode | from .models import Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order | ||||||
|  |  | ||||||
|  |  | ||||||
| class QRCodeForms(forms.ModelForm): | class QRCodeForms(forms.ModelForm): | ||||||
| @@ -185,3 +186,60 @@ ManageIngredientsFormSet = forms.formset_factory( | |||||||
|     ManageIngredientsForm, |     ManageIngredientsForm, | ||||||
|     extra=1, |     extra=1, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishForm(forms.ModelForm): | ||||||
|  |     """ | ||||||
|  |     Form to create a dish | ||||||
|  |     """ | ||||||
|  |     class Meta: | ||||||
|  |         model = Dish | ||||||
|  |         fields = ('main', 'price', 'available') | ||||||
|  |         widgets = { | ||||||
|  |             "price": AmountInput(), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SupplementForm(forms.ModelForm): | ||||||
|  |     """ | ||||||
|  |     Form to create a dish | ||||||
|  |     """ | ||||||
|  |     class Meta: | ||||||
|  |         model = Supplement | ||||||
|  |         fields = '__all__' | ||||||
|  |         widgets = { | ||||||
|  |             "price": AmountInput(), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # The 2 following classes are copied from treasury app | ||||||
|  | # Add a subform per supplement in the dish form, and manage correctly the link between the dish and | ||||||
|  | # its supplements. The FormSet will search automatically the ForeignKey in the Supplement model. | ||||||
|  | SupplementFormSet = forms.inlineformset_factory( | ||||||
|  |     Dish, | ||||||
|  |     Supplement, | ||||||
|  |     form=SupplementForm, | ||||||
|  |     extra=0, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SupplementFormSetHelper(FormHelper): | ||||||
|  |     """ | ||||||
|  |     Specify some template information for the supplement form | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, form=None): | ||||||
|  |         super().__init__(form) | ||||||
|  |         self.form_tag = False | ||||||
|  |         self.form_method = 'POST' | ||||||
|  |         self.form_class = 'form-inline' | ||||||
|  |         self.template = 'bootstrap4/table_inline_formset.html' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderForm(forms.ModelForm): | ||||||
|  |     """ | ||||||
|  |     Form to order food | ||||||
|  |     """ | ||||||
|  |     class Meta: | ||||||
|  |         model = Order | ||||||
|  |         exclude = ("activity", "number", "ordered_at", "served", "served_at") | ||||||
|   | |||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | # Generated by Django 5.2.4 on 2025-08-30 00:16 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('food', '0001_initial'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='food', | ||||||
|  |             name='end_of_life', | ||||||
|  |             field=models.CharField(blank=True, max_length=255, verbose_name='end of life'), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='food', | ||||||
|  |             name='order', | ||||||
|  |             field=models.CharField(blank=True, max_length=255, verbose_name='order'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -0,0 +1,86 @@ | |||||||
|  | # Generated by Django 5.2.6 on 2025-10-30 22:46 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | import django.utils.timezone | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('activity', '0007_alter_guest_activity'), | ||||||
|  |         ('food', '0002_alter_food_end_of_life_alter_food_order'), | ||||||
|  |         ('note', '0007_alter_note_polymorphic_ctype_and_more'), | ||||||
|  |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Dish', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('price', models.PositiveIntegerField(verbose_name='price')), | ||||||
|  |                 ('available', models.BooleanField(default=True, verbose_name='available')), | ||||||
|  |                 ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dishes', to='activity.activity', verbose_name='activity')), | ||||||
|  |                 ('main', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='dishes_as_main', to='food.transformedfood', verbose_name='main food')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'Dish', | ||||||
|  |                 'verbose_name_plural': 'Dishes', | ||||||
|  |                 'unique_together': {('main', 'activity')}, | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Order', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('request', models.TextField(blank=True, help_text='A specific request (to remove an ingredient for example)', verbose_name='request')), | ||||||
|  |                 ('number', models.PositiveIntegerField(default=1, verbose_name='number')), | ||||||
|  |                 ('ordered_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='order date')), | ||||||
|  |                 ('served', models.BooleanField(default=False, verbose_name='served')), | ||||||
|  |                 ('served_at', models.DateTimeField(blank=True, null=True, verbose_name='served date')), | ||||||
|  |                 ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='food_orders', to='activity.activity', verbose_name='activity')), | ||||||
|  |                 ('dish', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='food.dish', verbose_name='dish')), | ||||||
|  |                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='food_orders', to=settings.AUTH_USER_MODEL, verbose_name='user')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'Order', | ||||||
|  |                 'verbose_name_plural': 'Orders', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='FoodTransaction', | ||||||
|  |             fields=[ | ||||||
|  |                 ('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.transaction')), | ||||||
|  |                 ('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transaction', to='food.order', verbose_name='order')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'food transaction', | ||||||
|  |                 'verbose_name_plural': 'food transactions', | ||||||
|  |             }, | ||||||
|  |             bases=('note.transaction',), | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Supplement', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('price', models.PositiveIntegerField(verbose_name='price')), | ||||||
|  |                 ('dish', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supplements', to='food.dish', verbose_name='dish')), | ||||||
|  |                 ('food', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='supplements', to='food.food', verbose_name='food')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'Supplement', | ||||||
|  |                 'verbose_name_plural': 'Supplements', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='order', | ||||||
|  |             name='supplements', | ||||||
|  |             field=models.ManyToManyField(blank=True, related_name='orders', to='food.supplement', verbose_name='supplements'), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterUniqueTogether( | ||||||
|  |             name='order', | ||||||
|  |             unique_together={('activity', 'number')}, | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -4,10 +4,14 @@ | |||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
|  |  | ||||||
| from django.db import models, transaction | from django.db import models, transaction | ||||||
|  | from django.core.exceptions import ValidationError | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
|  | from django.contrib.auth.models import User | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from polymorphic.models import PolymorphicModel | from polymorphic.models import PolymorphicModel | ||||||
| from member.models import Club | from member.models import Club | ||||||
|  | from activity.models import Activity | ||||||
|  | from note.models import Transaction | ||||||
|  |  | ||||||
|  |  | ||||||
| class Allergen(models.Model): | class Allergen(models.Model): | ||||||
| @@ -284,3 +288,199 @@ class QRCode(models.Model): | |||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return _('QR-code number') + ' ' + str(self.qr_code_number) |         return _('QR-code number') + ' ' + str(self.qr_code_number) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Dish(models.Model): | ||||||
|  |     """ | ||||||
|  |     A dish is a food proposed during a meal | ||||||
|  |     """ | ||||||
|  |     main = models.ForeignKey( | ||||||
|  |         TransformedFood, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         related_name='dishes_as_main', | ||||||
|  |         verbose_name=_('main food'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     price = models.PositiveIntegerField( | ||||||
|  |         verbose_name=_('price') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     activity = models.ForeignKey( | ||||||
|  |         Activity, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         related_name='dishes', | ||||||
|  |         verbose_name=_('activity'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     available = models.BooleanField( | ||||||
|  |         default=True, | ||||||
|  |         verbose_name=_('available'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _('Dish') | ||||||
|  |         verbose_name_plural = _('Dishes') | ||||||
|  |         unique_together = ('main', 'activity') | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.main.name + ' (' + str(self.activity) + ')' | ||||||
|  |  | ||||||
|  |     def save(self, *args, **kwargs): | ||||||
|  |         "Check the type of activity" | ||||||
|  |         if self.activity.activity_type.name != 'Perm bouffe': | ||||||
|  |             raise ValidationError(_('(You cannot select this type of activity.')) | ||||||
|  |  | ||||||
|  |         return super().save(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Supplement(models.Model): | ||||||
|  |     """ | ||||||
|  |     A supplement is a food added to a dish | ||||||
|  |     """ | ||||||
|  |     dish = models.ForeignKey( | ||||||
|  |         Dish, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         related_name='supplements', | ||||||
|  |         verbose_name=_('dish'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     food = models.ForeignKey( | ||||||
|  |         Food, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         related_name='supplements', | ||||||
|  |         verbose_name=_('food'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     price = models.PositiveIntegerField( | ||||||
|  |         verbose_name=_('price') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _('Supplement') | ||||||
|  |         verbose_name_plural = _('Supplements') | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return _("Supplement {food} for {dish}").format( | ||||||
|  |             food=str(self.food), dish=str(self.dish)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Order(models.Model): | ||||||
|  |     """ | ||||||
|  |     An order is a dish ordered by a member during an activity | ||||||
|  |     """ | ||||||
|  |     user = models.ForeignKey( | ||||||
|  |         User, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         related_name='food_orders', | ||||||
|  |         verbose_name=_('user'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     activity = models.ForeignKey( | ||||||
|  |         Activity, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         related_name='food_orders', | ||||||
|  |         verbose_name=_('activity'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     dish = models.ForeignKey( | ||||||
|  |         Dish, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         related_name='orders', | ||||||
|  |         verbose_name=_('dish'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     supplements = models.ManyToManyField( | ||||||
|  |         Supplement, | ||||||
|  |         related_name='orders', | ||||||
|  |         verbose_name=_('supplements'), | ||||||
|  |         blank=True, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     request = models.TextField( | ||||||
|  |         blank=True, | ||||||
|  |         verbose_name=_('request'), | ||||||
|  |         help_text=_('A specific request (to remove an ingredient for example)') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     number = models.PositiveIntegerField( | ||||||
|  |         verbose_name=_('number'), | ||||||
|  |         default=1, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     ordered_at = models.DateTimeField( | ||||||
|  |         default=timezone.now, | ||||||
|  |         verbose_name=_('order date'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     served = models.BooleanField( | ||||||
|  |         default=False, | ||||||
|  |         verbose_name=_('served'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     served_at = models.DateTimeField( | ||||||
|  |         null=True, | ||||||
|  |         blank=True, | ||||||
|  |         verbose_name=_('served date'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _('Order') | ||||||
|  |         verbose_name_plural = _('Orders') | ||||||
|  |         unique_together = ('activity', 'number', ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def amount(self): | ||||||
|  |         return self.dish.price + sum(s.price for s in self.supplements.all()) | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return _("Order of {dish} by {user}").format( | ||||||
|  |             dish=str(self.dish), | ||||||
|  |             user=str(self.user)) | ||||||
|  |  | ||||||
|  |     def save(self, *args, **kwargs): | ||||||
|  |         created = self.pk is None | ||||||
|  |         if created: | ||||||
|  |             last_order = Order.objects.filter(activity=self.activity).last() | ||||||
|  |             if last_order is None: | ||||||
|  |                 self.number = 1 | ||||||
|  |             else: | ||||||
|  |                 self.number = last_order.number + 1 | ||||||
|  |  | ||||||
|  |         elif self.served: | ||||||
|  |             if FoodTransaction.objects.filter(order=self).exists(): | ||||||
|  |                 transaction = FoodTransaction.objects.get(order=self) | ||||||
|  |                 transaction.valid = True | ||||||
|  |                 transaction.save() | ||||||
|  |             else: | ||||||
|  |                 transaction = FoodTransaction( | ||||||
|  |                     source=self.user.note, | ||||||
|  |                     destination=self.activity.organizer.note, | ||||||
|  |                     amount=self.amount, | ||||||
|  |                     quantity=1, | ||||||
|  |                     valid=True, | ||||||
|  |                     order=self, | ||||||
|  |                 ) | ||||||
|  |                 transaction.save() | ||||||
|  |         else: | ||||||
|  |             if FoodTransaction.objects.filter(order=self).exists(): | ||||||
|  |                 transaction = FoodTransaction.objects.get(order=self) | ||||||
|  |                 transaction.valid = False | ||||||
|  |                 transaction.save() | ||||||
|  |  | ||||||
|  |         return super().save(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodTransaction(Transaction): | ||||||
|  |     """ | ||||||
|  |     Special type of :model:`note.Transaction` associated to a :model:`food.Order`. | ||||||
|  |     """ | ||||||
|  |     order = models.ForeignKey( | ||||||
|  |         Order, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         related_name='transaction', | ||||||
|  |         verbose_name=_('order') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("food transaction") | ||||||
|  |         verbose_name_plural = _("food transactions") | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								apps/food/static/food/js/order.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								apps/food/static/food/js/order.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | /** | ||||||
|  |  * On click of "delete", delete the order | ||||||
|  |  * @param button_id:Integer Order id to remove | ||||||
|  |  * @param table_id: Id of the table to reload | ||||||
|  |  */ | ||||||
|  | function delete_button (button_id, table_id) { | ||||||
|  |   $.ajax({ | ||||||
|  |     url: '/api/food/order/' + button_id + '/', | ||||||
|  |     method: 'DELETE', | ||||||
|  |     headers: { 'X-CSRFTOKEN': CSRF_TOKEN } | ||||||
|  |   }).done(function () { | ||||||
|  |     $('#' + table_id).load(location.pathname + ' #' + table_id + ' > *') | ||||||
|  |   }).fail(function (xhr, _textStatus, _error) { | ||||||
|  |     errMsg(xhr.responseJSON, 10000) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * On click of "Serve", mark the order as served | ||||||
|  |  * @param button_id: Order id | ||||||
|  |  * @param table_id:  Id of the table to reload | ||||||
|  |  */ | ||||||
|  | function serve_button(button_id, table_id, current_state) { | ||||||
|  |     console.log("update") | ||||||
|  |   const new_state = !current_state; | ||||||
|  |   $.ajax({ | ||||||
|  |     url: '/api/food/order/' + button_id + '/', | ||||||
|  |     method: 'PATCH', | ||||||
|  |     headers: { 'X-CSRFTOKEN': CSRF_TOKEN }, | ||||||
|  |     contentType: 'application/json', | ||||||
|  |     data: JSON.stringify({ | ||||||
|  |       served: new_state | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  |   .done(function () { | ||||||
|  |     if (current_state) { | ||||||
|  |     $('table').load(location.pathname + ' table') | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |     $('#' + table_id).load(location.pathname + ' #' + table_id + ' > *'); | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |   .fail(function (xhr) { | ||||||
|  |     errMsg(xhr.responseJSON, 10000); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -3,8 +3,11 @@ | |||||||
|  |  | ||||||
| import django_tables2 as tables | import django_tables2 as tables | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  | from note_kfet.middlewares import get_current_request | ||||||
|  | from note.templatetags.pretty_money import pretty_money | ||||||
|  | from permission.backends import PermissionBackend | ||||||
|  |  | ||||||
| from .models import Food | from .models import Food, Dish, Order | ||||||
|  |  | ||||||
|  |  | ||||||
| class FoodTable(tables.Table): | class FoodTable(tables.Table): | ||||||
| @@ -35,3 +38,84 @@ class FoodTable(tables.Table): | |||||||
|             'data-href': lambda record: 'detail/' + str(record.pk), |             'data-href': lambda record: 'detail/' + str(record.pk), | ||||||
|             'style': 'cursor:pointer', |             'style': 'cursor:pointer', | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishTable(tables.Table): | ||||||
|  |     """ | ||||||
|  |     List dishes | ||||||
|  |     """ | ||||||
|  |     supplements = tables.Column(empty_values=(), verbose_name=_('Available supplements'), orderable=False) | ||||||
|  |  | ||||||
|  |     def render_supplements(self, record): | ||||||
|  |         return ", ".join(str(q.food) for q in record.supplements.all()) | ||||||
|  |  | ||||||
|  |     def render_price(self, value): | ||||||
|  |         return pretty_money(value) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = Dish | ||||||
|  |         template_name = 'django_tables2/bootstrap4.html' | ||||||
|  |         fields = ('main', 'supplements', 'price', 'available') | ||||||
|  |         row_attrs = { | ||||||
|  |             'class': 'table-row', | ||||||
|  |             'data-href': lambda record: str(record.pk), | ||||||
|  |             'style': 'cursor:pointer', | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | DELETE_TEMPLATE = """ | ||||||
|  | <button id="{{ record.pk }}" | ||||||
|  |         class="btn btn-danger btn-sm" | ||||||
|  |         onclick="delete_button(this.id, 'orders_table_{{ table.prefix }}')"> | ||||||
|  |     {{ delete_trans }} | ||||||
|  | </button> | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SERVE_TEMPLATE = """ | ||||||
|  | <button id="{{ record.pk }}" | ||||||
|  |         class="btn btn-sm {% if record.served %}btn-secondary{% else %}btn-success{% endif %}" | ||||||
|  |         onclick="serve_button(this.id, 'orders_table_{{ table.prefix }}', {{ record.served|yesno:'true,false' }})"> | ||||||
|  |     {% if record.served %} | ||||||
|  |         {{ record.served_at|date:"d/m/Y H:i" }} | ||||||
|  |     {% else %}""" + _('Serve') + """ | ||||||
|  |     {% endif %} | ||||||
|  | </button> | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderTable(tables.Table): | ||||||
|  |     """ | ||||||
|  |     Lis all orders. | ||||||
|  |     """ | ||||||
|  |     delete = tables.TemplateColumn( | ||||||
|  |         template_code=DELETE_TEMPLATE, | ||||||
|  |         extra_context={"delete_trans": _('Delete')}, | ||||||
|  |         orderable=False, | ||||||
|  |         attrs={'td': {'class': lambda record: 'col-sm-1' + ( | ||||||
|  |             ' d-none' if not PermissionBackend.check_perm( | ||||||
|  |                 get_current_request(), "food.delete_order", | ||||||
|  |                 record) else '')}}, verbose_name=_("Delete"), ) | ||||||
|  |  | ||||||
|  |     serve = tables.TemplateColumn( | ||||||
|  |         template_code=SERVE_TEMPLATE, | ||||||
|  |         extra_context={"serve_trans": _('Serve')}, | ||||||
|  |         orderable=False, | ||||||
|  |         attrs={'td': {'class': lambda record: 'col-sm-1' + ( | ||||||
|  |             ' d-none' if not PermissionBackend.check_perm( | ||||||
|  |                 get_current_request(), "food.change_order_saved", | ||||||
|  |                 record) else '')}}, verbose_name=_("Serve"), ) | ||||||
|  |  | ||||||
|  |     request = tables.Column( | ||||||
|  |         orderable=False | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = Order | ||||||
|  |         template_name = 'django_tables2/bootstrap4.html' | ||||||
|  |         fields = ('ordered_at', 'user', 'dish', 'supplements', 'request', 'serve', 'delete') | ||||||
|  |         order_by = ('ordered_at', ) | ||||||
|  |         row_attrs = { | ||||||
|  |             'class': 'table-row', | ||||||
|  |             'style': 'cursor:pointer', | ||||||
|  |         } | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								apps/food/templates/food/dish_confirm_delete.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								apps/food/templates/food/dish_confirm_delete.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load i18n crispy_forms_tags %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  |     <div class="card bg-light"> | ||||||
|  |         <div class="card-header text-center"> | ||||||
|  |             <h4>{% trans "Delete dish" %}</h4> | ||||||
|  |         </div> | ||||||
|  |         <div class="card-body"> | ||||||
|  |             <div class="alert alert-warning"> | ||||||
|  |                 {% blocktrans %}Are you sure you want to delete this dish? This action can't be undone.{% endblocktrans %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="card-footer text-center"> | ||||||
|  |             <form method="post"> | ||||||
|  |                 {% csrf_token %} | ||||||
|  |                 <a class="btn btn-primary" href="{% url 'food:dish_detail' activity_pk=object.activity.pk pk=object.pk%}">{% trans "Return to dish detail" %}</a> | ||||||
|  |                 <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button> | ||||||
|  |             </form> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										41
									
								
								apps/food/templates/food/dish_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								apps/food/templates/food/dish_detail.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load render_table from django_tables2 %} | ||||||
|  | {% load i18n pretty_money %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card bg-white mb-3"> | ||||||
|  |   <h3 class="card-header text-center"> | ||||||
|  |     {{ title }} {{ food.name }} | ||||||
|  |   </h3> | ||||||
|  |   <div class="card-body"> | ||||||
|  |     <ul> | ||||||
|  |       <li> {% trans "Associated food" %} :  | ||||||
|  |         <a href="{% url "food:transformedfood_view" pk=food.pk %}"> | ||||||
|  |           {{ food.name }} | ||||||
|  |         </a> | ||||||
|  |       </li> | ||||||
|  |       <li> {% trans "Sell price" %} : {{ dish.price|pretty_money }}</li> | ||||||
|  |       <li> {% trans "Available" %} : {{ dish.available|yesno }}</li> | ||||||
|  |       <li> {% trans "Possible supplements" %} :  | ||||||
|  |         {% for supp in supplements %} | ||||||
|  |           <a href="{% url "food:food_view" pk=supp.food.pk %}">{{ supp.food.name }} ({{ supp.price|pretty_money }})</a>{% if not forloop.last %},{% endif %} | ||||||
|  |         {% endfor %} | ||||||
|  |       </li> | ||||||
|  |     </ul> | ||||||
|  |     {% if update %} | ||||||
|  |         <a class="btn btn-sm btn-secondary" href="{% url "food:dish_update" activity_pk=dish.activity.pk pk=dish.pk %}"> | ||||||
|  |           {% trans "Update" %} | ||||||
|  |         </a> | ||||||
|  |     {% endif %} | ||||||
|  |     {% if delete %} | ||||||
|  |     <a class="btn btn-sm btn-danger" href="{% url "food:dish_delete" activity_pk=dish.activity.pk pk=dish.pk %}"> | ||||||
|  |           {% trans "Delete" %} | ||||||
|  |         </a> | ||||||
|  |     {% endif %} | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										94
									
								
								apps/food/templates/food/dish_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								apps/food/templates/food/dish_form.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load i18n crispy_forms_tags %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card bg-white mb-3"> | ||||||
|  |   <h3 class="card-header text-center"> | ||||||
|  |     {{ title }} | ||||||
|  |   </h3> | ||||||
|  |   <form method="post" action=""> | ||||||
|  |     {% csrf_token %} | ||||||
|  |     <div class="card-body"> | ||||||
|  |       {% crispy form %} | ||||||
|  |     </div> | ||||||
|  |   <h3 class="card-header text-center"> | ||||||
|  |     {% trans "Ajouter des suppléments (optionnel)" %} | ||||||
|  |   </h3> | ||||||
|  |   {{ formset.management_form }} | ||||||
|  |   <table class="table table-condensed table-striped"> | ||||||
|  |     {% for form in formset %} | ||||||
|  |     {% if forloop.first %} | ||||||
|  |     <thead> | ||||||
|  |       <tr> | ||||||
|  |         <th>{{ form.food.label }}<span class="asteriskField">*</span></th> | ||||||
|  |         <th>{{ form.price.label }}<span class="asteriskField">*</span></th> | ||||||
|  |       </tr> | ||||||
|  |     </thead> | ||||||
|  |     <tbody id="form_body"> | ||||||
|  |       {% endif %} | ||||||
|  |       <tr class="row-formset"> | ||||||
|  |         <td>{{ form.food }}</td> | ||||||
|  |         <td>{{ form.price }}</td> | ||||||
|  |         {# These fields are hidden but handled by the formset to link the id and the invoice id #} | ||||||
|  |         {{ form.dish }} | ||||||
|  |         {{ form.id }} | ||||||
|  |       </tr> | ||||||
|  |       {% endfor %} | ||||||
|  |     </tbody> | ||||||
|  |   </table> | ||||||
|  |  | ||||||
|  |   {# Display buttons to add and remove supplements #} | ||||||
|  |     <div class="card-body"> | ||||||
|  |         <div class="btn-group btn-block" role="group"> | ||||||
|  |             <button type="button" id="add_more" class="btn btn-success">{% trans "Add supplement" %}</button> | ||||||
|  |             <button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove supplement" %}</button> | ||||||
|  |         </div> | ||||||
|  |         <button type="submit" class="btn btn-block btn-primary">{% trans "Submit" %}</button> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {# Hidden div that store an empty supplement form, to be copied into new forms #} | ||||||
|  | <div id="empty_form" style="display: none;"> | ||||||
|  |     <table class='no_error'> | ||||||
|  |         <tbody id="for_real"> | ||||||
|  |             <tr class="row-formset"> | ||||||
|  |                 <td>{{ formset.empty_form.food }}</td> | ||||||
|  |                 <td>{{ formset.empty_form.price }} </td> | ||||||
|  |                 {{ formset.empty_form.dish }} | ||||||
|  |                 {{ formset.empty_form.id }} | ||||||
|  |             </tr> | ||||||
|  |         </tbody> | ||||||
|  |     </table> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block extrajavascript %} | ||||||
|  | <script> | ||||||
|  |     /* script that handles add and remove lines */ | ||||||
|  |     IDS = {}; | ||||||
|  |  | ||||||
|  |     $("#id_supplements-TOTAL_FORMS").val($(".row-formset").length - 1); | ||||||
|  |  | ||||||
|  |     $('#add_more').click(function () { | ||||||
|  |         let form_idx = $('#id_supplements-TOTAL_FORMS').val(); | ||||||
|  |         $('#form_body').append($('#for_real').html().replace(/__prefix__/g, form_idx)); | ||||||
|  |         $('#id_supplements-TOTAL_FORMS').val(parseInt(form_idx) + 1); | ||||||
|  |         $('#id_supplements-' + parseInt(form_idx) + '-id').val(IDS[parseInt(form_idx)]); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     $('#remove_one').click(function () { | ||||||
|  |         let form_idx = $('#id_supplements-TOTAL_FORMS').val(); | ||||||
|  |         if (form_idx > 0) { | ||||||
|  |             IDS[parseInt(form_idx) - 1] = $('#id_supplements-' + (parseInt(form_idx) - 1) + '-id').val(); | ||||||
|  |             $('#form_body tr:last-child').remove(); | ||||||
|  |             $('#id_supplements-TOTAL_FORMS').val(parseInt(form_idx) - 1); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | </script> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										33
									
								
								apps/food/templates/food/dish_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								apps/food/templates/food/dish_list.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load render_table from django_tables2 %} | ||||||
|  | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card bg-white mb-3"> | ||||||
|  |     <h3 class="card-header text-center"> | ||||||
|  |         {{ title }} {{activity.name}} | ||||||
|  |     </h3> | ||||||
|  |     {% render_table table %} | ||||||
|  |     <div class="card-footer"> | ||||||
|  |         {% if can_add_dish %} | ||||||
|  |         <a class="btn btn-sm btn-success" href="{% url 'food:dish_create' activity_pk=activity.pk %}">{% trans "New dish" %}</a> | ||||||
|  |         {% endif %} | ||||||
|  |         <a class="btn btn-sm btn-secondary" href="{% url 'activity:activity_detail' pk=activity.pk %}">{% trans "Activity page" %}</a> | ||||||
|  |         <a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}"> | ||||||
|  |           {% trans "Return to the food list" %} | ||||||
|  |         </a> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block extrajavascript %} | ||||||
|  | <script type="text/javascript"> | ||||||
|  |     $(".table-row").click(function () { | ||||||
|  |             window.document.location = $(this).data("href"); | ||||||
|  |         }); | ||||||
|  | </script> | ||||||
|  | {% endblock %} | ||||||
| @@ -64,13 +64,19 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|   <h3 class="card-header text-center"> |   <h3 class="card-header text-center"> | ||||||
|     {% trans "Meal served" %} |     {% trans "Meal served" %} | ||||||
|   </h3> |   </h3> | ||||||
|   {% if can_add_meal %} |  | ||||||
|   <div class="card-footer"> |   <div class="card-footer"> | ||||||
|  |     {% if can_add_meal %} | ||||||
|     <a class="btn btn-sm btn-primary" href="{% url 'food:transformedfood_create' %}"> |     <a class="btn btn-sm btn-primary" href="{% url 'food:transformedfood_create' %}"> | ||||||
|       {% trans "New meal" %} |       {% trans "New meal" %} | ||||||
|     </a> |     </a> | ||||||
|  |     {% endif %} | ||||||
|  |     {% for activity in open_activities %} | ||||||
|  |       <a class="btn btn-sm btn-secondary" href="{% url 'food:dish_list' activity_pk=activity.pk %}"> | ||||||
|  |       {% trans "View" %} {{ activity.name }} | ||||||
|  |     </a> | ||||||
|  |     {% endfor %} | ||||||
|   </div> |   </div> | ||||||
|   {% endif %} |  | ||||||
|   {% if served.data %} |   {% if served.data %} | ||||||
|   {% render_table served %} |   {% render_table served %} | ||||||
|   {% else %} |   {% else %} | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								apps/food/templates/food/order_confirm_delete.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								apps/food/templates/food/order_confirm_delete.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load i18n crispy_forms_tags %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  |     <div class="card bg-light"> | ||||||
|  |         <div class="card-header text-center"> | ||||||
|  |             <h4>{% trans "Delete order" %}</h4> | ||||||
|  |         </div> | ||||||
|  |         <div class="card-body"> | ||||||
|  |             <div class="alert alert-warning"> | ||||||
|  |                 {% blocktrans %}Are you sure you want to delete this order? This action can't be undone.{% endblocktrans %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="card-footer text-center"> | ||||||
|  |             <form method="post"> | ||||||
|  |                 {% csrf_token %} | ||||||
|  |                 <a class="btn btn-primary" href="{% url 'food:order_list' activity_pk=object.activity.pk%}">{% trans "Return to order list" %}</a> | ||||||
|  |                 <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button> | ||||||
|  |             </form> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										21
									
								
								apps/food/templates/food/order_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/food/templates/food/order_form.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load i18n crispy_forms_tags %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card bg-white mb-3"> | ||||||
|  |   <h3 class="card-header text-center"> | ||||||
|  |     {{ title }} | ||||||
|  |   </h3> | ||||||
|  |   <div class="card-body" id="form"> | ||||||
|  |     <form method="post"> | ||||||
|  |       {% csrf_token %} | ||||||
|  |       {{ form | crispy }} | ||||||
|  |       <button class="btn btn-primary" type="submit">{% trans "Submit"%}</button> | ||||||
|  |     </form> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										30
									
								
								apps/food/templates/food/order_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/food/templates/food/order_list.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load render_table from django_tables2 %} | ||||||
|  | {% load static i18n %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card bg-white mb-3"> | ||||||
|  |     <h3 class="card-header text-center"> | ||||||
|  |         {{ title }} | ||||||
|  |     </h3> | ||||||
|  |     <a class="btn btn-primary" href="{% url 'food:served_order_list' activity_pk=activity.pk %}">{% trans "View served orders" %}</a> | ||||||
|  |       {% for table in tables %} | ||||||
|  |         <div class="card bg-light mb-3" id="orders_table_{{ table.prefix }}"> | ||||||
|  |         <h3 class="card-header text-center"> | ||||||
|  |             {% trans "Orders of " %} {{ table.prefix }}  | ||||||
|  |         </h3> | ||||||
|  |         {% if table.data %} | ||||||
|  |             {% render_table table %} | ||||||
|  |         {% endif %} | ||||||
|  |         </div> | ||||||
|  |       {% endfor %} | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block extrajavascript %} | ||||||
|  | <script src="{% static "food/js/order.js" %}"></script> | ||||||
|  | {% endblock%} | ||||||
							
								
								
									
										21
									
								
								apps/food/templates/food/served_order_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/food/templates/food/served_order_list.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load render_table from django_tables2 %} | ||||||
|  | {% load static i18n %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card bg-white mb-3"> | ||||||
|  |     <h3 class="card-header text-center"> | ||||||
|  |         {{ title }} {{activity.name}} | ||||||
|  |     </h3> | ||||||
|  |     <a class="btn btn-primary" href="{% url 'food:order_list' activity_pk=activity.pk %}">{% trans "View unserved orders" %}</a> | ||||||
|  |     {% render_table table %} | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block extrajavascript %} | ||||||
|  | <script src="{% static "food/js/order.js" %}"></script> | ||||||
|  | {% endblock%} | ||||||
							
								
								
									
										17
									
								
								apps/food/templates/food/supplement_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/food/templates/food/supplement_detail.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load render_table from django_tables2 %} | ||||||
|  | {% load i18n pretty_money %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card bg-white mb-3"> | ||||||
|  |   <h3 class="card-header text-center"> | ||||||
|  |     {{ title }} {{ supplement.name }} | ||||||
|  |   </h3> | ||||||
|  |   <div class="card-body"> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
| @@ -19,4 +19,14 @@ urlpatterns = [ | |||||||
|     path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'), |     path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'), | ||||||
|     path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'), |     path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'), | ||||||
|     path('redirect/', views.QRCodeRedirectView.as_view(), name='redirect_view'), |     path('redirect/', views.QRCodeRedirectView.as_view(), name='redirect_view'), | ||||||
|  |     # TODO not always store activity_pk in url | ||||||
|  |     path('activity/<int:activity_pk>/dishes/add/', views.DishCreateView.as_view(), name='dish_create'), | ||||||
|  |     path('activity/<int:activity_pk>/dishes/', views.DishListView.as_view(), name='dish_list'), | ||||||
|  |     path('activity/<int:activity_pk>/dishes/<int:pk>/', views.DishDetailView.as_view(), name='dish_detail'), | ||||||
|  |     path('activity/<int:activity_pk>/dishes/<int:pk>/update/', views.DishUpdateView.as_view(), name='dish_update'), | ||||||
|  |     path('activity/<int:activity_pk>/dishes/<int:pk>/delete/', views.DishDeleteView.as_view(), name='dish_delete'), | ||||||
|  |     path('activity/<int:activity_pk>/order/', views.OrderCreateView.as_view(), name='order_create'), | ||||||
|  |     path('activity/<int:activity_pk>/orders/', views.OrderListView.as_view(), name='order_list'), | ||||||
|  |     path('activity/<int:activity_pk>/orders/served', views.ServedOrderListView.as_view(), name='served_order_list'), | ||||||
|  |     path('activity/orders/<int:pk>/delete/', views.OrderDeleteView.as_view(), name='order_delete'), | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -4,25 +4,30 @@ | |||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
|  |  | ||||||
| from api.viewsets import is_regex | from api.viewsets import is_regex | ||||||
| from django_tables2.views import MultiTableMixin | from crispy_forms.helper import FormHelper | ||||||
|  | from django_tables2.views import SingleTableView, MultiTableMixin | ||||||
|  | from django.core.exceptions import PermissionDenied | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| from django.http import HttpResponseRedirect, Http404 | from django.http import HttpResponseRedirect, Http404 | ||||||
| from django.views.generic import DetailView, UpdateView, CreateView | from django.views.generic import DetailView, UpdateView, CreateView | ||||||
| from django.views.generic.list import ListView | from django.views.generic.list import ListView | ||||||
| from django.views.generic.base import RedirectView | from django.views.generic.base import RedirectView | ||||||
|  | from django.views.generic.edit import DeleteView | ||||||
| from django.urls import reverse_lazy | from django.urls import reverse_lazy | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from member.models import Club, Membership | from member.models import Club, Membership | ||||||
|  | from activity.models import Activity | ||||||
| from permission.backends import PermissionBackend | from permission.backends import PermissionBackend | ||||||
| from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin | from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin | ||||||
|  |  | ||||||
| from .models import Food, BasicFood, TransformedFood, QRCode | from .models import Food, BasicFood, TransformedFood, QRCode, Order, Dish | ||||||
| from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \ | from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \ | ||||||
|     ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \ |     ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \ | ||||||
|     BasicFoodUpdateForms, TransformedFoodUpdateForms |     BasicFoodUpdateForms, TransformedFoodUpdateForms, \ | ||||||
| from .tables import FoodTable |     DishForm, SupplementFormSet, SupplementFormSetHelper, OrderForm | ||||||
|  | from .tables import FoodTable, DishTable, OrderTable | ||||||
| from .utils import pretty_duration | from .utils import pretty_duration | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -112,6 +117,9 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li | |||||||
|         context['club_tables'] = tables[3:] |         context['club_tables'] = tables[3:] | ||||||
|  |  | ||||||
|         context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add') |         context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add') | ||||||
|  |  | ||||||
|  |         context["open_activities"] = Activity.objects.filter(activity_type__name="Perm bouffe", open=True) | ||||||
|  |  | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -526,3 +534,270 @@ class QRCodeRedirectView(RedirectView): | |||||||
|         if slug: |         if slug: | ||||||
|             return reverse_lazy('food:qrcode_create', kwargs={'slug': slug}) |             return reverse_lazy('food:qrcode_create', kwargs={'slug': slug}) | ||||||
|         return reverse_lazy('food:list') |         return reverse_lazy('food:list') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishCreateView(ProtectQuerysetMixin, ProtectedCreateView): | ||||||
|  |     """ | ||||||
|  |     Create a dish | ||||||
|  |     """ | ||||||
|  |     model = Dish | ||||||
|  |     form_class = DishForm | ||||||
|  |     extra_context = {"title": _('Create dish')} | ||||||
|  |  | ||||||
|  |     def get_sample_object(self): | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |         sample_food = TransformedFood( | ||||||
|  |             name="Sample food", | ||||||
|  |             owner=activity.organizer, | ||||||
|  |             expiry_date=timezone.now() + timedelta(days=7), | ||||||
|  |             is_ready=True, | ||||||
|  |         ) | ||||||
|  |         sample_dish = Dish( | ||||||
|  |             main=sample_food, | ||||||
|  |             price=100, | ||||||
|  |             activity=activity, | ||||||
|  |         ) | ||||||
|  |         return sample_dish | ||||||
|  |  | ||||||
|  |     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 supplements | ||||||
|  |         form_set = SupplementFormSet(instance=form.instance) | ||||||
|  |         context['formset'] = form_set | ||||||
|  |         context['helper'] = SupplementFormSetHelper() | ||||||
|  |  | ||||||
|  |         return context | ||||||
|  |  | ||||||
|  |     def get_form(self, form_class=None): | ||||||
|  |         form = super().get_form(form_class) | ||||||
|  |         if "available" in form.fields: | ||||||
|  |             del form.fields["available"] | ||||||
|  |         return form | ||||||
|  |  | ||||||
|  |     @transaction.atomic | ||||||
|  |     def form_valid(self, form): | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |  | ||||||
|  |         form.instance.activity = activity | ||||||
|  |  | ||||||
|  |         ret = super().form_valid(form) | ||||||
|  |  | ||||||
|  |         # For each supplement, we save it | ||||||
|  |         formset = SupplementFormSet(self.request.POST, instance=form.instance) | ||||||
|  |         if formset.is_valid(): | ||||||
|  |             for f in formset: | ||||||
|  |                 if f.is_valid(): | ||||||
|  |                     f.save() | ||||||
|  |                     f.instance.save() | ||||||
|  |                 else: | ||||||
|  |                     f.instance = None | ||||||
|  |  | ||||||
|  |         return ret | ||||||
|  |  | ||||||
|  |     def get_success_url(self): | ||||||
|  |         return reverse_lazy('food:dish_list', kwargs={"activity_pk": self.kwargs["activity_pk"]}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||||
|  |     """ | ||||||
|  |     List dishes for this activity | ||||||
|  |     """ | ||||||
|  |     model = Dish | ||||||
|  |     table_class = DishTable | ||||||
|  |     extra_context = {"title": _('Dishes served during')} | ||||||
|  |     template_name = 'food/dish_list.html' | ||||||
|  |  | ||||||
|  |     def get_queryset(self): | ||||||
|  |         return super().get_queryset().filter(activity__pk=self.kwargs["activity_pk"]) | ||||||
|  |  | ||||||
|  |     def get_context_data(self, **kwargs): | ||||||
|  |         context = super().get_context_data(**kwargs) | ||||||
|  |  | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |         context["activity"] = activity | ||||||
|  |  | ||||||
|  |         context["can_add_dish"] = PermissionBackend.check_perm(self.request, 'food.dish_add') | ||||||
|  |  | ||||||
|  |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||||
|  |     """ | ||||||
|  |     View a dish for this activity | ||||||
|  |     """ | ||||||
|  |     model = Dish | ||||||
|  |     extra_context = {"title": _('Details of:')} | ||||||
|  |     context_oject_name = "dish" | ||||||
|  |  | ||||||
|  |     def get_context_data(self, **kwargs): | ||||||
|  |         context = super().get_context_data(**kwargs) | ||||||
|  |  | ||||||
|  |         context["food"] = self.object.main | ||||||
|  |  | ||||||
|  |         context["supplements"] = self.object.supplements.all() | ||||||
|  |  | ||||||
|  |         context["update"] = PermissionBackend.check_perm(self.request, "food.change_dish") | ||||||
|  |  | ||||||
|  |         context["delete"] = not Order.objects.filter(dish=self.get_object()).exists() and PermissionBackend.check_perm(self.request, "food.delete_dish") | ||||||
|  |  | ||||||
|  |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||||
|  |     """ | ||||||
|  |     A view to update a dish | ||||||
|  |     """ | ||||||
|  |     model = Dish | ||||||
|  |     form_class = DishForm | ||||||
|  |     extra_context = {"title": _("Update a dish")} | ||||||
|  |  | ||||||
|  |     def get_form(self, **kwargs): | ||||||
|  |         form = super().get_form(**kwargs) | ||||||
|  |         if 'main' in form.fields: | ||||||
|  |             del form.fields["main"] | ||||||
|  |         return form | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DishDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): | ||||||
|  |     """ | ||||||
|  |     Delete a dish with no order yet | ||||||
|  |     """ | ||||||
|  |     model = Dish | ||||||
|  |     extra_context = {"title": _('Delete dish')} | ||||||
|  |  | ||||||
|  |     def delete(self, request, *args, **kwargs): | ||||||
|  |         if Order.objects.filter(dish=self.get_object()).exists(): | ||||||
|  |             raise PermissionDenied(_("This dish cannot be deleted because it has already been ordered")) | ||||||
|  |         return super().delete(request, *args, **kwargs) | ||||||
|  |  | ||||||
|  |     def get_success_url(self): | ||||||
|  |         return reverse_lazy('food:dish_list', kwargs={"activity_pk": self.kwargs["activity_pk"]}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderCreateView(ProtectQuerysetMixin, ProtectedCreateView): | ||||||
|  |     """ | ||||||
|  |     Order a meal | ||||||
|  |     """ | ||||||
|  |     model = Order | ||||||
|  |     form_class = OrderForm | ||||||
|  |     extra_context = {"title": _('Order food')} | ||||||
|  |  | ||||||
|  |     def get_sample_object(self): | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |         sample_order = Order( | ||||||
|  |             user=self.request.user, | ||||||
|  |             activity=activity, | ||||||
|  |             dish=Dish.objects.filter(activity=activity).last(), | ||||||
|  |         ) | ||||||
|  |         return sample_order | ||||||
|  |  | ||||||
|  |     def get_form(self): | ||||||
|  |         form = super().get_form() | ||||||
|  |  | ||||||
|  |         form.fields["user"].initial = self.request.user | ||||||
|  |         form.fields["user"].disabled = True | ||||||
|  |  | ||||||
|  |         return form | ||||||
|  |  | ||||||
|  |     def form_valid(self, form): | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |  | ||||||
|  |         form.instance.activity = activity | ||||||
|  |  | ||||||
|  |         return super().form_valid(form) | ||||||
|  |  | ||||||
|  |     def get_success_url(self): | ||||||
|  |         return reverse_lazy('food:food_list') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): | ||||||
|  |     """ | ||||||
|  |     List existing Families | ||||||
|  |     """ | ||||||
|  |     model = Order | ||||||
|  |     table_class = OrderTable | ||||||
|  |     extra_context = {"title": _('Order list')} | ||||||
|  |     paginate_by = 10 | ||||||
|  |  | ||||||
|  |     def get_queryset(self, **kwargs): | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |         return Order.objects.filter(activity=activity) | ||||||
|  |  | ||||||
|  |     def get_tables(self): | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |         dishes = Dish.objects.filter(activity=activity) | ||||||
|  |  | ||||||
|  |         tables = [OrderTable] * dishes.count() | ||||||
|  |         self.tables = tables | ||||||
|  |         tables = super().get_tables() | ||||||
|  |         for i in range(dishes.count()): | ||||||
|  |             tables[i].prefix = dishes[i].main.name | ||||||
|  |         return tables | ||||||
|  |  | ||||||
|  |     def get_tables_data(self): | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |         dishes = Dish.objects.filter(activity=activity) | ||||||
|  |  | ||||||
|  |         tables = [] | ||||||
|  |  | ||||||
|  |         for dish in dishes: | ||||||
|  |             tables.append(self.get_queryset().order_by('ordered_at').filter( | ||||||
|  |                 dish=dish, served=False).filter( | ||||||
|  |                     PermissionBackend.filter_queryset(self.request, Order, 'view') | ||||||
|  |             )) | ||||||
|  |  | ||||||
|  |         return tables | ||||||
|  |  | ||||||
|  |     def get_context_data(self, **kwargs): | ||||||
|  |         context = super().get_context_data(**kwargs) | ||||||
|  |  | ||||||
|  |         context["activity"] = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |  | ||||||
|  |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ServedOrderListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||||
|  |     """ | ||||||
|  |     View served orders | ||||||
|  |     """ | ||||||
|  |     model = Order | ||||||
|  |     template_name = 'food/served_order_list.html' | ||||||
|  |     table_class = OrderTable | ||||||
|  |  | ||||||
|  |     def get_queryset(self): | ||||||
|  |         return super().get_queryset().filter(activity__pk=self.kwargs["activity_pk"], served=True).order_by('-served_at') | ||||||
|  |  | ||||||
|  |     def get_table(self, **kwargs): | ||||||
|  |         table = super().get_table(**kwargs) | ||||||
|  |  | ||||||
|  |         table.columns.hide("delete") | ||||||
|  |  | ||||||
|  |         return table | ||||||
|  |  | ||||||
|  |     def get_context_data(self, **kwargs): | ||||||
|  |         context = super().get_context_data(**kwargs) | ||||||
|  |  | ||||||
|  |         context["activity"] = Activity.objects.get(pk=self.kwargs["activity_pk"]) | ||||||
|  |  | ||||||
|  |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): | ||||||
|  |     """ | ||||||
|  |     Delete an order | ||||||
|  |     """ | ||||||
|  |     model = Order | ||||||
|  |     extra_context = {"title": _('Delete dish')} | ||||||
|  |  | ||||||
|  |     def delete(self, request, *args, **kwargs): | ||||||
|  |         if self.get_object().served: | ||||||
|  |             raise PermissionDenied(_("This order cannot be deleted because it has already been served")) | ||||||
|  |         return super().delete(request, *args, **kwargs) | ||||||
|  |  | ||||||
|  |     def get_success_url(self): | ||||||
|  |         return reverse_lazy('food:order_list', kwargs={"activity_pk": self.kwargs["activity_pk"]}) | ||||||
|   | |||||||
| @@ -74,6 +74,7 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView): | |||||||
|  |  | ||||||
|         # For each product, we save it |         # For each product, we save it | ||||||
|         formset = ProductFormSet(self.request.POST, instance=form.instance) |         formset = ProductFormSet(self.request.POST, instance=form.instance) | ||||||
|  |         print(formset) | ||||||
|         if formset.is_valid(): |         if formset.is_valid(): | ||||||
|             for f in formset: |             for f in formset: | ||||||
|                 # We don't save the product if the designation is not entered, ie. if the line is empty |                 # We don't save the product if the designation is not entered, ie. if the line is empty | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user