1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 18:08:21 +02:00

Compare commits

..

6 Commits

Author SHA1 Message Date
51d60d064c Add waiting lists interfaces
Signed-off-by: Emmy D'ANELLO <ynerant@crans.org>
2022-08-18 23:44:49 +02:00
45334e4e02 Add order interface
Signed-off-by: Emmy D'ANELLO <ynerant@crans.org>
2022-08-18 17:27:59 +02:00
5174c84b33 Manage food options
Signed-off-by: Emmy D'ANELLO <ynerant@crans.org>
2022-08-18 14:50:45 +02:00
51e5e3669e Add interface to create and see note sheets
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-08-18 14:27:02 +02:00
44994a3ae7 Add new application to manage note sheets
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-08-18 12:33:10 +02:00
ba017c38c0 Fix permission that allows users to create OAuth2 apps
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-07-22 17:18:53 +02:00
44 changed files with 2538 additions and 242 deletions

View File

@ -6,7 +6,7 @@
"name": "Pot",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 1000
"guest_entry_fee": 500
}
},
{
@ -28,25 +28,5 @@
"can_invite": false,
"guest_entry_fee": 0
}
},
{
"model": "activity.activitytype",
"pk": 5,
"fields": {
"name": "Soir\u00e9e avec entrées",
"manage_entries": true,
"can_invite": false,
"guest_entry_fee": 0
}
},
{
"model": "activity.activitytype",
"pk": 7,
"fields": {
"name": "Soir\u00e9e avec invitations",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 0
}
}
]

View File

@ -1,5 +0,0 @@
from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
page_size_query_param = 'page_size'

View File

@ -26,6 +26,10 @@ if "note" in settings.INSTALLED_APPS:
from note.api.urls import register_note_urls
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:
from treasury.api.urls import register_treasury_urls
register_treasury_urls(router, 'treasury')

View 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'),
),
]

View File

@ -221,7 +221,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
.done(function () {
if (!isNaN(source.balance)) {
const newBalance = source.balance - quantity * amount
if (newBalance <= -2000) {
if (newBalance <= -5000) {
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
} else if (newBalance < 0) {

View File

@ -314,7 +314,7 @@ $('#btn_transfer').click(function () {
if (!isNaN(source.note.balance)) {
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
if (newBalance <= -2000) {
if (newBalance <= -5000) {
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
reset()

View File

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

View File

@ -0,0 +1,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 %}

View 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 %}

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
# Generated by Django 2.2.28 on 2023-07-24 10:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('permission', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='role',
name='for_club',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='for club'),
),
]

View File

@ -339,7 +339,6 @@ class Role(models.Model):
"member.Club",
verbose_name=_("for club"),
on_delete=models.PROTECT,
blank=True,
null=True,
default=None,
)

4
apps/sheets/__init__.py Normal file
View 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
View 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

View File

View 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
View 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
View 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
View 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
View 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(),
}

View 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'),
),
]

View File

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

View File

289
apps/sheets/models.py Normal file
View 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
View 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,))
}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

26
apps/sheets/urls.py Normal file
View 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
View 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

View File

@ -310,8 +310,8 @@ class SogeCredit(models.Model):
amount = sum(transaction.total for transaction in self.transactions.all())
if 'wei' in settings.INSTALLED_APPS:
from wei.models import WEIMembership
if not WEIMembership.objects\
.filter(club__weiclub__year=self.credit_transaction.created_at.year, user=self.user).exists():
if not WEIMembership.objects.filter(club__weiclub__year=datetime.date.today().year, user=self.user)\
.exists():
# 80 € for people that don't go to WEI
amount += 8000
return amount
@ -329,18 +329,17 @@ class SogeCredit(models.Model):
bde_qs = Membership.objects.filter(user=self.user, club=bde, date_start__gte=bde.membership_start)
kfet_qs = Membership.objects.filter(user=self.user, club=kfet, date_start__gte=kfet.membership_start)
## Soge do not pay BDE and kfet memberships this year (2022-2023)
# if bde_qs.exists():
# m = bde_qs.get()
# if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
# if m.transaction not in self.transactions.all():
# self.transactions.add(m.transaction)
#
# if kfet_qs.exists():
# m = kfet_qs.get()
# if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
# if m.transaction not in self.transactions.all():
# self.transactions.add(m.transaction)
if bde_qs.exists():
m = bde_qs.get()
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction)
if kfet_qs.exists():
m = kfet_qs.get()
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction)
if 'wei' in settings.INSTALLED_APPS:
from wei.models import WEIClub

View File

@ -108,7 +108,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
name="",
address="",
)
if not PermissionBackend.check_perm(self.request, "treasury.view_invoice", sample_invoice):
if not PermissionBackend.check_perm(self.request, "treasury.add_invoice", sample_invoice):
raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs)

View File

@ -14,17 +14,14 @@ from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInf
from ...models import WEIMembership
WORDS = [
'ABBA', 'After', 'Alcoolique anonyme', 'Ambiance festive', 'Années 2000', 'Apéro', 'Art',
'Baby foot billard biere pong', 'BBQ', 'Before', 'Bière pong', 'Bon enfant', 'Calme', 'Canapé',
'Chanson paillarde', 'Chanson populaire', 'Chartreuse', 'Cheerleader', 'Chill', 'Choré',
'Cinéma', 'Cocktail', 'Comédie musicle', 'Commercial', 'Copaing', 'Danse', 'Dancefloor',
'Electro', 'Fanfare', 'Gin tonic', 'Inclusif', 'Jazz', "Jeux d'alcool", 'Jeux de carte',
'Jeux de rôle', 'Jeux de société', 'JUL', 'Jus de fruit', 'Kfet', 'Kleptomanie assurée',
'LGBTQ+', 'Livre', 'Morning beer', 'Musique', 'NAPS', 'Paillettes', 'Pastis', 'Paté Hénaff',
'Peluche', 'Pena baiona', "Peu d'alcool", 'Pilier de bar', 'PMU', 'Poulpe', 'Punch', 'Rap',
'Réveil', 'Rock', 'Rugby', 'Sandwich', 'Serge', 'Shot', 'Sociable', 'Spectacle', 'Techno',
'Techno house', 'Thérapie Taxi', 'Tradition kchanaises', 'Troisième mi-temps', 'Turn up',
'Vodka', 'Vodka pomme', 'Volley', 'Vomi stratégique'
'13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant',
'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill',
'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial',
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit',
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic',
'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi',
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
]

View File

@ -118,13 +118,13 @@ Exemples
{"F": [
"ADD",
["F", "source__balance"],
2000]
5000]
}
}
]
| si la destination est la note du club dont on est membre et si le montant est inférieur au solde de la source + 20 €,
autrement dit le solde final est au-dessus de -20 €.
| si la destination est la note du club dont on est membre et si le montant est inférieur au solde de la source + 50 €,
autrement dit le solde final est au-dessus de -50 €.
Masques de permissions

View File

@ -83,6 +83,13 @@ Je suis trésorier d'un club, qu'ai-je le droit de faire ?
bien sûr permis pour faciliter des transferts. Tout abus de droits constaté
pourra mener à des sanctions prises par le bureau du BDE.
.. warning::
Une fonctionnalité pour permettre de gérer plus proprement les remboursements
entre amis est en cours de développement. Temporairement et pour des raisons
de confort, les trésoriers de clubs ont le droit de prélever n'importe quelle
adhérente vers n'importe quelle autre note adhérente, tant que la source ne
descend pas sous ``- 50 €``. Ces droits seront retirés d'ici quelques semaines.
Je suis trésorier d'un club, je n'arrive pas à voir le solde du club / faire des transactions
---------------------------------------------------------------------------------------------------

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \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"
"Last-Translator: elkmaennchen <elkmaennchen@crans.org>\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/permission/models.py:330
#: 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/templates/wei/base.html:26
#: apps/wei/templates/wei/weimembership_form.html:14
@ -95,7 +96,8 @@ msgstr "types d'activité"
#: apps/activity/models.py:68
#: apps/activity/templates/activity/includes/activity_info.html:19
#: 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"
msgstr "description"
@ -143,6 +145,7 @@ msgstr ""
#: apps/activity/models.py:109
#: apps/activity/templates/activity/includes/activity_info.html:25
#: apps/sheets/models.py:20
msgid "start date"
msgstr "date de début"
@ -171,7 +174,7 @@ msgid "entry time"
msgstr "heure d'entrée"
#: 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"
msgstr "note"
@ -334,7 +337,10 @@ msgstr "Entrée effectuée !"
#: apps/member/templates/member/add_members.html:46
#: apps/member/templates/member/club_form.html:16
#: 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/wei/templates/wei/bus_form.html:17
#: apps/wei/templates/wei/busteam_form.html:17
@ -1470,7 +1476,7 @@ msgstr "modèles de transaction"
msgid "used alias"
msgstr "alias utilisé"
#: apps/note/models/transactions.py:136
#: apps/note/models/transactions.py:136 apps/sheets/views.py:277
msgid "quantity"
msgstr "quantité"
@ -1592,8 +1598,10 @@ msgid "Delete"
msgstr "Supprimer"
#: apps/note/tables.py:222 apps/note/templates/note/conso_form.html:132
#: apps/wei/tables.py:49 apps/wei/tables.py:50
#: apps/wei/templates/wei/base.html:89
#: apps/sheets/templates/sheets/sheet_detail.html:21
#: 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/busteam_detail.html:20
#: apps/wei/templates/wei/busteam_detail.html:40
@ -1746,6 +1754,98 @@ msgstr "Bouton affiché"
msgid "An error occured"
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
msgid "Transfer money"
msgstr "Transférer de l'argent"
@ -2167,6 +2267,237 @@ msgstr ""
msgid "Invalidate pre-registration"
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
msgid "Treasury"
msgstr "Trésorerie"
@ -2253,10 +2584,6 @@ msgstr "Facture n°{id}"
msgid "Designation"
msgstr "Désignation"
#: apps/treasury/models.py:140
msgid "Quantity"
msgstr "Quantité"
#: apps/treasury/models.py:145
msgid "Unit price"
msgstr "Prix unitaire"
@ -3185,19 +3512,19 @@ msgstr "Répartir les 1A dans les bus"
msgid "Attribute bus"
msgstr "Attribuer un bus"
#: note_kfet/settings/base.py:172
#: note_kfet/settings/base.py:173
msgid "German"
msgstr "Allemand"
#: note_kfet/settings/base.py:173
#: note_kfet/settings/base.py:174
msgid "English"
msgstr "Anglais"
#: note_kfet/settings/base.py:174
#: note_kfet/settings/base.py:175
msgid "Spanish"
msgstr "Espagnol"
#: note_kfet/settings/base.py:175
#: note_kfet/settings/base.py:176
msgid "French"
msgstr "Français"
@ -3347,11 +3674,6 @@ msgstr "Il n'y a pas de résultat."
msgid "Are you sure to delete the 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
msgid "Client id"
msgstr "ID client"
@ -3562,3 +3884,6 @@ msgstr ""
"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 "
"lien que vous avez reçu."
#~ msgid "Detailed view"
#~ msgstr "Vue détaillée"

View File

@ -75,6 +75,7 @@ INSTALLED_APPS = [
'permission',
'registration',
'scripts',
'sheets',
'treasury',
'wei',
]
@ -252,7 +253,7 @@ REST_FRAMEWORK = {
'rest_framework.authentication.TokenAuthentication',
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
],
'DEFAULT_PAGINATION_CLASS': 'apps.api.pagination.CustomPagination',
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}

View File

@ -97,13 +97,16 @@ body {
.btn-primary:hover,
.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;
background-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);
background-color: rgb(102, 83, 105);
border-color: rgb(102, 83, 105);

View File

@ -96,7 +96,7 @@ function displayStyle (note) {
if (!note) { return '' }
const balance = note.balance
var css = ''
if (balance < -2000) { css += ' text-danger bg-dark' }
if (balance < -5000) { css += ' text-danger bg-dark' }
else if (balance < -1000) { css += ' text-danger' }
else if (balance < 0) { css += ' text-warning' }
if (!note.email_confirmed) { css += ' bg-primary' }

View File

@ -23,7 +23,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% csrf_token %}
{{ form|crispy }}
{{ profile_form|crispy }}
{% comment "Soge not for membership (only WEI)" %} {{ soge_form|crispy }} {% endcomment %}
{{ soge_form|crispy }}
<button class="btn btn-success" type="submit">
{% trans "Sign up" %}
</button>

View File

@ -21,6 +21,7 @@ urlpatterns = [
path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')),
path('wei/', include('wei.urls')),
path('sheets/', include('sheets.urls')),
# Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')),