mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-30 23:39:54 +01:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			03932672f3
			...
			sheets
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 51d60d064c | |||
| 45334e4e02 | |||
| 5174c84b33 | |||
| 51e5e3669e | |||
| 44994a3ae7 | |||
| ba017c38c0 | 
| @@ -26,6 +26,10 @@ if "note" in settings.INSTALLED_APPS: | |||||||
|     from note.api.urls import register_note_urls |     from note.api.urls import register_note_urls | ||||||
|     register_note_urls(router, 'note') |     register_note_urls(router, 'note') | ||||||
|  |  | ||||||
|  | if "sheets" in settings.INSTALLED_APPS: | ||||||
|  |     from sheets.api.urls import register_sheets_urls | ||||||
|  |     register_sheets_urls(router, 'sheets') | ||||||
|  |  | ||||||
| if "treasury" in settings.INSTALLED_APPS: | if "treasury" in settings.INSTALLED_APPS: | ||||||
|     from treasury.api.urls import register_treasury_urls |     from treasury.api.urls import register_treasury_urls | ||||||
|     register_treasury_urls(router, 'treasury') |     register_treasury_urls(router, 'treasury') | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								apps/member/migrations/0009_auto_20220818_1301.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/member/migrations/0009_auto_20220818_1301.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | # Generated by Django 2.2.27 on 2022-08-18 11:01 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('member', '0008_auto_20211005_1544'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='profile', | ||||||
|  |             name='promotion', | ||||||
|  |             field=models.PositiveSmallIntegerField(default=2022, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										17
									
								
								apps/note/templates/sheets/order.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/note/templates/sheets/order.html
									
									
									
									
									
										Normal 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 %} | ||||||
							
								
								
									
										88
									
								
								apps/note/templates/sheets/waiting_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								apps/note/templates/sheets/waiting_list.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  |  | ||||||
|  | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card"> | ||||||
|  | <div class="card-header text-center"> | ||||||
|  |     <h1>{{ food.name }}</h1> | ||||||
|  | </div> | ||||||
|  | <div class="card-body"> | ||||||
|  |     <div class="row"> | ||||||
|  |         <div class="card col-xl-6"> | ||||||
|  |             <div class="card-header text-center"> | ||||||
|  |                 <h2>{% trans "queued"|capfirst %}{% if queue %} ({{ queue|length }}){% endif %}</h2> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <ul> | ||||||
|  |                     {% for ordered_food in queue %} | ||||||
|  |                         <li> | ||||||
|  |                             {{ ordered_food.order.note }} | ||||||
|  |                             {% if ordered_food.priority %} | ||||||
|  |                                 <span class="badge badge-secondary">{{ ordered_food.priority }}</span> | ||||||
|  |                             {% endif %} | ||||||
|  |                         </li> | ||||||
|  |                     {% empty %} | ||||||
|  |                         <div class="alert alert-warning"> | ||||||
|  |                             {% trans "There is no queued order." %} | ||||||
|  |                         </div> | ||||||
|  |                     {% endfor %} | ||||||
|  |                 </ul> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="card col-xl-6"> | ||||||
|  |             <div class="card-header text-center"> | ||||||
|  |                 <h2>{% trans "ready"|capfirst %}</h2> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <ul> | ||||||
|  |                     {% for ordered_food in ready %} | ||||||
|  |                         <li>{{ ordered_food.order.note }}</li> | ||||||
|  |                     {% empty %} | ||||||
|  |                         <div class="alert alert-warning"> | ||||||
|  |                             {% trans "There is no ready order." %} | ||||||
|  |                         </div> | ||||||
|  |                     {% endfor %} | ||||||
|  |                 </ul> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <hr> | ||||||
|  |  | ||||||
|  |     <h3>{% trans "Other waiting lists:" %}</h3> | ||||||
|  |     <ul> | ||||||
|  |         {% for other_food in food.sheet.food_set.all %} | ||||||
|  |             {% if other_food != food %} | ||||||
|  |                 <li> | ||||||
|  |                     <a href="{% url 'sheets:waiting_list' pk=other_food.pk %}">{{ other_food }}</a> | ||||||
|  |                 </li> | ||||||
|  |             {% endif %} | ||||||
|  |         {% endfor %} | ||||||
|  |     </ul> | ||||||
|  | </div> | ||||||
|  | <div class="card-footer text-center"> | ||||||
|  |     <a href="{% url 'sheets:queued_list' pk=food.pk %}" class="btn btn-primary"> | ||||||
|  |         {% trans "Queued orders" %} | ||||||
|  |     </a> | ||||||
|  |     <a href="{% url 'sheets:ready_list' pk=food.pk %}" class="btn btn-primary"> | ||||||
|  |         {% trans "Ready orders" %} | ||||||
|  |     </a> | ||||||
|  |     <a href="{% url 'sheets:sheet_detail' pk=food.sheet_id %}" class="btn btn-secondary"> | ||||||
|  |         {% trans "Back to note sheet detail" %} | ||||||
|  |     </a> | ||||||
|  | </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block extrajavascript %} | ||||||
|  |     <script> | ||||||
|  |         function reload() { | ||||||
|  |             reloadWithTurbolinks() | ||||||
|  |             timeout = setTimeout(reload, 15000) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (timeout === undefined) | ||||||
|  |             var timeout = setTimeout(reload, 15000) | ||||||
|  |     </script> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										152
									
								
								apps/note/templates/sheets/waiting_list_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								apps/note/templates/sheets/waiting_list_detail.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  |  | ||||||
|  | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card"> | ||||||
|  | <div class="card-header text-center"> | ||||||
|  |     <h1>{{ title }}</h1> | ||||||
|  | </div> | ||||||
|  | <div class="card-body"> | ||||||
|  |     {% for of in orders %} | ||||||
|  |         <div class="card mb-4"> | ||||||
|  |             <div class="card-header text-center"> | ||||||
|  |                 <h3>{{ of.order.note }}</h3> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body"> | ||||||
|  |                 <dl class="row"> | ||||||
|  |                     <dt class="col-xl-3">{% trans 'date'|capfirst %}</dt> | ||||||
|  |                     <dd class="col-xl-9">{{ of.order.date }}</dd> | ||||||
|  |  | ||||||
|  |                     {% if of.number > 1 %} | ||||||
|  |                         <dt class="col-xl-3">{% trans 'order number'|capfirst %}</dt> | ||||||
|  |                         <dd class="col-xl-9">{{ of.number }}</dd> | ||||||
|  |                     {% endif %} | ||||||
|  |  | ||||||
|  |                     {% if of.priority %} | ||||||
|  |                         <dt class="col-xl-3">{% trans 'priority request'|capfirst %}</dt> | ||||||
|  |                         <dd class="col-xl-9">{{ of.priority }}</dd> | ||||||
|  |                     {% endif %} | ||||||
|  |  | ||||||
|  |                     {% if of.remark %} | ||||||
|  |                         <dt class="col-xl-3">{% trans 'remark'|capfirst %}</dt> | ||||||
|  |                         <dd class="col-xl-9">{{ of.remark }}</dd> | ||||||
|  |                     {% endif %} | ||||||
|  |  | ||||||
|  |                     {% if of.options.count %} | ||||||
|  |                         <dt class="col-xl-3">{% trans 'options'|capfirst %}</dt> | ||||||
|  |                         <dd class="col-xl-9">{{ of.options.all|join:', ' }}</dd> | ||||||
|  |                     {% endif %} | ||||||
|  |                 </dl> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-footer text-center"> | ||||||
|  |                 {% if list_type != 'READY' %} | ||||||
|  |                     <a href="#" class="btn btn-success" onclick="setOrderStatus({{ of.pk }}, 'READY')"> | ||||||
|  |                         {% trans "Mark as ready" %} | ||||||
|  |                     </a> | ||||||
|  |                 {% endif %} | ||||||
|  |                 {% if list_type != 'SERVED' %} | ||||||
|  |                     <a href="#" class="btn btn-primary" onclick="setOrderStatus({{ of.pk }}, 'SERVED')"> | ||||||
|  |                         {% trans "Mark as served" %} | ||||||
|  |                     </a> | ||||||
|  |                 {% endif %} | ||||||
|  |                 {% if list_type != 'QUEUED' %} | ||||||
|  |                     <a href="#" class="btn btn-warning" onclick="setOrderStatus({{ of.pk }}, 'QUEUED')"> | ||||||
|  |                         {% trans "Re-queue" %} | ||||||
|  |                     </a> | ||||||
|  |                 {% endif %} | ||||||
|  |                 {% if list_type != 'CANCELED' %} | ||||||
|  |                     <a href="#" class="btn btn-danger" onclick="setOrderStatus({{ of.pk }}, 'CANCELED')"> | ||||||
|  |                         {% trans "Cancel" %} | ||||||
|  |                     </a> | ||||||
|  |                 {% endif %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     {% empty %} | ||||||
|  |         <div class="alert alert-warning"> | ||||||
|  |             {% trans "There is no queued order." %} | ||||||
|  |         </div> | ||||||
|  |     {% endfor %} | ||||||
|  | </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | <div class="card mt-5"> | ||||||
|  | <div class="card-body"> | ||||||
|  |     <h3>{% trans "Other waiting lists:" %}</h3> | ||||||
|  |     <ul> | ||||||
|  |         {% for other_food in food.sheet.food_set.all %} | ||||||
|  |             {% if other_food != food %} | ||||||
|  |                 <li> | ||||||
|  |                     {% if list_type == 'QUEUED' %} | ||||||
|  |                         <a href="{% url 'sheets:queued_list' pk=other_food.pk %}">{{ other_food }}</a> | ||||||
|  |                     {% else %} | ||||||
|  |                         <a href="{% url 'sheets:ready_list' pk=other_food.pk %}">{{ other_food }}</a> | ||||||
|  |                     {% endif %} | ||||||
|  |                 </li> | ||||||
|  |             {% endif %} | ||||||
|  |         {% endfor %} | ||||||
|  |     </ul> | ||||||
|  | </div> | ||||||
|  | <div class="card-footer text-center"> | ||||||
|  |     {% if list_type != 'QUEUED' %} | ||||||
|  |         <a href="{% url 'sheets:queued_list' pk=food.pk %}" class="btn btn-primary"> | ||||||
|  |             {% trans "Queued orders" %} | ||||||
|  |         </a> | ||||||
|  |     {% endif %} | ||||||
|  |     {% if list_type != 'READY' %} | ||||||
|  |         <a href="{% url 'sheets:ready_list' pk=food.pk %}" class="btn btn-success"> | ||||||
|  |             {% trans "Ready orders" %} | ||||||
|  |         </a> | ||||||
|  |     {% endif %} | ||||||
|  |     {% if list_type != 'SERVED' %} | ||||||
|  |         <a href="{% url 'sheets:served_list' pk=food.pk %}" class="btn btn-secondary"> | ||||||
|  |             {% trans "Served orders" %} | ||||||
|  |         </a> | ||||||
|  |     {% endif %} | ||||||
|  |     {% if list_type != 'CANCELED' %} | ||||||
|  |         <a href="{% url 'sheets:canceled_list' pk=food.pk %}" class="btn btn-danger"> | ||||||
|  |             {% trans "Canceled orders" %} | ||||||
|  |         </a> | ||||||
|  |     {% endif %} | ||||||
|  |     <a href="{% url 'sheets:waiting_list' pk=food.pk %}" class="btn btn-primary"> | ||||||
|  |         {% trans "Waiting list" %} | ||||||
|  |     </a> | ||||||
|  |     <a href="{% url 'sheets:sheet_detail' pk=food.sheet_id %}" class="btn btn-secondary"> | ||||||
|  |         {% trans "Back to note sheet detail" %} | ||||||
|  |     </a> | ||||||
|  | </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block extrajavascript %} | ||||||
|  |     <script> | ||||||
|  |         function reload() { | ||||||
|  |             reloadWithTurbolinks() | ||||||
|  |             timeout = setTimeout(reload, 15000) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (timeout === undefined) | ||||||
|  |             var timeout = setTimeout(reload, 15000) | ||||||
|  |  | ||||||
|  |         function setOrderStatus(ordered_food_id, status) { | ||||||
|  |             fetch('/api/sheets/orderedfood/' + ordered_food_id + '/', { | ||||||
|  |                 method: 'PATCH', | ||||||
|  |                 body: JSON.stringify({ | ||||||
|  |                     status: status, | ||||||
|  |                     served_date: status === 'QUEUED' ? null : new Date().toISOString(), | ||||||
|  |                 }), | ||||||
|  |                 headers: { | ||||||
|  |                     'Content-Type': "application/json; charset=UTF-8", | ||||||
|  |                     'X-CSRFTOKEN': "{{ csrf_token }}" | ||||||
|  |                 } | ||||||
|  |             }).then(response => response.json()).then(response => { | ||||||
|  |                 if ('detail' in response) | ||||||
|  |                     addMsg("{% trans "An error occurred" %}" + " : " + response['detail'], "danger") | ||||||
|  |                 else { | ||||||
|  |                     clearTimeout(timeout) | ||||||
|  |                     reload() | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
|  | {% endblock %} | ||||||
| @@ -2928,7 +2928,7 @@ | |||||||
| 				"application" | 				"application" | ||||||
| 			], | 			], | ||||||
| 			"query": "{\"user\": [\"user\"]}", | 			"query": "{\"user\": [\"user\"]}", | ||||||
| 			"type": "create", | 			"type": "add", | ||||||
| 			"mask": 1, | 			"mask": 1, | ||||||
| 			"field": "", | 			"field": "", | ||||||
| 			"permanent": true, | 			"permanent": true, | ||||||
| @@ -3114,10 +3114,10 @@ | |||||||
| 				187, | 				187, | ||||||
| 				188, | 				188, | ||||||
| 				189, | 				189, | ||||||
|                 190, | 				190, | ||||||
|                 191, | 				191, | ||||||
|                 195, | 				195, | ||||||
|                 196 | 				196 | ||||||
| 			] | 			] | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| @@ -3159,8 +3159,8 @@ | |||||||
| 				159, | 				159, | ||||||
| 				160, | 				160, | ||||||
| 				179, | 				179, | ||||||
|                 189, | 				189, | ||||||
|                 190 | 				190 | ||||||
| 			] | 			] | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| @@ -3310,10 +3310,10 @@ | |||||||
| 				176, | 				176, | ||||||
| 				177, | 				177, | ||||||
| 				178, | 				178, | ||||||
|                 188, | 				188, | ||||||
| 				183, | 				183, | ||||||
|                 186, | 				186, | ||||||
|                 187 | 				187 | ||||||
| 			] | 			] | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| @@ -3508,13 +3508,13 @@ | |||||||
| 				187, | 				187, | ||||||
| 				188, | 				188, | ||||||
| 				189, | 				189, | ||||||
|                 190, | 				190, | ||||||
|                 191, | 				191, | ||||||
|                 192, | 				192, | ||||||
|                 193, | 				193, | ||||||
|                 194, | 				194, | ||||||
|                 195, | 				195, | ||||||
|                 196 | 				196 | ||||||
| 			] | 			] | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								apps/sheets/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/sheets/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | # Copyright (C) 2018-2022 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | default_app_config = 'sheets.apps.SheetsConfig' | ||||||
							
								
								
									
										46
									
								
								apps/sheets/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								apps/sheets/admin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | # Copyright (C) 2018-2022 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from django.contrib import admin | ||||||
|  | from note_kfet.admin import admin_site | ||||||
|  | from sheets.models import Sheet, Food, FoodOption, Meal, Order, OrderedMeal, OrderedFood, SheetOrderTransaction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @admin.register(Sheet, site=admin_site) | ||||||
|  | class SheetAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @admin.register(Food, site=admin_site) | ||||||
|  | class FoodAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @admin.register(FoodOption, site=admin_site) | ||||||
|  | class FoodOptionAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @admin.register(Meal, site=admin_site) | ||||||
|  | class MealAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @admin.register(Order, site=admin_site) | ||||||
|  | class OrderAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @admin.register(OrderedMeal, site=admin_site) | ||||||
|  | class OrderedMealAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @admin.register(OrderedFood, site=admin_site) | ||||||
|  | class OrderedFoodAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @admin.register(SheetOrderTransaction, site=admin_site) | ||||||
|  | class SheetOrderTransactionAdmin(admin.ModelAdmin): | ||||||
|  |     pass | ||||||
							
								
								
									
										0
									
								
								apps/sheets/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/sheets/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										55
									
								
								apps/sheets/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								apps/sheets/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from rest_framework import serializers | ||||||
|  |  | ||||||
|  |  | ||||||
|  | from ..models import Sheet, Food, FoodOption, Meal, Order, OrderedMeal, OrderedFood, SheetOrderTransaction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SheetSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = Sheet | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = Food | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodOptionSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = FoodOption | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MealSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = Meal | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = Order | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderedMealSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = OrderedMeal | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderedFoodSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = OrderedFood | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SheetOrderTransactionSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = SheetOrderTransaction | ||||||
|  |         fields = '__all__' | ||||||
							
								
								
									
										19
									
								
								apps/sheets/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/sheets/api/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from sheets.api.views import SheetViewSet, FoodViewSet, FoodOptionViewSet, MealViewSet, OrderViewSet, \ | ||||||
|  |     OrderedMealViewSet, OrderedFoodViewSet, SheetOrderTransactionViewSet | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def register_sheets_urls(router, path): | ||||||
|  |     """ | ||||||
|  |     Configure router for Sheets REST API. | ||||||
|  |     """ | ||||||
|  |     router.register(path + '/sheet', SheetViewSet) | ||||||
|  |     router.register(path + '/food', FoodViewSet) | ||||||
|  |     router.register(path + '/foodoption', FoodOptionViewSet) | ||||||
|  |     router.register(path + '/meal', MealViewSet) | ||||||
|  |     router.register(path + '/order', OrderViewSet) | ||||||
|  |     router.register(path + '/orderedmeal', OrderedMealViewSet) | ||||||
|  |     router.register(path + '/orderedfood', OrderedFoodViewSet) | ||||||
|  |     router.register(path + '/sheetordertransaction', SheetOrderTransactionViewSet) | ||||||
							
								
								
									
										78
									
								
								apps/sheets/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								apps/sheets/api/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | |||||||
|  | # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from api.viewsets import ReadProtectedModelViewSet | ||||||
|  | from django_filters.rest_framework import DjangoFilterBackend | ||||||
|  | from rest_framework.filters import SearchFilter, OrderingFilter | ||||||
|  |  | ||||||
|  | from .serializers import SheetSerializer, FoodSerializer, FoodOptionSerializer, MealSerializer, OrderSerializer, \ | ||||||
|  |     OrderedMealSerializer, OrderedFoodSerializer, SheetOrderTransactionSerializer | ||||||
|  | from ..models import Sheet, Food, FoodOption, Meal, Order, OrderedMeal, OrderedFood, SheetOrderTransaction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SheetViewSet(ReadProtectedModelViewSet): | ||||||
|  |     queryset = Sheet.objects.order_by('id') | ||||||
|  |     serializer_class = SheetSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['name', 'date', ] | ||||||
|  |     search_fields = ['$name', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodViewSet(ReadProtectedModelViewSet): | ||||||
|  |     queryset = Food.objects.order_by('id') | ||||||
|  |     serializer_class = FoodSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['name', 'sheet', 'price', 'club', 'available', ] | ||||||
|  |     search_fields = ['$name', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodOptionViewSet(ReadProtectedModelViewSet): | ||||||
|  |     queryset = FoodOption.objects.order_by('id') | ||||||
|  |     serializer_class = FoodOptionSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['name', 'food', 'extra_cost', 'available', ] | ||||||
|  |     search_fields = ['$name', '$food__name', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MealViewSet(ReadProtectedModelViewSet): | ||||||
|  |     queryset = Meal.objects.order_by('id') | ||||||
|  |     serializer_class = MealSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['name', 'content', 'price', 'available', ] | ||||||
|  |     search_fields = ['$name', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderViewSet(ReadProtectedModelViewSet): | ||||||
|  |     queryset = Order.objects.order_by('id') | ||||||
|  |     serializer_class = OrderSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['sheet', 'note', 'date', 'gift', ] | ||||||
|  |     search_fields = ['$sheet__name', '$note__alias__name', '$note__alias__normalized_name', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderedMealViewSet(ReadProtectedModelViewSet): | ||||||
|  |     queryset = OrderedMeal.objects.order_by('id') | ||||||
|  |     serializer_class = OrderedMealSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend] | ||||||
|  |     filterset_fields = ['order', 'meal', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderedFoodViewSet(ReadProtectedModelViewSet): | ||||||
|  |     queryset = OrderedFood.objects.order_by('id') | ||||||
|  |     serializer_class = OrderedFoodSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend] | ||||||
|  |     filterset_fields = ['order', 'meal', 'food', 'options', 'number', 'status', 'served_date', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SheetOrderTransactionViewSet(ReadProtectedModelViewSet): | ||||||
|  |     queryset = SheetOrderTransaction.objects.order_by('-created_at') | ||||||
|  |     serializer_class = SheetOrderTransactionSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] | ||||||
|  |     filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name', | ||||||
|  |                         'destination', 'destination_alias', 'destination__alias__name', | ||||||
|  |                         'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount', | ||||||
|  |                         'created_at', 'valid', 'invalidity_reason', 'ordered_food', ] | ||||||
|  |     search_fields = ['$reason', '$source_alias', '$source__alias__name', '$source__alias__normalized_name', | ||||||
|  |                      '$destination_alias', '$destination__alias__name', '$destination__alias__normalized_name', | ||||||
|  |                      '$invalidity_reason', ] | ||||||
|  |     ordering_fields = ['created_at', 'amount', ] | ||||||
							
								
								
									
										10
									
								
								apps/sheets/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								apps/sheets/apps.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | # Copyright (C) 2018-2022 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from django.apps import AppConfig | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SheetsConfig(AppConfig): | ||||||
|  |     name = 'sheets' | ||||||
|  |     verbose_name = _('note sheets') | ||||||
							
								
								
									
										67
									
								
								apps/sheets/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								apps/sheets/forms.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | # Copyright (C) 2018-2022 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | from crispy_forms.helper import FormHelper | ||||||
|  | from django import forms | ||||||
|  |  | ||||||
|  | from member.models import Club | ||||||
|  | from note_kfet.inputs import AmountInput, Autocomplete, DateTimePickerInput | ||||||
|  |  | ||||||
|  | from .models import Food, FoodOption, Meal, Sheet | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SheetForm(forms.ModelForm): | ||||||
|  |     class Meta: | ||||||
|  |         model = Sheet | ||||||
|  |         fields = '__all__' | ||||||
|  |         widgets = { | ||||||
|  |             'date': DateTimePickerInput(), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodForm(forms.ModelForm): | ||||||
|  |     class Meta: | ||||||
|  |         model = Food | ||||||
|  |         exclude = ('sheet', ) | ||||||
|  |         widgets = { | ||||||
|  |             'price': AmountInput(), | ||||||
|  |             'club': Autocomplete( | ||||||
|  |                 model=Club, | ||||||
|  |                 attrs={"api_url": "/api/members/club/"}, | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodOptionForm(forms.ModelForm): | ||||||
|  |     class Meta: | ||||||
|  |         model = FoodOption | ||||||
|  |         fields = '__all__' | ||||||
|  |         widgets = { | ||||||
|  |             'extra_cost': AmountInput(), | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FoodOptionsFormSet = forms.inlineformset_factory( | ||||||
|  |     Food, | ||||||
|  |     FoodOption, | ||||||
|  |     form=FoodOptionForm, | ||||||
|  |     extra=0, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodOptionFormSetHelper(FormHelper): | ||||||
|  |     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 MealForm(forms.ModelForm): | ||||||
|  |     class Meta: | ||||||
|  |         model = Meal | ||||||
|  |         exclude = ('sheet', ) | ||||||
|  |         widgets = { | ||||||
|  |             'content': forms.CheckboxSelectMultiple(), | ||||||
|  |             'price': AmountInput(), | ||||||
|  |         } | ||||||
							
								
								
									
										157
									
								
								apps/sheets/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								apps/sheets/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | |||||||
|  | # Generated by Django 2.2.27 on 2022-08-18 11:01 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  | import django.db.models.deletion | ||||||
|  | import django.utils.timezone | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     initial = True | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('member', '0009_auto_20220818_1301'), | ||||||
|  |         ('note', '0006_trust'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Food', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('name', models.CharField(max_length=255, verbose_name='food')), | ||||||
|  |                 ('price', models.IntegerField(verbose_name='price')), | ||||||
|  |                 ('available', models.BooleanField(default=True, help_text="If set to false, this option won't be offered (in case of out of stock)", verbose_name='available')), | ||||||
|  |                 ('club', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='destination club')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'food', | ||||||
|  |                 'verbose_name_plural': 'food', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='FoodOption', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('name', models.CharField(max_length=255, verbose_name='name')), | ||||||
|  |                 ('extra_cost', models.IntegerField(default=0, verbose_name='extra cost')), | ||||||
|  |                 ('available', models.BooleanField(default=True, help_text="If set to false, this option won't be offered (in case of out of stock)", verbose_name='available')), | ||||||
|  |                 ('food', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sheets.Food', verbose_name='food')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'food option', | ||||||
|  |                 'verbose_name_plural': 'food options', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Meal', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('name', models.CharField(max_length=255, verbose_name='name')), | ||||||
|  |                 ('price', models.IntegerField(verbose_name='price')), | ||||||
|  |                 ('available', models.BooleanField(default=True, help_text="If set to false, this option won't be offered (in case of out of stock)", verbose_name='available')), | ||||||
|  |                 ('content', models.ManyToManyField(to='sheets.Food', verbose_name='content')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'meal', | ||||||
|  |                 'verbose_name_plural': 'meals', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Order', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('date', models.DateTimeField(auto_now_add=True, verbose_name='date')), | ||||||
|  |                 ('gift', models.IntegerField(verbose_name='gift')), | ||||||
|  |                 ('note', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='note.Note', verbose_name='note')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'order', | ||||||
|  |                 'verbose_name_plural': 'orders', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='OrderedFood', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('remark', models.TextField(blank=True, default='', verbose_name='remark')), | ||||||
|  |                 ('priority', models.CharField(blank=True, default='', max_length=64, verbose_name='priority request')), | ||||||
|  |                 ('number', models.IntegerField(help_text='How many times the user ordered this.', verbose_name='number')), | ||||||
|  |                 ('status', models.CharField(choices=[('QUEUED', 'queued'), ('READY', 'ready'), ('SERVED', 'served'), ('CANCELED', 'canceled')], max_length=8, verbose_name='status')), | ||||||
|  |                 ('served_date', models.DateTimeField(default=None, null=True, verbose_name='served date')), | ||||||
|  |                 ('food', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sheets.Food', verbose_name='food')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'ordered food', | ||||||
|  |                 'verbose_name_plural': 'ordered food', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Sheet', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('name', models.CharField(max_length=255, verbose_name='name')), | ||||||
|  |                 ('date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='start date')), | ||||||
|  |                 ('description', models.TextField(verbose_name='description')), | ||||||
|  |                 ('visible', models.BooleanField(default=False, help_text='the note sheet will be private until this field is checked.', verbose_name='visible')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'note sheet', | ||||||
|  |                 'verbose_name_plural': 'note sheets', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='SheetOrderTransaction', | ||||||
|  |             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')), | ||||||
|  |                 ('ordered_food', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sheets.OrderedFood', verbose_name='ordered food')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'sheet order transaction', | ||||||
|  |                 'verbose_name_plural': 'sheet order transactions', | ||||||
|  |             }, | ||||||
|  |             bases=('note.transaction',), | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='OrderedMeal', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('meal', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sheets.Meal', verbose_name='meal')), | ||||||
|  |                 ('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sheets.Order', verbose_name='order')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'ordered meal', | ||||||
|  |                 'verbose_name_plural': 'ordered meals', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='orderedfood', | ||||||
|  |             name='meal', | ||||||
|  |             field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='sheets.OrderedMeal', verbose_name='ordered meal'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='orderedfood', | ||||||
|  |             name='options', | ||||||
|  |             field=models.ManyToManyField(blank=True, to='sheets.FoodOption', verbose_name='options'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='orderedfood', | ||||||
|  |             name='order', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sheets.Order', verbose_name='order'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='order', | ||||||
|  |             name='sheet', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sheets.Sheet', verbose_name='note sheet'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='meal', | ||||||
|  |             name='sheet', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sheets.Sheet', verbose_name='note sheet'), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='food', | ||||||
|  |             name='sheet', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sheets.Sheet', verbose_name='note sheet'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										34
									
								
								apps/sheets/migrations/0002_auto_20220818_1713.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								apps/sheets/migrations/0002_auto_20220818_1713.py
									
									
									
									
									
										Normal 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'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										0
									
								
								apps/sheets/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/sheets/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										289
									
								
								apps/sheets/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								apps/sheets/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,289 @@ | |||||||
|  | # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from django.db import models | ||||||
|  | from django.urls import reverse_lazy | ||||||
|  | from django.utils import timezone | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  | from member.models import Club | ||||||
|  | from note.models import Note, Transaction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Sheet(models.Model): | ||||||
|  |     name = models.CharField( | ||||||
|  |         max_length=255, | ||||||
|  |         verbose_name=_("name"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     date = models.DateTimeField( | ||||||
|  |         verbose_name=_("start date"), | ||||||
|  |         default=timezone.now, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     description = models.TextField( | ||||||
|  |         verbose_name=_("description"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     visible = models.BooleanField( | ||||||
|  |         default=False, | ||||||
|  |         verbose_name=_("visible"), | ||||||
|  |         help_text=_("the note sheet will be private until this field is checked."), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def get_absolute_url(self): | ||||||
|  |         return reverse_lazy('sheets:sheet_detail', args=(self.pk,)) | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.name | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("note sheet") | ||||||
|  |         verbose_name_plural = _("note sheets") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Food(models.Model): | ||||||
|  |     name = models.CharField( | ||||||
|  |         max_length=255, | ||||||
|  |         verbose_name=_("food"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     sheet = models.ForeignKey( | ||||||
|  |         Sheet, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         verbose_name=_("note sheet"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     price = models.IntegerField( | ||||||
|  |         verbose_name=_("price"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     club = models.ForeignKey( | ||||||
|  |         Club, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         verbose_name=_("destination club"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     available = models.BooleanField( | ||||||
|  |         default=True, | ||||||
|  |         verbose_name=_("available"), | ||||||
|  |         help_text=_("If set to false, this option won't be offered (in case of out of stock)"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.name | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("food") | ||||||
|  |         verbose_name_plural = _("food") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodOption(models.Model): | ||||||
|  |     name = models.CharField( | ||||||
|  |         max_length=255, | ||||||
|  |         verbose_name=_("name"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     food = models.ForeignKey( | ||||||
|  |         Food, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         verbose_name=_("food"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     extra_cost = models.IntegerField( | ||||||
|  |         default=0, | ||||||
|  |         verbose_name=_("extra cost"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     available = models.BooleanField( | ||||||
|  |         default=True, | ||||||
|  |         verbose_name=_("available"), | ||||||
|  |         help_text=_("If set to false, this option won't be offered (in case of out of stock)"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.name | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("food option") | ||||||
|  |         verbose_name_plural = _("food options") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Meal(models.Model): | ||||||
|  |     sheet = models.ForeignKey( | ||||||
|  |         Sheet, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         verbose_name=_("note sheet"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     name = models.CharField( | ||||||
|  |         max_length=255, | ||||||
|  |         verbose_name=_("name"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     content = models.ManyToManyField( | ||||||
|  |         Food, | ||||||
|  |         verbose_name=_("content"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     price = models.IntegerField( | ||||||
|  |         verbose_name=_("price"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     available = models.BooleanField( | ||||||
|  |         default=True, | ||||||
|  |         verbose_name=_("available"), | ||||||
|  |         help_text=_("If set to false, this option won't be offered (in case of out of stock)"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return _("meal").capitalize() + " " + self.name | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("meal") | ||||||
|  |         verbose_name_plural = _("meals") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Order(models.Model): | ||||||
|  |     sheet = models.ForeignKey( | ||||||
|  |         Sheet, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         verbose_name=_("note sheet"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     note = models.ForeignKey( | ||||||
|  |         Note, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         verbose_name=_("note"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     date = models.DateTimeField( | ||||||
|  |         verbose_name=_("date"), | ||||||
|  |         auto_now_add=True, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("order") | ||||||
|  |         verbose_name_plural = _("orders") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderedMeal(models.Model): | ||||||
|  |     order = models.ForeignKey( | ||||||
|  |         Order, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         verbose_name=_("order"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     meal = models.ForeignKey( | ||||||
|  |         Meal, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         verbose_name=_("meal"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     gift = models.IntegerField( | ||||||
|  |         verbose_name=_("gift"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("ordered meal") | ||||||
|  |         verbose_name_plural = _("ordered meals") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OrderedFood(models.Model): | ||||||
|  |     order = models.ForeignKey( | ||||||
|  |         Order, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         verbose_name=_("order"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     meal = models.ForeignKey( | ||||||
|  |         OrderedMeal, | ||||||
|  |         on_delete=models.SET_NULL, | ||||||
|  |         null=True, | ||||||
|  |         default=None, | ||||||
|  |         verbose_name=_("ordered meal"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     food = models.ForeignKey( | ||||||
|  |         Food, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         verbose_name=_("food"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     options = models.ManyToManyField( | ||||||
|  |         FoodOption, | ||||||
|  |         blank=True, | ||||||
|  |         verbose_name=_("options"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     remark = models.TextField( | ||||||
|  |         blank=True, | ||||||
|  |         default="", | ||||||
|  |         verbose_name=_("remark"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     priority = models.CharField( | ||||||
|  |         max_length=64, | ||||||
|  |         blank=True, | ||||||
|  |         default="", | ||||||
|  |         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."), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     status = models.CharField( | ||||||
|  |         max_length=8, | ||||||
|  |         choices=[ | ||||||
|  |             ('QUEUED', _("queued")), | ||||||
|  |             ('READY', _("ready")), | ||||||
|  |             ('SERVED', _("served")), | ||||||
|  |             ('CANCELED', _("canceled")), | ||||||
|  |         ], | ||||||
|  |         default='QUEUED', | ||||||
|  |         verbose_name=_("status"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     served_date = models.DateTimeField( | ||||||
|  |         null=True, | ||||||
|  |         default=None, | ||||||
|  |         verbose_name=_("served date") | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("ordered food") | ||||||
|  |         verbose_name_plural = _("ordered food") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SheetOrderTransaction(Transaction): | ||||||
|  |     ordered_food = models.ForeignKey( | ||||||
|  |         OrderedFood, | ||||||
|  |         on_delete=models.PROTECT, | ||||||
|  |         verbose_name=_("ordered food"), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def type(self): | ||||||
|  |         return _("note sheet") | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def get_price(self): | ||||||
|  |         if self.ordered_food.meal: | ||||||
|  |             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 + self.ordered_food.gift \ | ||||||
|  |                    + sum(opt.extra_cost for opt in self.ordered_food.options.all()) | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("sheet order transaction") | ||||||
|  |         verbose_name_plural = _("sheet order transactions") | ||||||
							
								
								
									
										22
									
								
								apps/sheets/tables.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								apps/sheets/tables.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | # Copyright (C) 2018-2022 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | import django_tables2 as tables | ||||||
|  | from django.urls import reverse_lazy | ||||||
|  |  | ||||||
|  | from sheets.models import Sheet | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SheetTable(tables.Table): | ||||||
|  |     class Meta: | ||||||
|  |         attrs = { | ||||||
|  |             'class': 'table table-condensed table-striped table-hover' | ||||||
|  |         } | ||||||
|  |         model = Sheet | ||||||
|  |         template_name = 'django_tables2/bootstrap4.html' | ||||||
|  |         fields = ('name', 'date', ) | ||||||
|  |         row_attrs = { | ||||||
|  |             'class': 'table-row', | ||||||
|  |             'id': lambda record: "row-" + str(record.pk), | ||||||
|  |             'data-href': lambda record: reverse_lazy('sheets:sheet_detail', args=(record.pk,)) | ||||||
|  |         } | ||||||
							
								
								
									
										86
									
								
								apps/sheets/templates/sheets/food_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								apps/sheets/templates/sheets/food_form.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | {% 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"> | ||||||
|  |         <form method="post"> | ||||||
|  |             {% csrf_token %} | ||||||
|  |             {{ form|crispy }} | ||||||
|  |  | ||||||
|  |             {# The next part concerns the option formset #} | ||||||
|  |             {# Generate some hidden fields that manage the number of options, and make easier the parsing #} | ||||||
|  |             {{ formset.management_form }} | ||||||
|  |             <table class="table table-condensed table-striped"> | ||||||
|  |                 {# Fill initial data #} | ||||||
|  |                 {% for form in formset %} | ||||||
|  |                 {% if forloop.first %} | ||||||
|  |                 <thead> | ||||||
|  |                     <tr> | ||||||
|  |                         <th>{{ form.name.label }}<span class="asteriskField">*</span></th> | ||||||
|  |                         <th>{{ form.extra_cost.label }}<span class="asteriskField">*</span></th> | ||||||
|  |                         <th>{{ form.available.label }}<span class="asteriskField">*</span></th> | ||||||
|  |                     </tr> | ||||||
|  |                 </thead> | ||||||
|  |                 <tbody id="form_body"> | ||||||
|  |                     {% endif %} | ||||||
|  |                     <tr class="row-formset"> | ||||||
|  |                         <td>{{ form.name }}</td> | ||||||
|  |                         <td>{{ form.extra_cost }}</td> | ||||||
|  |                         <td>{{ form.available }}</td> | ||||||
|  |                         {# These fields are hidden but handled by the formset to link the id and the invoice id #} | ||||||
|  |                         {{ form.food }} | ||||||
|  |                         {{ form.id }} | ||||||
|  |                     </tr> | ||||||
|  |                     {% endfor %} | ||||||
|  |                 </tbody> | ||||||
|  |             </table> | ||||||
|  |  | ||||||
|  |             {# Display buttons to add and remove options #} | ||||||
|  |             <div class="card-body"> | ||||||
|  |             <button type="button" id="add_more" class="btn btn-success">{% trans "Add option" %}</button> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> | ||||||
|  |         </form> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {# Hidden div that store an empty product 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.name }}</td> | ||||||
|  |                 <td>{{ formset.empty_form.extra_cost }} </td> | ||||||
|  |                 <td>{{ formset.empty_form.available }}</td> | ||||||
|  |                 {{ formset.empty_form.food }} | ||||||
|  |                 {{ formset.empty_form.id }} | ||||||
|  |             </tr> | ||||||
|  |         </tbody> | ||||||
|  |     </table> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block extrajavascript %} | ||||||
|  | <script> | ||||||
|  |     /* script that handles add and remove lines */ | ||||||
|  |     IDS = {}; | ||||||
|  |  | ||||||
|  |     $("#id_foodoption_set-TOTAL_FORMS").val($(".row-formset").length - 1); | ||||||
|  |  | ||||||
|  |     $('#add_more').click(function () { | ||||||
|  |         let form_idx = $('#id_foodoption_set-TOTAL_FORMS').val(); | ||||||
|  |         $('#form_body').append($('#for_real').html().replace(/__prefix__/g, form_idx)); | ||||||
|  |         $('#id_foodoption_set-TOTAL_FORMS').val(parseInt(form_idx) + 1); | ||||||
|  |         $('#id_foodoption_set-' + parseInt(form_idx) + '-id').val(IDS[parseInt(form_idx)]); | ||||||
|  |     }); | ||||||
|  | </script> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										21
									
								
								apps/sheets/templates/sheets/meal_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/sheets/templates/sheets/meal_form.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | {% 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"> | ||||||
|  |         <form method="post"> | ||||||
|  |             {% csrf_token %} | ||||||
|  |             {{ form|crispy }} | ||||||
|  |             <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> | ||||||
|  |         </form> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										87
									
								
								apps/sheets/templates/sheets/sheet_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								apps/sheets/templates/sheets/sheet_detail.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  |  | ||||||
|  | {% load i18n %} | ||||||
|  | {% load pretty_money %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="card"> | ||||||
|  | <div class="card-header text-center"> | ||||||
|  |     <h1>{{ sheet.name }}</h1> | ||||||
|  | </div> | ||||||
|  | <div class="card-body"> | ||||||
|  |     <div class="alert alert-secondary"> | ||||||
|  |         <div class="row"> | ||||||
|  |             <div class="col-sm-11"> | ||||||
|  |                 {{ sheet.description }} | ||||||
|  |             </div> | ||||||
|  |             {% if can_change_sheet %} | ||||||
|  |                 <div class="col-sm-1"> | ||||||
|  |                     <a class="badge badge-primary" href="{% url 'sheets:sheet_update' pk=sheet.pk %}"> | ||||||
|  |                         <i class="fa fa-edit"></i> | ||||||
|  |                         {% trans "Edit" %} | ||||||
|  |                     </a> | ||||||
|  |                 </div> | ||||||
|  |             {% endif %} | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <h3>{% trans "menu"|capfirst %} :</h3> | ||||||
|  |     <ul> | ||||||
|  |         {% for meal in sheet.meal_set.all %} | ||||||
|  |             <li{% if not meal.available %} class="text-danger" style="text-decoration: line-through !important;" title="{% trans "This product is unavailable." %}"{% endif %}> | ||||||
|  |                 {{ meal }} ({{ meal.price|pretty_money }}) | ||||||
|  |                 {% if can_change_sheet %} | ||||||
|  |                     <a href="{% url 'sheets:meal_update' pk=meal.pk %}" class="badge badge-primary"> | ||||||
|  |                         <i class="fa fa-edit"></i> | ||||||
|  |                         {% trans "Edit" %} | ||||||
|  |                     </a> | ||||||
|  |                 {% endif %} | ||||||
|  |             </li> | ||||||
|  |         {% endfor %} | ||||||
|  |         <hr> | ||||||
|  |         {% for food in sheet.food_set.all %} | ||||||
|  |             <li{% if not food.available %} class="text-danger" style="text-decoration: line-through !important;" title="{% trans "This product is unavailable." %}"{% endif %}> | ||||||
|  |                 {{ food }} ({{ food.price|pretty_money }}) | ||||||
|  |                 <a href="{% url 'sheets:waiting_list' pk=food.pk %}" class="badge badge-primary"> | ||||||
|  |                     <i class="fa fa-list"></i> | ||||||
|  |                     {% trans "Waiting list" %} | ||||||
|  |                 </a> | ||||||
|  |                 {% if can_change_sheet %} | ||||||
|  |                     <a href="{% url 'sheets:food_update' pk=food.pk %}" class="badge badge-primary"> | ||||||
|  |                         <i class="fa fa-edit"></i> | ||||||
|  |                         {% trans "Edit" %} | ||||||
|  |                     </a> | ||||||
|  |                 {% endif %} | ||||||
|  |                 {% if food.foodoption_set.all %} | ||||||
|  |                     <ul> | ||||||
|  |                         {% for option in food.foodoption_set.all %} | ||||||
|  |                             <li{% if not option.available %} class="text-danger" style="text-decoration: line-through !important;" title="{% trans "This product is unavailable." %}"{% endif %}> | ||||||
|  |                                 {{ option }}{% if option.extra_cost %} ({{ option.extra_cost|pretty_money }}){% endif %} | ||||||
|  |                             </li> | ||||||
|  |                         {% endfor %} | ||||||
|  |                     </ul> | ||||||
|  |                 {% endif %} | ||||||
|  |             </li> | ||||||
|  |         {% empty %} | ||||||
|  |             <div class="alert alert-warning"> | ||||||
|  |                 {% trans "The menu is empty for now." %} | ||||||
|  |             </div> | ||||||
|  |         {% endfor %} | ||||||
|  |     </ul> | ||||||
|  |  | ||||||
|  |     <div class="text-center"> | ||||||
|  |         {% if can_add_food %} | ||||||
|  |             <a href="{% url 'sheets:food_create' pk=sheet.pk %}" class="btn btn-primary">{% trans "Add new food" %}</a> | ||||||
|  |         {% endif %} | ||||||
|  |         {% if can_add_meal %} | ||||||
|  |             <a href="{% url 'sheets:meal_create' pk=sheet.pk %}" class="btn btn-primary">{% trans "Add new meal" %}</a> | ||||||
|  |         {% endif %} | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | <div class="card-footer text-center"> | ||||||
|  |     <a href="{% url 'sheets:sheet_order' pk=sheet.pk %}" class="btn btn-success"> | ||||||
|  |         {% trans "Order now" %} | ||||||
|  |     </a> | ||||||
|  | </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										21
									
								
								apps/sheets/templates/sheets/sheet_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/sheets/templates/sheets/sheet_form.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | {% 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"> | ||||||
|  |         <form method="post"> | ||||||
|  |             {% csrf_token %} | ||||||
|  |             {{ form|crispy }} | ||||||
|  |             <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> | ||||||
|  |         </form> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										74
									
								
								apps/sheets/templates/sheets/sheet_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								apps/sheets/templates/sheets/sheet_list.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load render_table from django_tables2 %} | ||||||
|  | {% load i18n %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <div class="row justify-content-center mb-4"> | ||||||
|  |     <div class="col-md-10 text-center"> | ||||||
|  |         <input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved()" id="search_field"/> | ||||||
|  |         {% if can_create_sheet %} | ||||||
|  |             <hr> | ||||||
|  |             <a class="btn btn-primary text-center my-4" href="{% url 'sheets:sheet_create' %}">{% trans "Create a sheet" %}</a> | ||||||
|  |         {% endif %} | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | <div class="row justify-content-center">    | ||||||
|  |     <div class="col-md-10"> | ||||||
|  |         <div class="card card-border shadow"> | ||||||
|  |             <div class="card-header text-center"> | ||||||
|  |                 <h5> {% trans "Note sheet listing" %}</h5> | ||||||
|  |             </div> | ||||||
|  |             <div class="card-body px-0 py-0" id="sheets_table"> | ||||||
|  |                 {% render_table table %} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {% endblock %} | ||||||
|  | {% block extrajavascript %} | ||||||
|  | <script type="text/javascript"> | ||||||
|  |  | ||||||
|  | function getInfo() { | ||||||
|  |     var asked = $("#search_field").val(); | ||||||
|  |     /* on ne fait la requête que si on a au moins un caractère pour chercher */ | ||||||
|  |     var sel = $(".table-row"); | ||||||
|  |     if (asked.length >= 1) { | ||||||
|  |         $.getJSON("/api/sheets/sheet/?format=json&search="+asked, function(buttons){ | ||||||
|  |             let selected_id = buttons.results.map((a => "#row-"+a.id)); | ||||||
|  |             if (selected_id.length) | ||||||
|  |                 $(".table-row,"+selected_id.join()).show(); | ||||||
|  |             $(".table-row").not(selected_id.join()).hide(); | ||||||
|  |              | ||||||
|  |         }); | ||||||
|  |     }else{ | ||||||
|  |         // show everything | ||||||
|  |         $('table tr').show(); | ||||||
|  |     }        | ||||||
|  | } | ||||||
|  | var timer; | ||||||
|  | var timer_on; | ||||||
|  | /* Fontion appelée quand le texte change (délenche le timer) */ | ||||||
|  | function search_field_moved(secondfield) { | ||||||
|  |     if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur. | ||||||
|  |         clearTimeout(timer); | ||||||
|  |         timer = setTimeout("getInfo(" + secondfield + ")", 300); | ||||||
|  |     } | ||||||
|  |     else { // Sinon, on le lance et on enregistre le fait qu'il tourne. | ||||||
|  |         timer = setTimeout("getInfo(" + secondfield + ")", 300); | ||||||
|  |         timer_on = true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // clickable row  | ||||||
|  | $(document).ready(function($) { | ||||||
|  |     $(".table-row").click(function() { | ||||||
|  |         window.document.location = $(this).data("href"); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | </script> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										0
									
								
								apps/sheets/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/sheets/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										26
									
								
								apps/sheets/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								apps/sheets/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | # Copyright (C) 2018-2022 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from django.urls import path | ||||||
|  |  | ||||||
|  | from sheets.views import FoodCreateView, FoodUpdateView, MealCreateView, MealUpdateView, OrderView, \ | ||||||
|  |     SheetCreateView, SheetDetailView, SheetListView, SheetUpdateView, WaitingListDetailView, WaitingListView | ||||||
|  |  | ||||||
|  | app_name = 'sheets' | ||||||
|  |  | ||||||
|  | urlpatterns = [ | ||||||
|  |     path('list/', SheetListView.as_view(), name="sheet_list"), | ||||||
|  |     path('create/', SheetCreateView.as_view(), name="sheet_create"), | ||||||
|  |     path('update/<int:pk>/', SheetUpdateView.as_view(), name="sheet_update"), | ||||||
|  |     path('detail/<int:pk>/', SheetDetailView.as_view(), name="sheet_detail"), | ||||||
|  |     path('food/create/<int:pk>/', FoodCreateView.as_view(), name="food_create"), | ||||||
|  |     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"), | ||||||
|  |     path('waiting-list/<int:pk>/', WaitingListView.as_view(), name="waiting_list"), | ||||||
|  |     path('waiting-list/<int:pk>/queued/', WaitingListDetailView.as_view(), name="queued_list"), | ||||||
|  |     path('waiting-list/<int:pk>/ready/', WaitingListDetailView.as_view(), name="ready_list"), | ||||||
|  |     path('waiting-list/<int:pk>/served/', WaitingListDetailView.as_view(), name="served_list"), | ||||||
|  |     path('waiting-list/<int:pk>/canceled/', WaitingListDetailView.as_view(), name="canceled_list"), | ||||||
|  | ] | ||||||
							
								
								
									
										444
									
								
								apps/sheets/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										444
									
								
								apps/sheets/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,444 @@ | |||||||
|  | # 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 | ||||||
| @@ -7,7 +7,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: \n" | "Project-Id-Version: \n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2022-04-10 22:34+0200\n" | "POT-Creation-Date: 2022-08-18 22:51+0200\n" | ||||||
| "PO-Revision-Date: 2022-04-11 22:05+0200\n" | "PO-Revision-Date: 2022-04-11 22:05+0200\n" | ||||||
| "Last-Translator: elkmaennchen <elkmaennchen@crans.org>\n" | "Last-Translator: elkmaennchen <elkmaennchen@crans.org>\n" | ||||||
| "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" | "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" | ||||||
| @@ -60,6 +60,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." | |||||||
| #: apps/note/models/transactions.py:46 apps/note/models/transactions.py:301 | #: apps/note/models/transactions.py:46 apps/note/models/transactions.py:301 | ||||||
| #: apps/permission/models.py:330 | #: apps/permission/models.py:330 | ||||||
| #: apps/registration/templates/registration/future_profile_detail.html:16 | #: apps/registration/templates/registration/future_profile_detail.html:16 | ||||||
|  | #: apps/sheets/models.py:16 apps/sheets/models.py:84 apps/sheets/models.py:121 | ||||||
| #: apps/wei/models.py:67 apps/wei/models.py:131 apps/wei/tables.py:282 | #: apps/wei/models.py:67 apps/wei/models.py:131 apps/wei/tables.py:282 | ||||||
| #: apps/wei/templates/wei/base.html:26 | #: apps/wei/templates/wei/base.html:26 | ||||||
| #: apps/wei/templates/wei/weimembership_form.html:14 | #: apps/wei/templates/wei/weimembership_form.html:14 | ||||||
| @@ -95,7 +96,8 @@ msgstr "types d'activité" | |||||||
| #: apps/activity/models.py:68 | #: apps/activity/models.py:68 | ||||||
| #: apps/activity/templates/activity/includes/activity_info.html:19 | #: apps/activity/templates/activity/includes/activity_info.html:19 | ||||||
| #: apps/note/models/transactions.py:81 apps/permission/models.py:110 | #: apps/note/models/transactions.py:81 apps/permission/models.py:110 | ||||||
| #: apps/permission/models.py:189 apps/wei/models.py:78 apps/wei/models.py:142 | #: apps/permission/models.py:189 apps/sheets/models.py:25 apps/wei/models.py:78 | ||||||
|  | #: apps/wei/models.py:142 | ||||||
| msgid "description" | msgid "description" | ||||||
| msgstr "description" | msgstr "description" | ||||||
|  |  | ||||||
| @@ -143,6 +145,7 @@ msgstr "" | |||||||
|  |  | ||||||
| #: apps/activity/models.py:109 | #: apps/activity/models.py:109 | ||||||
| #: apps/activity/templates/activity/includes/activity_info.html:25 | #: apps/activity/templates/activity/includes/activity_info.html:25 | ||||||
|  | #: apps/sheets/models.py:20 | ||||||
| msgid "start date" | msgid "start date" | ||||||
| msgstr "date de début" | msgstr "date de début" | ||||||
|  |  | ||||||
| @@ -171,7 +174,7 @@ msgid "entry time" | |||||||
| msgstr "heure d'entrée" | msgstr "heure d'entrée" | ||||||
|  |  | ||||||
| #: apps/activity/models.py:178 apps/note/apps.py:14 | #: apps/activity/models.py:178 apps/note/apps.py:14 | ||||||
| #: apps/note/models/notes.py:77 | #: apps/note/models/notes.py:77 apps/sheets/models.py:157 | ||||||
| msgid "note" | msgid "note" | ||||||
| msgstr "note" | msgstr "note" | ||||||
|  |  | ||||||
| @@ -334,7 +337,10 @@ msgstr "Entrée effectuée !" | |||||||
| #: apps/member/templates/member/add_members.html:46 | #: apps/member/templates/member/add_members.html:46 | ||||||
| #: apps/member/templates/member/club_form.html:16 | #: apps/member/templates/member/club_form.html:16 | ||||||
| #: apps/note/templates/note/transactiontemplate_form.html:18 | #: apps/note/templates/note/transactiontemplate_form.html:18 | ||||||
| #: apps/treasury/forms.py:89 apps/treasury/forms.py:143 | #: apps/sheets/templates/sheets/food_form.html:51 | ||||||
|  | #: apps/sheets/templates/sheets/meal_form.html:17 | ||||||
|  | #: apps/sheets/templates/sheets/sheet_form.html:17 apps/treasury/forms.py:89 | ||||||
|  | #: apps/treasury/forms.py:143 | ||||||
| #: apps/treasury/templates/treasury/invoice_form.html:74 | #: apps/treasury/templates/treasury/invoice_form.html:74 | ||||||
| #: apps/wei/templates/wei/bus_form.html:17 | #: apps/wei/templates/wei/bus_form.html:17 | ||||||
| #: apps/wei/templates/wei/busteam_form.html:17 | #: apps/wei/templates/wei/busteam_form.html:17 | ||||||
| @@ -1470,7 +1476,7 @@ msgstr "modèles de transaction" | |||||||
| msgid "used alias" | msgid "used alias" | ||||||
| msgstr "alias utilisé" | msgstr "alias utilisé" | ||||||
|  |  | ||||||
| #: apps/note/models/transactions.py:136 | #: apps/note/models/transactions.py:136 apps/sheets/views.py:277 | ||||||
| msgid "quantity" | msgid "quantity" | ||||||
| msgstr "quantité" | msgstr "quantité" | ||||||
|  |  | ||||||
| @@ -1592,8 +1598,10 @@ msgid "Delete" | |||||||
| msgstr "Supprimer" | msgstr "Supprimer" | ||||||
|  |  | ||||||
| #: apps/note/tables.py:222 apps/note/templates/note/conso_form.html:132 | #: apps/note/tables.py:222 apps/note/templates/note/conso_form.html:132 | ||||||
| #: apps/wei/tables.py:49 apps/wei/tables.py:50 | #: apps/sheets/templates/sheets/sheet_detail.html:21 | ||||||
| #: apps/wei/templates/wei/base.html:89 | #: apps/sheets/templates/sheets/sheet_detail.html:36 | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:52 apps/wei/tables.py:49 | ||||||
|  | #: apps/wei/tables.py:50 apps/wei/templates/wei/base.html:89 | ||||||
| #: apps/wei/templates/wei/bus_detail.html:20 | #: apps/wei/templates/wei/bus_detail.html:20 | ||||||
| #: apps/wei/templates/wei/busteam_detail.html:20 | #: apps/wei/templates/wei/busteam_detail.html:20 | ||||||
| #: apps/wei/templates/wei/busteam_detail.html:40 | #: apps/wei/templates/wei/busteam_detail.html:40 | ||||||
| @@ -1746,6 +1754,98 @@ msgstr "Bouton affiché" | |||||||
| msgid "An error occured" | msgid "An error occured" | ||||||
| msgstr "Une erreur s'est produite" | msgstr "Une erreur s'est produite" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list.html:14 apps/sheets/models.py:244 | ||||||
|  | msgid "queued" | ||||||
|  | msgstr "en attente" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list.html:27 | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:67 | ||||||
|  | msgid "There is no queued order." | ||||||
|  | msgstr "Il n'y a pas de commande en attente." | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list.html:35 apps/sheets/models.py:245 | ||||||
|  | msgid "ready" | ||||||
|  | msgstr "prêt" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list.html:43 | ||||||
|  | msgid "There is no ready order." | ||||||
|  | msgstr "Il n'y a pas de commande prête." | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list.html:53 | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:75 | ||||||
|  | msgid "Other waiting lists:" | ||||||
|  | msgstr "Autres listes d'attente :" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list.html:66 | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:93 | ||||||
|  | msgid "Queued orders" | ||||||
|  | msgstr "Commandes en attente" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list.html:69 | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:98 | ||||||
|  | msgid "Ready orders" | ||||||
|  | msgstr "Commandes prêtes" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list.html:72 | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:115 | ||||||
|  | msgid "Back to note sheet detail" | ||||||
|  | msgstr "Retour aux détails de la feuille de note" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:18 | ||||||
|  | #: apps/sheets/models.py:161 | ||||||
|  | msgid "date" | ||||||
|  | msgstr "date" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:22 | ||||||
|  | msgid "order number" | ||||||
|  | msgstr "nombre de commandes" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:27 | ||||||
|  | #: apps/sheets/models.py:229 apps/sheets/views.py:249 apps/sheets/views.py:295 | ||||||
|  | msgid "priority request" | ||||||
|  | msgstr "demande de priorité" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:32 | ||||||
|  | #: apps/sheets/models.py:222 apps/sheets/views.py:243 apps/sheets/views.py:289 | ||||||
|  | msgid "remark" | ||||||
|  | msgstr "remarques" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:37 | ||||||
|  | #: apps/sheets/models.py:216 | ||||||
|  | msgid "options" | ||||||
|  | msgstr "options" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:45 | ||||||
|  | msgid "Mark as ready" | ||||||
|  | msgstr "Marquer comme prêt" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:50 | ||||||
|  | msgid "Mark as served" | ||||||
|  | msgstr "Marquer comme servi" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:55 | ||||||
|  | msgid "Re-queue" | ||||||
|  | msgstr "Remettre en attente" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:60 | ||||||
|  | #: note_kfet/templates/oauth2_provider/application_confirm_delete.html:17 | ||||||
|  | #: note_kfet/templates/oauth2_provider/authorize.html:28 | ||||||
|  | msgid "Cancel" | ||||||
|  | msgstr "Annuler" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:103 | ||||||
|  | msgid "Served orders" | ||||||
|  | msgstr "Commandes servies" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:108 | ||||||
|  | msgid "Canceled orders" | ||||||
|  | msgstr "Commandes annulées" | ||||||
|  |  | ||||||
|  | #: apps/note/templates/sheets/waiting_list_detail.html:112 | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:47 apps/sheets/views.py:416 | ||||||
|  | msgid "Waiting list" | ||||||
|  | msgstr "Liste d'attente" | ||||||
|  |  | ||||||
| #: apps/note/views.py:36 | #: apps/note/views.py:36 | ||||||
| msgid "Transfer money" | msgid "Transfer money" | ||||||
| msgstr "Transférer de l'argent" | msgstr "Transférer de l'argent" | ||||||
| @@ -2167,6 +2267,237 @@ msgstr "" | |||||||
| msgid "Invalidate pre-registration" | msgid "Invalidate pre-registration" | ||||||
| msgstr "Invalider l'inscription" | msgstr "Invalider l'inscription" | ||||||
|  |  | ||||||
|  | #: apps/sheets/apps.py:10 apps/sheets/models.py:42 | ||||||
|  | msgid "note sheets" | ||||||
|  | msgstr "feuilles de notes" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:30 | ||||||
|  | msgid "visible" | ||||||
|  | msgstr "visible" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:31 | ||||||
|  | msgid "the note sheet will be private until this field is checked." | ||||||
|  | msgstr "la feuille de note restera privée tant que ce champ n'est pas coché." | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:41 apps/sheets/models.py:54 apps/sheets/models.py:116 | ||||||
|  | #: apps/sheets/models.py:151 apps/sheets/models.py:273 | ||||||
|  | msgid "note sheet" | ||||||
|  | msgstr "feuille de note" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:48 apps/sheets/models.py:77 apps/sheets/models.py:78 | ||||||
|  | #: apps/sheets/models.py:90 apps/sheets/models.py:210 | ||||||
|  | msgid "food" | ||||||
|  | msgstr "nourriture" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:58 apps/sheets/models.py:130 | ||||||
|  | msgid "price" | ||||||
|  | msgstr "prix" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:64 | ||||||
|  | msgid "destination club" | ||||||
|  | msgstr "club de destination" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:69 apps/sheets/models.py:100 apps/sheets/models.py:135 | ||||||
|  | msgid "available" | ||||||
|  | msgstr "disponible" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:70 apps/sheets/models.py:101 apps/sheets/models.py:136 | ||||||
|  | msgid "If set to false, this option won't be offered (in case of out of stock)" | ||||||
|  | msgstr "" | ||||||
|  | "Si mis à faux, cette option ne sera pas présentée (en cas de rupture de " | ||||||
|  | "stock)" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:95 | ||||||
|  | msgid "extra cost" | ||||||
|  | msgstr "surcoût" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:108 | ||||||
|  | msgid "food option" | ||||||
|  | msgstr "option de nourriture" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:109 | ||||||
|  | msgid "food options" | ||||||
|  | msgstr "options de nourriture" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:126 | ||||||
|  | msgid "content" | ||||||
|  | msgstr "contenu" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:140 apps/sheets/models.py:143 | ||||||
|  | #: apps/sheets/models.py:180 | ||||||
|  | msgid "meal" | ||||||
|  | msgstr "menu" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:144 | ||||||
|  | msgid "meals" | ||||||
|  | msgstr "menus" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:166 apps/sheets/models.py:174 | ||||||
|  | #: apps/sheets/models.py:196 | ||||||
|  | msgid "order" | ||||||
|  | msgstr "commande" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:167 | ||||||
|  | msgid "orders" | ||||||
|  | msgstr "commandes" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:184 apps/sheets/models.py:233 apps/sheets/views.py:235 | ||||||
|  | #: apps/sheets/views.py:281 | ||||||
|  | msgid "gift" | ||||||
|  | msgstr "don" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:188 apps/sheets/models.py:204 | ||||||
|  | msgid "ordered meal" | ||||||
|  | msgstr "menu commandé" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:189 | ||||||
|  | msgid "ordered meals" | ||||||
|  | msgstr "menus commandés" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:237 | ||||||
|  | msgid "number" | ||||||
|  | msgstr "numéro" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:238 | ||||||
|  | msgid "How many times the user ordered this." | ||||||
|  | msgstr "Combien de fois cet⋅te utilisateur⋅rice a commandé ceci." | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:246 | ||||||
|  | msgid "served" | ||||||
|  | msgstr "servi" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:247 | ||||||
|  | msgid "canceled" | ||||||
|  | msgstr "annulé" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:250 | ||||||
|  | msgid "status" | ||||||
|  | msgstr "statut" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:256 | ||||||
|  | msgid "served date" | ||||||
|  | msgstr "date de service" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:260 apps/sheets/models.py:261 | ||||||
|  | #: apps/sheets/models.py:268 | ||||||
|  | msgid "ordered food" | ||||||
|  | msgstr "nourriture commandée" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:288 | ||||||
|  | msgid "sheet order transaction" | ||||||
|  | msgstr "transaction de commande sur feuille de note" | ||||||
|  |  | ||||||
|  | #: apps/sheets/models.py:289 | ||||||
|  | msgid "sheet order transactions" | ||||||
|  | msgstr "transactions de commande sur feuille de note" | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/food_form.html:48 | ||||||
|  | msgid "Add option" | ||||||
|  | msgstr "Ajouter une option" | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:28 | ||||||
|  | msgid "menu" | ||||||
|  | msgstr "menu" | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:31 | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:43 | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:58 | ||||||
|  | msgid "This product is unavailable." | ||||||
|  | msgstr "Ce produit est indisponible." | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:67 | ||||||
|  | msgid "The menu is empty for now." | ||||||
|  | msgstr "Le menu est vide pour le moment." | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:74 | ||||||
|  | msgid "Add new food" | ||||||
|  | msgstr "Ajouter un plat" | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:77 | ||||||
|  | msgid "Add new meal" | ||||||
|  | msgstr "Ajouter un menu" | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/sheet_detail.html:83 apps/sheets/views.py:206 | ||||||
|  | #: apps/sheets/views.py:319 | ||||||
|  | msgid "Order now" | ||||||
|  | msgstr "Commander maintenant" | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/sheet_list.html:14 | ||||||
|  | msgid "Create a sheet" | ||||||
|  | msgstr "Créer une feuille de note" | ||||||
|  |  | ||||||
|  | #: apps/sheets/templates/sheets/sheet_list.html:22 | ||||||
|  | msgid "Note sheet listing" | ||||||
|  | msgstr "Liste des feuilles de notes" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:33 | ||||||
|  | msgid "Search note sheet" | ||||||
|  | msgstr "Chercher une feuille de note" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:48 | ||||||
|  | msgid "Create note sheet" | ||||||
|  | msgstr "Créer une feuille de note" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:61 | ||||||
|  | msgid "Update note sheet" | ||||||
|  | msgstr "Modifier une feuille de note" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:84 | ||||||
|  | msgid "Create new food" | ||||||
|  | msgstr "Créer un plat" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:130 | ||||||
|  | msgid "Update food" | ||||||
|  | msgstr "Modifier un plat" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:167 | ||||||
|  | msgid "Create new meal" | ||||||
|  | msgstr "Créer un menu" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:192 | ||||||
|  | msgid "Update meal" | ||||||
|  | msgstr "Modifier un menu" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:217 | ||||||
|  | msgid "Orderer" | ||||||
|  | msgstr "Commanditaire" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:223 | ||||||
|  | msgid "Who orders" | ||||||
|  | msgstr "Qui commande" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:231 apps/treasury/models.py:140 | ||||||
|  | msgid "Quantity" | ||||||
|  | msgstr "Quantité" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:238 apps/sheets/views.py:284 | ||||||
|  | msgid "Be careful: this gift will be multiplied for each order." | ||||||
|  | msgstr "Attention : ce don sera multiplié pour chaque commande." | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:244 apps/sheets/views.py:290 | ||||||
|  | msgid "Allergies,…" | ||||||
|  | msgstr "Allergies,…" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:250 apps/sheets/views.py:296 | ||||||
|  | msgid "Lesson at 13h30,…" | ||||||
|  | msgstr "Cours à 13h30,…" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:261 | ||||||
|  | msgid "Options for " | ||||||
|  | msgstr "Options pour " | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:306 | ||||||
|  | msgid "Options" | ||||||
|  | msgstr "Options" | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:403 | ||||||
|  | msgid "You didn't select anything." | ||||||
|  | msgstr "Vous n'avez rien sélectionné." | ||||||
|  |  | ||||||
|  | #: apps/sheets/views.py:432 | ||||||
|  | msgid "Detailed waiting list" | ||||||
|  | msgstr "Liste d'attente détaillée" | ||||||
|  |  | ||||||
| #: apps/treasury/apps.py:12 note_kfet/templates/base.html:96 | #: apps/treasury/apps.py:12 note_kfet/templates/base.html:96 | ||||||
| msgid "Treasury" | msgid "Treasury" | ||||||
| msgstr "Trésorerie" | msgstr "Trésorerie" | ||||||
| @@ -2253,10 +2584,6 @@ msgstr "Facture n°{id}" | |||||||
| msgid "Designation" | msgid "Designation" | ||||||
| msgstr "Désignation" | msgstr "Désignation" | ||||||
|  |  | ||||||
| #: apps/treasury/models.py:140 |  | ||||||
| msgid "Quantity" |  | ||||||
| msgstr "Quantité" |  | ||||||
|  |  | ||||||
| #: apps/treasury/models.py:145 | #: apps/treasury/models.py:145 | ||||||
| msgid "Unit price" | msgid "Unit price" | ||||||
| msgstr "Prix unitaire" | msgstr "Prix unitaire" | ||||||
| @@ -3185,19 +3512,19 @@ msgstr "Répartir les 1A dans les bus" | |||||||
| msgid "Attribute bus" | msgid "Attribute bus" | ||||||
| msgstr "Attribuer un bus" | msgstr "Attribuer un bus" | ||||||
|  |  | ||||||
| #: note_kfet/settings/base.py:172 | #: note_kfet/settings/base.py:173 | ||||||
| msgid "German" | msgid "German" | ||||||
| msgstr "Allemand" | msgstr "Allemand" | ||||||
|  |  | ||||||
| #: note_kfet/settings/base.py:173 | #: note_kfet/settings/base.py:174 | ||||||
| msgid "English" | msgid "English" | ||||||
| msgstr "Anglais" | msgstr "Anglais" | ||||||
|  |  | ||||||
| #: note_kfet/settings/base.py:174 | #: note_kfet/settings/base.py:175 | ||||||
| msgid "Spanish" | msgid "Spanish" | ||||||
| msgstr "Espagnol" | msgstr "Espagnol" | ||||||
|  |  | ||||||
| #: note_kfet/settings/base.py:175 | #: note_kfet/settings/base.py:176 | ||||||
| msgid "French" | msgid "French" | ||||||
| msgstr "Français" | msgstr "Français" | ||||||
|  |  | ||||||
| @@ -3347,11 +3674,6 @@ msgstr "Il n'y a pas de résultat." | |||||||
| msgid "Are you sure to delete the application" | msgid "Are you sure to delete the application" | ||||||
| msgstr "Êtes-vous sûr⋅e de vouloir supprimer l'application" | msgstr "Êtes-vous sûr⋅e de vouloir supprimer l'application" | ||||||
|  |  | ||||||
| #: note_kfet/templates/oauth2_provider/application_confirm_delete.html:17 |  | ||||||
| #: note_kfet/templates/oauth2_provider/authorize.html:28 |  | ||||||
| msgid "Cancel" |  | ||||||
| msgstr "Annuler" |  | ||||||
|  |  | ||||||
| #: note_kfet/templates/oauth2_provider/application_detail.html:11 | #: note_kfet/templates/oauth2_provider/application_detail.html:11 | ||||||
| msgid "Client id" | msgid "Client id" | ||||||
| msgstr "ID client" | msgstr "ID client" | ||||||
| @@ -3562,3 +3884,6 @@ msgstr "" | |||||||
| "vous connecter. Vous devez vous rendre à la Kfet et payer les frais " | "vous connecter. Vous devez vous rendre à la Kfet et payer les frais " | ||||||
| "d'adhésion. Vous devez également valider votre adresse email en suivant le " | "d'adhésion. Vous devez également valider votre adresse email en suivant le " | ||||||
| "lien que vous avez reçu." | "lien que vous avez reçu." | ||||||
|  |  | ||||||
|  | #~ msgid "Detailed view" | ||||||
|  | #~ msgstr "Vue détaillée" | ||||||
|   | |||||||
| @@ -75,6 +75,7 @@ INSTALLED_APPS = [ | |||||||
|     'permission', |     'permission', | ||||||
|     'registration', |     'registration', | ||||||
|     'scripts', |     'scripts', | ||||||
|  |     'sheets', | ||||||
|     'treasury', |     'treasury', | ||||||
|     'wei', |     'wei', | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -97,13 +97,16 @@ body { | |||||||
|  |  | ||||||
| .btn-primary:hover, | .btn-primary:hover, | ||||||
| .btn-primary:not(:disabled):not(.disabled).active, | .btn-primary:not(:disabled):not(.disabled).active, | ||||||
| .btn-primary:not(:disabled):not(.disabled):active { | .btn-primary:not(:disabled):not(.disabled):active, | ||||||
|  | a.badge-primary:hover, | ||||||
|  | a.badge-primary:not(:disabled):not(.disabled).active, | ||||||
|  | a.badge-primary:not(:disabled):not(.disabled):active { | ||||||
|     color: #fff; |     color: #fff; | ||||||
|     background-color: rgb(102, 83, 105); |     background-color: rgb(102, 83, 105); | ||||||
|     border-color: rgb(102, 83, 105); |     border-color: rgb(102, 83, 105); | ||||||
| } | } | ||||||
|  |  | ||||||
| .btn-primary { | .btn-primary, a.badge-primary { | ||||||
|     color: rgba(248, 249, 250, 0.9);  |     color: rgba(248, 249, 250, 0.9);  | ||||||
|     background-color: rgb(102, 83, 105); |     background-color: rgb(102, 83, 105); | ||||||
|     border-color: rgb(102, 83, 105); |     border-color: rgb(102, 83, 105); | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ urlpatterns = [ | |||||||
|     path('activity/', include('activity.urls')), |     path('activity/', include('activity.urls')), | ||||||
|     path('treasury/', include('treasury.urls')), |     path('treasury/', include('treasury.urls')), | ||||||
|     path('wei/', include('wei.urls')), |     path('wei/', include('wei.urls')), | ||||||
|  |     path('sheets/', include('sheets.urls')), | ||||||
|  |  | ||||||
|     # Include Django Contrib and Core routers |     # Include Django Contrib and Core routers | ||||||
|     path('i18n/', include('django.conf.urls.i18n')), |     path('i18n/', include('django.conf.urls.i18n')), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user