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

Compare commits

..

19 Commits

Author SHA1 Message Date
e9f4795d13 Implementing QRcode creation, modifying Allergen model and creating of few views 2024-07-03 19:20:01 +02:00
1aa779f479 linters 2024-07-02 22:13:19 +02:00
631a5a59ad Update .gitlab-ci.yml 2024-07-02 22:13:19 +02:00
aea6ec5e49 charte info 2024-07-02 22:09:22 +02:00
0e83ac32a2 error py37-django22 2024-07-02 22:09:22 +02:00
c49a94b87f new_logo 2024-07-02 22:09:22 +02:00
a3073ba5a5 Un peu de nettoyage, rajout de commentaires 2024-05-25 22:34:59 +02:00
9b9fa0bcfe few changes in models, delete default label 2024-05-25 16:47:24 +02:00
b1d0cf92b1 création de forms fonctionnel (form + views + url + html), few changes in models.py 2024-05-25 15:27:26 +02:00
0c3e712f8f création d'un form pour l'ajout d'aliments basiques 2024-05-24 21:49:23 +02:00
708216a67f nom app 2024-05-24 21:47:30 +02:00
c27a8fefe5 First forms 2024-05-23 23:53:33 +02:00
c8afee91d2 Annulation des modifications du Readme, voir https://wiki.crans.org/NoteKfet/NoteKfet2020 pour le cahier des charges 2024-05-21 14:49:07 +02:00
aaa6076e9b Réagencement des tables et de leurs attributs 2024-05-21 14:07:35 +02:00
77233e995e fusion de branche (j'avais fait nimp avec git) 2024-05-21 11:28:51 +02:00
c9980b0bd1 création de l'interface admin temporaire 2024-05-21 11:21:13 +02:00
4e6ec16e94 Rajout de la pseudo-doc 2024-05-17 21:33:30 +02:00
89785ce632 Création de l'apps et de la base de donnée 2024-05-17 20:46:38 +02:00
b636ca49d1 Update README.md 2024-05-17 20:40:52 +02:00
43 changed files with 1427 additions and 548 deletions

View File

@ -17,8 +17,7 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView
from django.views.generic.list import ListView
from django_tables2.views import MultiTableMixin
from django_tables2.views import SingleTableView
from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@ -58,40 +57,27 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Displays all Activities, and classify if they are on-going or upcoming ones.
"""
model = Activity
tables = [ActivityTable, ActivityTable]
table_class = ActivityTable
ordering = ('-date_start',)
extra_context = {"title": _("Activities")}
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).distinct()
def get_tables(self):
tables = super().get_tables()
tables[0].prefix = "all-"
tables[1].prefix = "upcoming-"
return tables
def get_tables_data(self):
# first table = all activities, second table = upcoming
return [
self.get_queryset().order_by("-date_start"),
Activity.objects.filter(date_end__gt=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
.distinct()
.order_by("date_start")
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tables = context["tables"]
for name, table in zip(["table", "upcoming"], tables):
context[name] = table
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
prefix='upcoming-',
order_by='date_start',
)
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
context["started_activities"] = started_activities

0
apps/food/__init__.py Normal file
View File

35
apps/food/admin.py Normal file
View File

@ -0,0 +1,35 @@
# Copyright (C) 2018-2024 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 .models import Allergen, BasicFood, QRCode, TransformedFood
@admin.register(QRCode, site=admin_site)
class QRCodeAdmin(admin.ModelAdmin):
"""
TEMPORARY
"""
@admin.register(BasicFood, site=admin_site)
class BasicFoodAdmin(admin.ModelAdmin):
"""
TEMPORARY
"""
@admin.register(TransformedFood, site=admin_site)
class TransformedFoodAdmin(admin.ModelAdmin):
"""
TEMPORARY
"""
@admin.register(Allergen, site=admin_site)
class AllergenAdmin(admin.ModelAdmin):
"""
TEMPORARY
"""

11
apps/food/apps.py Normal file
View File

@ -0,0 +1,11 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class FoodkfetConfig(AppConfig):
name = 'food'
verbose_name = _('food')

View File

@ -0,0 +1,107 @@
[
{
"model": "food.allergen",
"pk": 1,
"fields": {
"name": "alcohol"
}
},
{
"model": "food.allergen",
"pk": 2,
"fields": {
"name": "celery"
}
},
{
"model": "food.allergen",
"pk": 3,
"fields": {
"name": "crustecean"
}
},
{
"model": "food.allergen",
"pk": 4,
"fields": {
"name": "egg"
}
},
{
"model": "food.allergen",
"pk": 5,
"fields": {
"name": "fish"
}
},
{
"model": "food.allergen",
"pk": 6,
"fields": {
"name": "gluten"
}
},
{
"model": "food.allergen",
"pk": 7,
"fields": {
"name": "groundnut"
}
},
{
"model": "food.allergen",
"pk": 8,
"fields": {
"name": "lupine"
}
},
{
"model": "food.allergen",
"pk": 9,
"fields": {
"name": "milk"
}
},
{
"model": "food.allergen",
"pk": 10,
"fields": {
"name": "mollusc"
}
},
{
"model": "food.allergen",
"pk": 11,
"fields": {
"name": "mustard"
}
},
{
"model": "food.allergen",
"pk": 12,
"fields": {
"name": "nut"
}
},
{
"model": "food.allergen",
"pk": 13,
"fields": {
"name": "sesame"
}
},
{
"model": "food.allergen",
"pk": 14,
"fields": {
"name": "soy"
}
},
{
"model": "food.allergen",
"pk": 15,
"fields": {
"name": "sulphite"
}
}
]

73
apps/food/forms.py Normal file
View File

@ -0,0 +1,73 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from random import shuffle
from django import forms
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from member.models import Club
from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from .models import BasicFood, TransformedFood
class BasicFoodForms(forms.ModelForm):
"""
Form for add non-transformed food
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['name'].required = True
self.fields['owner'].required = True
# Some example
self.fields['name'].widget.attrs.update({"placeholder": _("pasta")})
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
class Meta:
model = BasicFood
fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
'expiry_date': DateTimePickerInput(),
}
class TransformedFoodForms(forms.ModelForm):
"""
Form for add transformed food
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['name'].required = True
self.fields['owner'].required = True
self.fields['creation_date'].required = True
self.fields['creation_date'].initial = timezone.now
self.fields['is_active'].initial = True
# Some example
self.fields['name'].widget.attrs.update({"placeholder": _("lasagna")})
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
class Meta:
model = TransformedFood
fields = ('name', 'creation_date', 'owner', 'is_active', 'allergens')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
'creation_date': DateTimePickerInput(),
}

View File

@ -0,0 +1,72 @@
# Generated by Django 2.2.28 on 2024-07-03 07:40
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('member', '0011_profile_vss_charter_read'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Allergen',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, null=True, verbose_name='name')),
],
options={
'verbose_name': 'Allergen',
'verbose_name_plural': 'Allergens',
},
),
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='name')),
('expiry_date', models.DateTimeField(verbose_name='expiry date')),
('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
('code', models.IntegerField(unique=True, verbose_name='code')),
('allergens', models.ManyToManyField(blank=True, to='food.Allergen', verbose_name='allergen')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_food.food_set+', to='contenttypes.ContentType')),
],
options={
'verbose_name': 'foods',
},
),
migrations.CreateModel(
name='BasicFood',
fields=[
('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')),
('date_type', models.CharField(choices=[('DLC', 'DLC'), ('DDM', 'DDM')], max_length=255)),
('arrival_date', models.DateTimeField(blank=True, default=django.utils.timezone.now, verbose_name='arrival date')),
],
options={
'verbose_name': 'Basic food',
'verbose_name_plural': 'Basic foods',
},
bases=('food.food',),
),
migrations.CreateModel(
name='TransformedFood',
fields=[
('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')),
('creation_date', models.DateTimeField(verbose_name='creation date')),
('is_active', models.BooleanField(default=True, verbose_name='is active')),
('ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.Food', verbose_name='transformed ingredient')),
],
options={
'verbose_name': 'Transformed food',
'verbose_name_plural': 'Transformed foods',
},
bases=('food.food',),
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.28 on 2024-07-03 13:49
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('food', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='food',
name='code',
),
migrations.CreateModel(
name='QRCode',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('qr_code_number', models.PositiveIntegerField(verbose_name='QR-code number')),
('food_container', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.Food', unique=True, verbose_name='food container')),
],
options={
'verbose_name': 'QR-code',
'verbose_name_plural': 'QR-codes',
},
),
]

View File

153
apps/food/models.py Normal file
View File

@ -0,0 +1,153 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models, transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import Club
from polymorphic.models import PolymorphicModel
#################################################################
# TO DO
# - link allergen with one food (basic or transformed) with check
# - check on basic food
# - check on transformed food
#################################################################
class QRCode(models.Model):
"""
An QRCode model
"""
qr_code_number = models.PositiveIntegerField(
verbose_name=_("QR-code number"),
)
food_container = models.ForeignKey(
'Food',
on_delete=models.PROTECT,
related_name='QR_code',
unique=True,
verbose_name=_('food container'),
)
class Meta:
verbose_name = _("QR-code")
verbose_name_plural = _("QR-codes")
def __str__(self):
return _("QR-code number {qr_code_number}").format(qr_code_number=self.qr_code_number)
class Allergen(models.Model):
"""
A list of allergen and alimentary restrictions
"""
name = models.CharField(
verbose_name=_('name'),
max_length=255,
)
class Meta:
verbose_name = _('Allergen')
verbose_name_plural = _('Allergens')
def __str__(self):
return self.name
class Food(PolymorphicModel):
name = models.CharField(
verbose_name=_('name'),
max_length=255,
)
owner = models.ForeignKey(
Club,
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('owner'),
)
allergens = models.ManyToManyField(
Allergen,
blank=True,
verbose_name=_('allergen'),
)
expiry_date = models.DateTimeField(
verbose_name=_('expiry date'),
)
was_eaten = models.BooleanField(
default=False,
verbose_name=_('was eaten'),
)
def __str__(self):
return self.name
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
return super().save(force_insert, force_update, using, update_fields)
class Meta:
verbose_name = _('food')
verbose_name = _('foods')
class BasicFood(Food):
"""
Food which has been directly buy on supermarket
"""
date_type = models.CharField(
max_length=255,
choices=(
("DLC", "DLC"),
("DDM", "DDM"),
)
)
arrival_date = models.DateTimeField(
verbose_name=_('arrival date'),
default=timezone.now,
blank=True, # TEMPORARY
)
# label = models.ImageField(
# verbose_name=_('food label'),
# max_length=255,
# blank=False,
# null=False,
# upload_to='label/',
# )
class Meta:
verbose_name = _('Basic food')
verbose_name_plural = _('Basic foods')
class TransformedFood(Food):
"""
Transformed food are a mix between basic food and meal
"""
creation_date = models.DateTimeField(
verbose_name=_('creation date'),
)
ingredient = models.ManyToManyField(
Food,
blank=True,
symmetrical=False,
related_name='transformed_ingredient_inv',
verbose_name=_('transformed ingredient'),
)
is_active = models.BooleanField(
default=True,
verbose_name=_('is active'),
)
class Meta:
verbose_name = _('Transformed food')
verbose_name_plural = _('Transformed foods')

View File

@ -0,0 +1,422 @@
var LOCK = false
sources = []
sources_notes_display = []
dests = []
dests_notes_display = []
function refreshHistory () {
$('#history').load('/note/transfer/ #history')
}
function reset (refresh = true) {
sources_notes_display.length = 0
sources.length = 0
dests_notes_display.length = 0
dests.length = 0
$('#source_note_list').html('')
$('#dest_note_list').html('')
const source_field = $('#source_note')
source_field.val('')
const event = jQuery.Event('keyup')
event.originalEvent = { charCode: 97 }
source_field.trigger(event)
source_field.removeClass('is-invalid')
source_field.attr('data-original-title', '').tooltip('hide')
const dest_field = $('#dest_note')
dest_field.val('')
dest_field.trigger(event)
dest_field.removeClass('is-invalid')
dest_field.attr('data-original-title', '').tooltip('hide')
const amount_field = $('#amount')
amount_field.val('')
amount_field.removeClass('is-invalid')
$('#amount-required').html('')
const reason_field = $('#reason')
reason_field.val('')
reason_field.removeClass('is-invalid')
$('#reason-required').html('')
$('#last_name').val('')
$('#first_name').val('')
$('#bank').val('')
$('#user_note').val('')
$('#profile_pic').attr('src', '/static/member/img/default_picture.png')
$('#profile_pic_link').attr('href', '#')
if (refresh) {
refreshBalance()
refreshHistory()
}
LOCK = false
}
$(document).ready(function () {
/**
* If we are in credit/debit mode, check that only one note is entered.
* More over, get first name and last name to autocomplete fields.
*/
function checkUniqueNote () {
if ($('#type_credit').is(':checked') || $('#type_debit').is(':checked')) {
const arr = $('#type_credit').is(':checked') ? dests_notes_display : sources_notes_display
if (arr.length === 0) { return }
const last = arr[arr.length - 1]
arr.length = 0
arr.push(last)
last.quantity = 1
if (last.note.club) {
$('#last_name').val(last.note.name)
$('#first_name').val(last.note.name)
}
else if (!last.note.user) {
$.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) {
last.note.user = note.user
$.getJSON('/api/user/' + last.note.user + '/', function (user) {
$('#last_name').val(user.last_name)
$('#first_name').val(user.first_name)
})
})
} else {
$.getJSON('/api/user/' + last.note.user + '/', function (user) {
$('#last_name').val(user.last_name)
$('#first_name').val(user.first_name)
})
}
}
return true
}
autoCompleteNote('source_note', 'source_note_list', sources, sources_notes_display,
'source_alias', 'source_note', 'user_note', 'profile_pic', checkUniqueNote)
autoCompleteNote('dest_note', 'dest_note_list', dests, dests_notes_display,
'dest_alias', 'dest_note', 'user_note', 'profile_pic', checkUniqueNote)
const source = $('#source_note')
const dest = $('#dest_note')
$('#type_transfer').change(function () {
if (LOCK) { return }
$('#source_me_div').removeClass('d-none')
$('#source_note').removeClass('is-invalid')
$('#dest_note').removeClass('is-invalid')
$('#special_transaction_div').addClass('d-none')
source.removeClass('d-none')
$('#source_note_list').removeClass('d-none')
$('#credit_type').addClass('d-none')
dest.removeClass('d-none')
$('#dest_note_list').removeClass('d-none')
$('#debit_type').addClass('d-none')
$('#source_note_label').text(select_emitters_label)
$('#dest_note_label').text(select_receveirs_label)
location.hash = 'transfer'
})
$('#type_credit').change(function () {
if (LOCK) { return }
$('#source_me_div').addClass('d-none')
$('#source_note').removeClass('is-invalid')
$('#dest_note').removeClass('is-invalid')
$('#special_transaction_div').removeClass('d-none')
$('#source_note_list').addClass('d-none')
$('#dest_note_list').removeClass('d-none')
source.addClass('d-none')
source.tooltip('hide')
$('#credit_type').removeClass('d-none')
dest.removeClass('d-none')
dest.val('')
dest.tooltip('hide')
$('#debit_type').addClass('d-none')
$('#source_note_label').text(transfer_type_label)
$('#dest_note_label').text(select_receveir_label)
if (dests_notes_display.length > 1) {
$('#dest_note_list').html('')
dests_notes_display.length = 0
}
location.hash = 'credit'
})
$('#type_debit').change(function () {
if (LOCK) { return }
$('#source_me_div').addClass('d-none')
$('#source_note').removeClass('is-invalid')
$('#dest_note').removeClass('is-invalid')
$('#special_transaction_div').removeClass('d-none')
$('#source_note_list').removeClass('d-none')
$('#dest_note_list').addClass('d-none')
source.removeClass('d-none')
source.val('')
source.tooltip('hide')
$('#credit_type').addClass('d-none')
dest.addClass('d-none')
dest.tooltip('hide')
$('#debit_type').removeClass('d-none')
$('#source_note_label').text(select_emitter_label)
$('#dest_note_label').text(transfer_type_label)
if (sources_notes_display.length > 1) {
$('#source_note_list').html('')
sources_notes_display.length = 0
}
location.hash = 'debit'
})
$('#credit_type').change(function () {
const type = $('#credit_type option:selected').text()
if ($('#type_credit').is(':checked')) { source.val(type) } else { dest.val(type) }
})
// Ensure we begin in transfer mode. Removing these lines may cause problems when reloading.
const type_transfer = $('#type_transfer') // Default mode
type_transfer.removeAttr('checked')
$('#type_credit').removeAttr('checked')
$('#type_debit').removeAttr('checked')
if (location.hash) { $('#type_' + location.hash.substr(1)).click() } else { type_transfer.click() }
$('#source_me').click(function () {
if (LOCK) { return }
// Shortcut to set the current user as the only emitter
sources_notes_display.length = 0
sources.length = 0
$('#source_note_list').html('')
const source_note = $('#source_note')
source_note.focus()
source_note.val('')
let event = jQuery.Event('keyup')
event.originalEvent = { charCode: 97 }
source_note.trigger(event)
source_note.val(username)
event = jQuery.Event('keyup')
event.originalEvent = { charCode: 97 }
source_note.trigger(event)
const fill_note = function () {
if (sources.length === 0) {
setTimeout(fill_note, 100)
return
}
event = jQuery.Event('keypress')
event.originalEvent = { charCode: 13 }
source_note.trigger(event)
source_note.tooltip('hide')
source_note.val('')
$('#dest_note').focus()
}
fill_note()
})
})
// Make transfer when pressing Enter on the amount section
$('#amount, #reason, #last_name, #first_name, #bank').keypress((event) => {
if (event.originalEvent.charCode === 13) {
$('#btn_transfer').click()
}
})
$('#btn_transfer').click(function () {
if (LOCK) { return }
LOCK = true
let error = false
const amount_field = $('#amount')
amount_field.removeClass('is-invalid')
$('#amount-required').html('')
const reason_field = $('#reason')
reason_field.removeClass('is-invalid')
$('#reason-required').html('')
if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) {
amount_field.addClass('is-invalid')
$('#amount-required').html('<strong>' + gettext('This field is required and must contain a decimal positive number.') + '</strong>')
error = true
}
const amount = Math.round(100 * amount_field.val())
if (amount > 2147483647) {
amount_field.addClass('is-invalid')
$('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')
error = true
}
if (!reason_field.val() && $('#type_transfer').is(':checked')) {
reason_field.addClass('is-invalid')
$('#reason-required').html('<strong>' + gettext('This field is required.') + '</strong>')
error = true
}
if (!sources_notes_display.length && !$('#type_credit').is(':checked')) {
$('#source_note').addClass('is-invalid')
error = true
}
if (!dests_notes_display.length && !$('#type_debit').is(':checked')) {
$('#dest_note').addClass('is-invalid')
error = true
}
if (error) {
LOCK = false
return
}
let reason = reason_field.val()
if ($('#type_transfer').is(':checked')) {
// We copy the arrays to ensure that transactions are well-processed even if the form is reset
[...sources_notes_display].forEach(function (source) {
[...dests_notes_display].forEach(function (dest) {
if (source.note.id === dest.note.id) {
addMsg(interpolate(gettext('Warning: the transaction of %s from %s to %s was not made because ' +
'it is the same source and destination note.'), [pretty_money(amount), source.name, dest.name]), 'warning', 10000)
LOCK = false
return
}
$.post('/api/note/transaction/transaction/',
{
csrfmiddlewaretoken: CSRF_TOKEN,
quantity: source.quantity * dest.quantity,
amount: amount,
reason: reason,
valid: true,
polymorphic_ctype: TRANSFER_POLYMORPHIC_CTYPE,
resourcetype: 'Transaction',
source: source.note.id,
source_alias: source.name,
destination: dest.note.id,
destination_alias: dest.name
}).done(function () {
if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) {
addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000)
}
if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) {
addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [dest.name]), 'danger', 30000)
}
if (!isNaN(source.note.balance)) {
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
if (newBalance <= -2000) {
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()
return
} else if (newBalance < 0) {
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is negative.'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
reset()
return
}
}
addMsg(interpolate(gettext('Transfer of %s from %s to %s succeed!'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name]), 'success', 10000)
reset()
}).fail(function (err) { // do it again but valid = false
const errObj = JSON.parse(err.responseText)
if (errObj.non_field_errors) {
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, errObj.non_field_errors]), 'danger')
LOCK = false
return
}
$.post('/api/note/transaction/transaction/',
{
csrfmiddlewaretoken: CSRF_TOKEN,
quantity: source.quantity * dest.quantity,
amount: amount,
reason: reason,
valid: false,
invalidity_reason: 'Solde insuffisant',
polymorphic_ctype: TRANSFER_POLYMORPHIC_CTYPE,
resourcetype: 'Transaction',
source: source.note.id,
source_alias: source.name,
destination: dest.note.id,
destination_alias: dest.name
}).done(function () {
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, gettext('insufficient funds')]), 'danger', 10000)
reset()
}).fail(function (err) {
const errObj = JSON.parse(err.responseText)
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) { error = err.responseText }
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, error]), 'danger')
LOCK = false
})
})
})
})
} else if ($('#type_credit').is(':checked') || $('#type_debit').is(':checked')) {
let special_note
let user_note
let alias
const given_reason = reason
let source_id, dest_id
if ($('#type_credit').is(':checked')) {
special_note = $('#credit_type').val()
user_note = dests_notes_display[0].note
alias = dests_notes_display[0].name
source_id = special_note
dest_id = user_note.id
reason = 'Crédit ' + $('#credit_type option:selected').text().toLowerCase()
if (given_reason.length > 0) { reason += ' (' + given_reason + ')' }
} else {
special_note = $('#debit_type').val()
user_note = sources_notes_display[0].note
alias = sources_notes_display[0].name
source_id = user_note.id
dest_id = special_note
reason = 'Retrait ' + $('#debit_type option:selected').text().toLowerCase()
if (given_reason.length > 0) { reason += ' (' + given_reason + ')' }
}
$.post('/api/note/transaction/transaction/',
{
csrfmiddlewaretoken: CSRF_TOKEN,
quantity: 1,
amount: amount,
reason: reason,
valid: true,
polymorphic_ctype: SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
resourcetype: 'SpecialTransaction',
source: source_id,
source_alias: sources_notes_display.length ? alias : null,
destination: dest_id,
destination_alias: dests_notes_display.length ? alias : null,
last_name: $('#last_name').val(),
first_name: $('#first_name').val(),
bank: $('#bank').val()
}).done(function () {
addMsg(gettext('Credit/debit succeed!'), 'success', 10000)
if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) }
reset()
}).fail(function (err) {
const errObj = JSON.parse(err.responseText)
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) { error = err.responseText }
addMsg(interpolate(gettext('Credit/debit failed: %s'), [error]), 'danger', 10000)
LOCK = false
})
}
})

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>name : {{ food.name }}</p>
<a href="{% url "food:basic_update" pk=food.pk %}">Update</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-block">
<a href="{% url "food:basic_create" %}" class="btn btn-sm btn-outline-primary">Basic</a>
<a href="{% url "food:transformed_create" %}" class="btn btn-sm btn-outline-primary">Transformed</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-block">
<a href="{% url "food:qrcode_basic_create" slug=slug %}" class="btn btn-sm btn-outline-primary">Basic</a>
<a href="{% url "food:qrcode_transformed_create" slug=slug %}" class="btn btn-sm btn-outline-primary">Transformed</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>qrcode : {{ qrcode.qr_code_number }}</p>
<p>name : {{ qrcode.food_container.name }}</p>
{% if qrcode.food_container.polymorphic_ctype.name == 'Basic food' %}
<a href="{% url "food:basic_update" pk=qrcode.food_container.pk %}">Update</a>
{% else %}
<a href="{% url "food:transformed_update" pk=qrcode.food_container.pk %}">Update</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>name : {{ food.name }}</p>
<p>owner : {{ food.owner }}</p>
<a href="{% url "food:transformed_update" pk=food.pk %}">Update</a>
</div>
</div>
{% endblock %}

3
apps/food/tests.py Normal file
View File

@ -0,0 +1,3 @@
# from django.test import TestCase
# Create your tests here.

23
apps/food/urls.py Normal file
View File

@ -0,0 +1,23 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from . import views
app_name = 'food'
urlpatterns = [
path('<int:slug>', views.QRCodeView.as_view(), name='qrcode_view'),
path('detail/<int:pk>', views.FoodView.as_view(), name='food_view'),
path('<int:slug>/create_qrcode', views.QRCodeCreateView.as_view(), name='qrcode_create'),
path('create', views.FoodCreateView.as_view(), name='food_create'),
path('<int:slug>/create_qrcode/basic', views.QRCodeBasicFoodCreateView.as_view(), name='qrcode_basic_create'),
path('<int:slug>/create_qrcode/transformed', views.QRCodeTransformedFoodCreateView.as_view(), name='qrcode_transformed_create'),
path('create/basic', views.BasicFoodCreateView.as_view(), name='basic_create'),
path('create/transformed', views.TransformedFoodCreateView.as_view(), name='transformed_create'),
path('update/basic/<int:pk>', views.BasicFoodUpdateView.as_view(), name='basic_update'),
path('update/transformed/<int:pk>', views.TransformedFoodUpdateView.as_view(), name='transformed_update'),
]

251
apps/food/views.py Normal file
View File

@ -0,0 +1,251 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from django.db import transaction
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.views.generic import DetailView, UpdateView, TemplateView
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import BasicFoodForms, TransformedFoodForms
from .models import BasicFood, Food, QRCode, TransformedFood
class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
A view to add a basic food
"""
model = QRCode
extra_context = {"title": _("Add a new meal")}
context_object_name = "qrcode"
slug_field = "qr_code_number"
def get(self, *args, **kwargs):
qrcode = kwargs["slug"]
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
return super().get(*args, **kwargs)
else:
return HttpResponseRedirect(reverse("food:qrcode_create", kwargs=kwargs))
class QRCodeCreateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView):
"""
A view to add a basic food
"""
template_name = 'food/create_qrcode_form.html'
extra_context = {"title": _("Add a new aliment")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["slug"] = kwargs["slug"]
return context
class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
A view to add a basic food
"""
model = Food
extra_context = {"title": _("Add a new meal")}
context_object_name = "food"
class FoodCreateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView):
"""
A view to add a basic food
"""
template_name = 'food/create_food_form.html'
extra_context = {"title": _("Add a new aliment")}
class BasicFoodFormView(ProtectQuerysetMixin):
#####################################################################
# TO DO
# - fix picture save
# - implement solution crop and convert image (reuse or recode ImageForm from members apps)
#####################################################################
"""
A view to add a basic food
"""
model = BasicFood
form_class = BasicFoodForms
template_name = 'food/basic_food_form.html'
extra_context = {"title": _("Add a new aliment")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
basic_food_form = BasicFoodForms(data=self.request.POST)
if not basic_food_form.is_valid():
return self.form_invalid(form)
# Save the aliment and the allergens associed
basic_food = form.save(commit=False)
# We assume the date of labeling and the same as the date of arrival
basic_food.arrival_date = timezone.now()
basic_food._force_save = True
basic_food.save()
basic_food.refresh_from_db()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
class BasicFoodUpdateView(BasicFoodFormView, LoginRequiredMixin, UpdateView):
pass
class BasicFoodCreateView(BasicFoodFormView, ProtectedCreateView):
def get_sample_object(self):
return BasicFood(
name="",
expiry_date=timezone.now(),
)
class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
#####################################################################
# TO DO
# - fix picture save
# - implement solution crop and convert image (reuse or recode ImageForm from members apps)
#####################################################################
"""
A view to add a basic food
"""
model = BasicFood
form_class = BasicFoodForms
template_name = 'food/basic_food_form.html'
extra_context = {"title": _("Add a new aliment")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
basic_food_form = BasicFoodForms(data=self.request.POST)
if not basic_food_form.is_valid():
return self.form_invalid(form)
# Save the aliment and the allergens associed
basic_food = form.save(commit=False)
# We assume the date of labeling and the same as the date of arrival
basic_food.arrival_date = timezone.now()
basic_food._force_save = True
basic_food.save()
basic_food.refresh_from_db()
qrcode = QRCode()
qrcode.qr_code_number = self.kwargs['slug']
qrcode.food_container = basic_food
qrcode.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
def get_sample_object(self):
return BasicFood(
name="",
expiry_date=timezone.now(),
)
class TransformedFoodFormView(ProtectQuerysetMixin):
#####################################################################
# TO DO
# - fix picture save
# - implement solution crop and convert image (reuse or recode ImageForm from members apps)
#####################################################################
"""
A view to add a tranformed food
"""
model = TransformedFood
template_name = 'food/transformed_food_form.html'
form_class = TransformedFoodForms
extra_context = {"title": _("Add a new meal")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
transformed_food_form = TransformedFoodForms(data=self.request.POST)
if not transformed_food_form.is_valid():
return self.form_invalid(form)
# Save the aliment and allergens associated
transformed_food = form.save(commit=False)
# Without microbiological analyzes, the storage time is 3 days
transformed_food.expiry_date = transformed_food.creation_date + timedelta(days=3)
transformed_food._force_save = True
transformed_food.save()
transformed_food.refresh_from_db()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
class TransformedFoodUpdateView(TransformedFoodFormView, LoginRequiredMixin, UpdateView):
pass
class TransformedFoodCreateView(TransformedFoodFormView, ProtectedCreateView):
def get_sample_object(self):
return TransformedFood(
name="",
creation_date=timezone.now(),
)
class QRCodeTransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
#####################################################################
# TO DO
# - fix picture save
# - implement solution crop and convert image (reuse or recode ImageForm from members apps)
#####################################################################
"""
A view to add a basic food
"""
model = TransformedFood
template_name = 'food/transformed_food_form.html'
form_class = TransformedFoodForms
extra_context = {"title": _("Add a new meal")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
transformed_food_form = TransformedFoodForms(data=self.request.POST)
if not transformed_food_form.is_valid():
return self.form_invalid(form)
# Save the aliment and allergens associated
transformed_food = form.save(commit=False)
# Without microbiological analyzes, the storage time is 3 days
transformed_food.expiry_date = transformed_food.creation_date + timedelta(days=3)
transformed_food._force_save = True
transformed_food.save()
transformed_food.refresh_from_db()
qrcode = QRCode()
qrcode.qr_code_number = self.kwargs['slug']
qrcode.food_container = transformed_food
qrcode.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
def get_sample_object(self):
return BasicFood(
name="",
expiry_date=timezone.now(),
)

View File

@ -138,9 +138,6 @@ class ImageForm(forms.Form):
return cleaned_data
def is_valid(self):
return super().is_valid() or super().clean().get('image') is None
class ClubForm(forms.ModelForm):
def clean(self):
@ -154,7 +151,7 @@ class ClubForm(forms.ModelForm):
class Meta:
model = Club
exclude = ("add_registration_form",)
fields = '__all__'
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-07-15 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0011_profile_vss_charter_read'),
]
operations = [
migrations.AddField(
model_name='club',
name='add_registration_form',
field=models.BooleanField(default=False, verbose_name='add to registration form'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-07 12:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0012_club_add_registration_form'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2024, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -259,11 +259,6 @@ class Club(models.Model):
help_text=_('Maximal date of a membership, after which members must renew it.'),
)
add_registration_form = models.BooleanField(
verbose_name=_("add to registration form"),
default=False,
)
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")

View File

@ -14,9 +14,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<form method="post" enctype="multipart/form-data" id="formUpload">
{% csrf_token %}
{{ form |crispy }}
{% if user.note.display_image != "pic/default.png" %}
<input type="submit" class="btn btn-primary" value="{% trans "Remove" %}">
{% endif %}
</form>
</div>
<!-- MODAL TO CROP THE IMAGE -->

View File

@ -326,9 +326,6 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
"""Save image to note"""
image = form.cleaned_data['image']
if image is None:
image = "pic/default.png"
else:
# Rename as a PNG or GIF
extension = image.name.split(".")[-1]
if extension == "gif":

View File

@ -23,7 +23,7 @@
<p>
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
est inférieur à 0 €.
est inférieur à 0 € depuis plus de 24h.
</p>
<p>

View File

@ -1,65 +0,0 @@
{% load pretty_money %}
{% load i18n %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>[Note Kfet] Récapitulatif de trésorerie</title>
</head>
<body>
<h1>
Récapitulatif de trésorerie au {{ summary.date|date:"d/m/Y" }} à {{ summary.date|date:"H:i:s" }} :
</h1>
<h2>
Tous les utilisateur⋅rices :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_user }} soit {{ summary.balance_positive_user / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_user }}</li>
<li>Négatifs : {{ summary.total_negative_user }} soit {{ summary.balance_negative_user / 100 }} €</li>
</ul>
<h2>
Les {{ summary.total_positive_user_bde + summary.total_zero_user_bde + summary.total_negative_user_bde }} adhérent⋅es BDE :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_user_bde }} soit {{ summary.balance_positive_user_bde / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_user_bde }}</li>
<li>Négatifs : {{ summary.total_negative_user_bde }} soit {{ summary.balance_negative_user_bde / 100 }} €</li>
</ul>
<h2>
Clubs :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_club }} soit {{ summary.balance_positive_club / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_club }}</li>
<li>Négatifs : {{ summary.total_negative_club }} soit {{ summary.balance_negative_club / 100 }} €</li>
</ul>
<h2>
Clubs hors BDE / Kfet et club dont le nom fini par "- BDE" :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_club_nbde }} soit {{ summary.balance_positive_club_nbde / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_club_nbde }}</li>
<li>Négatifs : {{ summary.total_negative_club_nbde }} soit {{ summary.balance_negative_club_nbde / 100 }} €</li>
</ul>
<h2>
Progression :
</h2>
<ul>
<li>Ceci correspond à une différence de {{ balance_difference_user / 100 }} € pour les utilisateur⋅rices</li>
<li>Ceci correspond à une différence de {{ balance_difference_club / 100 }} € pour les clubs</li>
</ul>
--
<p>
Le BDE<br>
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
</p>
</body>
</html>

View File

@ -1,33 +0,0 @@
{% load pretty_money %}
{% load i18n %}
Récapitulatif de trésorerie au {{ summary.date|date:"d/m/Y" }} à {{ summary.date|date:"H:i:s" }} :
Tous les utilisateur⋅rices :
- Positifs : {{ summary.total_positive_user }} soit {{ summary.balance_positive_user / 100 }} €
- Neutres : {{ summary.total_zero_user }}
- Négatifs : {{ summary.total_negative_user }} soit {{ summary.balance_negative_user / 100 }} €
Les {{ summary.total_positive_user_bde + summary.total_zero_user_bde + summary.total_negative_user_bde }} adhérent⋅es BDE :
- Positifs : {{ summary.total_positive_user_bde }} soit {{ summary.balance_positive_user_bde / 100 }} €
- Neutres : {{ summary.total_zero_user_bde }}
- Négatifs : {{ summary.total_negative_user_bde }} soit {{ summary.balance_negative_user_bde /100 }} €
Clubs :
- Positifs : {{ summary.total_positive_club }} soit {{ summary.balance_positive_club / 100 }} €
- Neutres : {{ summary.total_zero_club }}
- Négatifs : {{ summary.total_negative_club }} soit {{ summary.balance_negative_club / 100 }} €
Clubs hors BDE / Kfet et club dont le nom fini par "- BDE" :
- Positifs : {{ summary.total_positive_club_nbde }} soit {{ summary.balance_positive_club_nbde / 100 }} €
- Neutres : {{ summary.total_zero_club_nbde }}
- Négatifs : {{ summary.total_negative_club_nbde }} soit {{ summary.balance_negative_club_nbde / 100 }} €
Progression :
- Ceci correspond à une différence de {{ balance_difference_user / 100 }} € pour les utilisateur⋅rices
- Ceci correspond à une différence de {{ balance_difference_club / 100 }} € pour les clubs
--
Le BDE
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}

View File

@ -2591,12 +2591,12 @@
"note",
"transaction"
],
"query": "[\"OR\", {\"source__balance__gte\": 0}, [\"AND\", [\"NOT\", {\"recurrenttransaction__template__category__name\": \"Alcool\"}], {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}], {\"valid\": false}]",
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]",
"type": "add",
"mask": 2,
"field": "",
"permanent": false,
"description": "Créer une transaction quelconque tant que la source reste positive s'il s'agit d'alcool, sinon au-dessus de -20€"
"description": "Créer une transaction quelconque tant que la source reste au-dessus de -20 €"
}
},
{

View File

@ -5,6 +5,7 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
# from member.models import Club
from note.models import NoteSpecial, Alias
from note_kfet.inputs import AmountInput
@ -114,3 +115,12 @@ class ValidationForm(forms.Form):
required=False,
initial=True,
)
# If the bda exists
# if Club.objects.filter(name__iexact="bda").exists():
# The user can join the bda club at the inscription
# join_bda = forms.BooleanField(
# label=_("Join BDA Club"),
# required=False,
# initial=True,
# )

View File

@ -1,7 +1,6 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
@ -239,8 +238,9 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
for club in Club.objects.filter(add_registration_form=True):
fee += club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
if Club.objects.filter(name__iexact="BDA").exists():
bda = Club.objects.get(name__iexact="BDA")
fee += bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
# ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
@ -249,16 +249,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
def get_form(self, form_class=None):
form = super().get_form(form_class)
# add clubs that are in registration form
for club in Club.objects.filter(add_registration_form=True).order_by("name"):
form_join_club = forms.BooleanField(
label=_("Join %(club)s Club") % {'club': club.name},
required=False,
initial=False,
)
form.fields.update({f"join_{club.id}": form_join_club})
user = self.get_object()
form.fields["last_name"].initial = user.last_name
form.fields["first_name"].initial = user.first_name
@ -276,6 +266,11 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
form.add_error(None, _("An alias with a similar name already exists."))
return self.form_invalid(form)
# Check if BDA exist to propose membership at regisration
bda_exists = False
if Club.objects.filter(name__iexact="BDA").exists():
bda_exists = True
# Get form data
# soge = form.cleaned_data["soge"]
credit_type = form.cleaned_data["credit_type"]
@ -285,9 +280,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
bank = form.cleaned_data["bank"]
join_bde = form.cleaned_data["join_bde"]
join_kfet = form.cleaned_data["join_kfet"]
clubs_registration = Club.objects.filter(add_registration_form=True).order_by("name")
join_clubs = [(club, form.cleaned_data[f"join_{club.id}"]) for club in clubs_registration]
if bda_exists:
join_bda = form.cleaned_data["join_bda"]
# if soge:
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
@ -309,12 +303,11 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
# Add extra fee for the full membership
fee += kfet_fee if join_kfet else 0
clubs_fee = dict()
for club, join_club in join_clubs:
club_fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
# Add extra fee for the club membership
clubs_fee[club] = club_fee
fee += club_fee if join_club else 0
if bda_exists:
bda = Club.objects.get(name__iexact="BDA")
bda_fee = bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
# Add extra fee for the bda membership
fee += bda_fee if join_bda else 0
# # If the bank pays, then we don't credit now. Treasurers will validate the transaction
# # and credit the note later.
@ -394,13 +387,12 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
membership.save()
for club, join_club in join_clubs:
if join_club:
if bda_exists and join_bda:
# Create membership for the user to the BDA starting today
membership = Membership(
club=club,
club=bda,
user=user,
fee=clubs_fee[club],
fee=bda_fee,
)
membership.save()
membership.refresh_from_db()

View File

@ -5,13 +5,13 @@ from django.contrib import admin
from note_kfet.admin import admin_site
from .forms import ProductForm
from .models import Invoice, NoteSummary, Product, RemittanceType, Remittance, SogeCredit
from .models import RemittanceType, Remittance, SogeCredit, Invoice, Product
@admin.register(RemittanceType, site=admin_site)
class RemittanceTypeAdmin(admin.ModelAdmin):
"""
Admin customisation for RemittanceType
Admin customisation for RemiitanceType
"""
list_display = ('note', )
@ -55,19 +55,3 @@ class InvoiceAdmin(admin.ModelAdmin):
"""
list_display = ('object', 'id', 'bde', 'name', 'date', 'acquitted',)
inlines = (ProductInline,)
@admin.register(NoteSummary, site=admin_site)
class NoteSummaryAdmin(admin.ModelAdmin):
"""
Admin customisation for NoteSummary
"""
list_display = (
'date', 'total_positive_user', 'balance_positive_user', 'total_positive_user_bde',
'balance_positive_user_bde', 'total_zero_user', 'total_zero_user_bde', 'total_negative_user',
'balance_negative_user', 'total_negative_user_bde', 'balance_negative_user_bde',
'total_vnegative_user', 'balance_vnegative_user', 'total_vnegative_user_bde',
'balance_vnegative_user_bde', 'total_positive_club', 'balance_positive_club',
'total_positive_club_nbde', 'balance_positive_club_nbde', 'total_zero_club', 'total_zero_club_nbde',
'total_negative_club', 'balance_negative_club', 'total_negative_club_nbde', 'balance_negative_club_nbde',
)

View File

@ -1,49 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-07 12:09
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0008_auto_20240322_0045'),
]
operations = [
migrations.CreateModel(
name='NoteSummary',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=datetime.date.today, verbose_name='Date')),
('total_positive_user', models.PositiveIntegerField(verbose_name='Total positive user')),
('balance_positive_user', models.PositiveIntegerField(verbose_name='Balance positive user')),
('total_positive_user_bde', models.PositiveIntegerField(verbose_name='Total positive user BDE')),
('balance_positive_user_bde', models.PositiveIntegerField(verbose_name='Balance positive user BDE')),
('total_zero_user', models.PositiveIntegerField(verbose_name='Total zero user')),
('total_zero_user_bde', models.PositiveIntegerField(verbose_name='Total zero user BDE')),
('total_negative_user', models.PositiveIntegerField(verbose_name='Total negative user')),
('balance_negative_user', models.PositiveIntegerField(verbose_name='Balance negative user')),
('total_negative_user_bde', models.PositiveIntegerField(verbose_name='Total negative user BDE')),
('balance_negative_user_bde', models.PositiveIntegerField(verbose_name='Balance negative user BDE')),
('total_vnegative_user', models.PositiveIntegerField(verbose_name='Total very negative user')),
('balance_vnegative_user', models.PositiveIntegerField(verbose_name='Balance very negative user')),
('total_vnegative_user_bde', models.PositiveIntegerField(verbose_name='Total very negative user BDE')),
('balance_vnegative_user_bde', models.PositiveIntegerField(verbose_name='Balance very negative user BDE')),
('total_positive_club', models.PositiveIntegerField(verbose_name='Total positive club')),
('balance_positive_club', models.PositiveIntegerField(verbose_name='Balance positive club')),
('total_positive_club_nbde', models.PositiveIntegerField(verbose_name='Total positive club nbde')),
('balance_positive_club_nbde', models.PositiveIntegerField(verbose_name='Balance positive club nbde')),
('total_zero_club', models.PositiveIntegerField(verbose_name='Total zero club')),
('total_zero_club_nbde', models.PositiveIntegerField(verbose_name='Total zero club nbde')),
('total_negative_club', models.PositiveIntegerField(verbose_name='Total negative club')),
('balance_negative_club', models.PositiveIntegerField(verbose_name='Balance negative club')),
('total_negative_club_nbde', models.PositiveIntegerField(verbose_name='Total negative club nbde')),
('balance_negative_club_nbde', models.PositiveIntegerField(verbose_name='Balance negative club nbde')),
],
options={
'verbose_name': 'Summary',
'verbose_name_plural': 'Summaries',
},
),
]

View File

@ -460,117 +460,3 @@ class SogeCredit(models.Model):
self.credit_transaction._force_save = True
self.credit_transaction.save()
super().delete(**kwargs)
class NoteSummary(models.Model):
"""
Summary of every notes
"""
date = models.DateField(
default=date.today,
verbose_name=_("Date"),
)
total_positive_user = models.PositiveIntegerField(
verbose_name=_("Total positive user"),
)
balance_positive_user = models.PositiveIntegerField(
verbose_name=_("Balance positive user"),
)
total_positive_user_bde = models.PositiveIntegerField(
verbose_name=_("Total positive user BDE"),
)
balance_positive_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance positive user BDE"),
)
total_zero_user = models.PositiveIntegerField(
verbose_name=_("Total zero user"),
)
total_zero_user_bde = models.PositiveIntegerField(
verbose_name=_("Total zero user BDE"),
)
total_negative_user = models.PositiveIntegerField(
verbose_name=_("Total negative user"),
)
balance_negative_user = models.PositiveIntegerField(
verbose_name=_("Balance negative user"),
)
total_negative_user_bde = models.PositiveIntegerField(
verbose_name=_("Total negative user BDE"),
)
balance_negative_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance negative user BDE"),
)
total_vnegative_user = models.PositiveIntegerField(
verbose_name=_("Total very negative user"),
)
balance_vnegative_user = models.PositiveIntegerField(
verbose_name=_("Balance very negative user"),
)
total_vnegative_user_bde = models.PositiveIntegerField(
verbose_name=_("Total very negative user BDE"),
)
balance_vnegative_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance very negative user BDE"),
)
total_positive_club = models.PositiveIntegerField(
verbose_name=_("Total positive club"),
)
balance_positive_club = models.PositiveIntegerField(
verbose_name=_("Balance positive club"),
)
total_positive_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total positive club nbde"),
)
balance_positive_club_nbde = models.PositiveIntegerField(
verbose_name=_("Balance positive club nbde"),
)
total_zero_club = models.PositiveIntegerField(
verbose_name=_("Total zero club"),
)
total_zero_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total zero club nbde"),
)
total_negative_club = models.PositiveIntegerField(
verbose_name=_("Total negative club"),
)
balance_negative_club = models.PositiveIntegerField(
verbose_name=_("Balance negative club"),
)
total_negative_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total negative club nbde"),
)
balance_negative_club_nbde = models.PositiveIntegerField(
verbose_name=_("Balance negative club nbde"),
)
class Meta:
verbose_name = _("Summary")
verbose_name_plural = _("Summaries")
def __str__(self):
return "Note summary of {date}".format(date=self.date)

View File

@ -114,7 +114,7 @@ msgstr "Lieu où l'activité est organisée, par exemple la Kfet."
msgid "type"
msgstr "type"
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:318
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:313
#: apps/note/models/notes.py:148 apps/treasury/models.py:293
#: apps/wei/models.py:171 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15
@ -247,7 +247,6 @@ msgid "The validation of the activity is pending."
msgstr "La validation de cette activité est en attente."
#: apps/activity/tables.py:43 apps/treasury/tables.py:107
#: apps/member/templates/member/picture_update.html:18
msgid "Remove"
msgstr "Supprimer"
@ -263,13 +262,13 @@ msgstr "supprimer"
msgid "Type"
msgstr "Type"
#: apps/activity/tables.py:84 apps/member/forms.py:196
#: apps/activity/tables.py:84 apps/member/forms.py:193
#: apps/registration/forms.py:92 apps/treasury/forms.py:131
#: apps/wei/forms/registration.py:104
msgid "Last name"
msgstr "Nom de famille"
#: apps/activity/tables.py:86 apps/member/forms.py:201
#: apps/activity/tables.py:86 apps/member/forms.py:198
#: apps/note/templates/note/transaction_form.html:138
#: apps/registration/forms.py:97 apps/treasury/forms.py:133
#: apps/wei/forms/registration.py:109
@ -401,37 +400,37 @@ msgstr "Inviter"
msgid "Create new activity"
msgstr "Créer une nouvelle activité"
#: apps/activity/views.py:68 note_kfet/templates/base.html:90
#: apps/activity/views.py:67 note_kfet/templates/base.html:90
msgid "Activities"
msgstr "Activités"
#: apps/activity/views.py:108
#: apps/activity/views.py:93
msgid "Activity detail"
msgstr "Détails de l'activité"
#: apps/activity/views.py:128
#: apps/activity/views.py:113
msgid "Update activity"
msgstr "Modifier l'activité"
#: apps/activity/views.py:155
#: apps/activity/views.py:140
msgid "Invite guest to the activity \"{}\""
msgstr "Invitation pour l'activité « {} »"
#: apps/activity/views.py:193
#: apps/activity/views.py:178
msgid "You are not allowed to display the entry interface for this activity."
msgstr ""
"Vous n'êtes pas autorisé·e à afficher l'interface des entrées pour cette "
"activité."
#: apps/activity/views.py:196
#: apps/activity/views.py:181
msgid "This activity does not support activity entries."
msgstr "Cette activité ne requiert pas d'entrées."
#: apps/activity/views.py:199
#: apps/activity/views.py:184
msgid "This activity is closed."
msgstr "Cette activité est fermée."
#: apps/activity/views.py:295
#: apps/activity/views.py:280
msgid "Entry for activity \"{}\""
msgstr "Entrées pour l'activité « {} »"
@ -508,11 +507,11 @@ msgstr "cotisation pour adhérer (normalien·ne élève)"
msgid "membership fee (unpaid students)"
msgstr "cotisation pour adhérer (normalien·ne étudiant·e)"
#: apps/member/admin.py:65 apps/member/models.py:330
#: apps/member/admin.py:65 apps/member/models.py:325
msgid "roles"
msgstr "rôles"
#: apps/member/admin.py:66 apps/member/models.py:344
#: apps/member/admin.py:66 apps/member/models.py:339
msgid "fee"
msgstr "cotisation"
@ -564,29 +563,29 @@ msgid "This image cannot be loaded."
msgstr "Cette image ne peut pas être chargée."
#: apps/member/forms.py:148 apps/member/views.py:102
#: apps/registration/forms.py:34 apps/registration/views.py:276
#: apps/registration/forms.py:34 apps/registration/views.py:266
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
#: apps/member/forms.py:175
#: apps/member/forms.py:172
msgid "Inscription paid by Société Générale"
msgstr "Inscription payée par la Société générale"
#: apps/member/forms.py:177
#: apps/member/forms.py:174
msgid "Check this case if the Société Générale paid the inscription."
msgstr "Cochez cette case si la Société Générale a payé l'inscription."
#: apps/member/forms.py:182 apps/registration/forms.py:79
#: apps/member/forms.py:179 apps/registration/forms.py:79
#: apps/wei/forms/registration.py:91
msgid "Credit type"
msgstr "Type de rechargement"
#: apps/member/forms.py:183 apps/registration/forms.py:80
#: apps/member/forms.py:180 apps/registration/forms.py:80
#: apps/wei/forms/registration.py:92
msgid "No credit"
msgstr "Pas de rechargement"
#: apps/member/forms.py:185
#: apps/member/forms.py:182
msgid "You can credit the note of the user."
msgstr "Vous pouvez créditer la note de l'utilisateur·ice avant l'adhésion."
@ -595,17 +594,17 @@ msgstr "Vous pouvez créditer la note de l'utilisateur·ice avant l'adhésion."
msgid "Credit amount"
msgstr "Montant à créditer"
#: apps/member/forms.py:206 apps/note/templates/note/transaction_form.html:144
#: apps/member/forms.py:203 apps/note/templates/note/transaction_form.html:144
#: apps/registration/forms.py:102 apps/treasury/forms.py:135
#: apps/wei/forms/registration.py:114
msgid "Bank"
msgstr "Banque"
#: apps/member/forms.py:233
#: apps/member/forms.py:230
msgid "User"
msgstr "Utilisateur·ice"
#: apps/member/forms.py:247
#: apps/member/forms.py:244
msgid "Roles"
msgstr "Rôles"
@ -853,50 +852,46 @@ msgstr ""
"Date maximale d'une fin d'adhésion, après laquelle les adhérent·e·s doivent la "
"renouveler."
#: apps/member/models.py:263
msgid "add to registration form"
msgstr "ajouter au formulaire d'inscription"
#: apps/member/models.py:268 apps/member/models.py:324
#: apps/member/models.py:263 apps/member/models.py:319
#: apps/note/models/notes.py:176
msgid "club"
msgstr "club"
#: apps/member/models.py:269
#: apps/member/models.py:264
msgid "clubs"
msgstr "clubs"
#: apps/member/models.py:335
#: apps/member/models.py:330
msgid "membership starts on"
msgstr "l'adhésion commence le"
#: apps/member/models.py:339
#: apps/member/models.py:334
msgid "membership ends on"
msgstr "l'adhésion finit le"
#: apps/member/models.py:348 apps/note/models/transactions.py:385
#: apps/member/models.py:343 apps/note/models/transactions.py:385
msgid "membership"
msgstr "adhésion"
#: apps/member/models.py:349
#: apps/member/models.py:344
msgid "memberships"
msgstr "adhésions"
#: apps/member/models.py:353
#: apps/member/models.py:348
#, python-brace-format
msgid "Membership of {user} for the club {club}"
msgstr "Adhésion de {user} pour le club {club}"
#: apps/member/models.py:372
#: apps/member/models.py:367
#, python-brace-format
msgid "The role {role} does not apply to the club {club}."
msgstr "Le rôle {role} ne s'applique pas au club {club}."
#: apps/member/models.py:381 apps/member/views.py:712
#: apps/member/models.py:376 apps/member/views.py:712
msgid "User is already a member of the club"
msgstr "L'utilisateur·ice est déjà membre du club"
#: apps/member/models.py:393 apps/member/views.py:721
#: apps/member/models.py:388 apps/member/views.py:721
msgid "User is not a member of the parent club"
msgstr "L'utilisateur·ice n'est pas membre du club parent"
@ -1158,11 +1153,11 @@ msgstr "Introspection :"
msgid "Show my applications"
msgstr "Voir mes applications"
#: apps/member/templates/member/picture_update.html:38
#: apps/member/templates/member/picture_update.html:35
msgid "Nevermind"
msgstr "Annuler"
#: apps/member/templates/member/picture_update.html:39
#: apps/member/templates/member/picture_update.html:36
msgid "Crop and upload"
msgstr "Recadrer et envoyer"
@ -1701,8 +1696,6 @@ msgstr "Historique des transactions récentes"
#: apps/note/templates/note/mails/negative_balance.txt:25
#: apps/note/templates/note/mails/negative_notes_report.html:46
#: apps/note/templates/note/mails/negative_notes_report.txt:13
#: apps/note/templates/note/mails/summary_notes_report.html:62
#: apps/note/templates/note/mails/summary_ntoes_report.txt:33
#: apps/note/templates/note/mails/weekly_report.html:51
#: apps/note/templates/note/mails/weekly_report.txt:32
#: apps/registration/templates/registration/mails/email_validation_email.html:40
@ -2190,23 +2183,18 @@ msgstr "Utilisateur·ice·s en attente d'inscription"
msgid "Registration detail"
msgstr "Détails de l'inscription"
#: apps/registration/views.py:256
#, python-format
msgid "Join %(club)s Club"
msgstr "Adhérer au club %(club)s"
#: apps/registration/views.py:299
#: apps/registration/views.py:293
msgid "You must join the BDE."
msgstr "Vous devez adhérer au BDE."
#: apps/registration/views.py:330
#: apps/registration/views.py:323
msgid ""
"The entered amount is not enough for the memberships, should be at least {}"
msgstr ""
"Le montant crédité est trop faible pour adhérer, il doit être au minimum de "
"{}"
#: apps/registration/views.py:425
#: apps/registration/views.py:417
msgid "Invalidate pre-registration"
msgstr "Invalider l'inscription"
@ -2264,7 +2252,6 @@ msgid "Address"
msgstr "Adresse"
#: apps/treasury/models.py:69 apps/treasury/models.py:202
#: apps/treasury/models.py:472
msgid "Date"
msgstr "Date"
@ -2383,102 +2370,6 @@ msgstr ""
"Cet·te utilisateur·ice n'a pas assez d'argent pour payer les adhésions avec sa "
"note. Merci de lui demander de recharger sa note avant d'invalider ce crédit."
#: apps/treasury/models.py:476
msgid "Total positive user"
msgstr "Nombre d'utilisateur⋅rices en positif"
#: apps/treasury/models.py:480
msgid "Balance positive user"
msgstr "Solde des utilisateur⋅rices en positif"
#: apps/treasury/models.py:484
msgid "Total positive user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en positif"
#: apps/treasury/models.py:488
msgid "Balance positive user BDE"
msgstr "Solde des adhérent⋅es au BDE en positif"
#: apps/treasury/models.py:492
msgid "Total zero user"
msgstr "Nombre d'utilisateur⋅rices à zéro"
#: apps/treasury/models.py:496
msgid "Total zero user BDE"
msgstr "Nombre d'adhérent⋅es au BDE à zéro"
#: apps/treasury/models.py:500
msgid "Total negative user"
msgstr "Nombre d'utilisateur⋅rices en négatif"
#: apps/treasury/models.py:504
msgid "Balance negative user"
msgstr "Solde des utilisateur⋅rices en négatif"
#: apps/treasury/models.py:508
msgid "Total negative user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en négatif"
#: apps/treasury/models.py:512
msgid "Balance negative user BDE"
msgstr "Solde des adhérent⋅es au BDE en négatif"
#: apps/treasury/models.py:516
msgid "Total very negative user"
msgstr "Nombre d'utilisateur⋅rices en négatif sévère"
#: apps/treasury/models.py:520
msgid "Balance very negative user"
msgstr "Solde des utilisateur⋅rices en négatif sévère"
#: apps/treasury/models.py:524
msgid "Total very negative user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en négatif sévère"
#: apps/treasury/models.py:528
msgid "Balance very negative user BDE"
msgstr "Solde des adhérent⋅es au BDE en négatif sévère"
#: apps/treasury/models.py:532
msgid "Total positive club"
msgstr "Nombre de clubs en positif"
#: apps/treasury/models.py:536
msgid "Balance positive club"
msgstr "Solde des clubs en positif"
#: apps/treasury/models.py:540
msgid "Total positive club nbde"
msgstr "Nombre de clubs non-BDE en positif"
#: apps/treasury/models.py:544
msgid "Balance positive club nbde"
msgstr "Solde des clubs non-BDE en positif"
#: apps/treasury/models.py:548
msgid "Total zero club"
msgstr "Nombre de clubs à zéro"
#: apps/treasury/models.py:552
msgid "Total zero club nbde"
msgstr "Nombre de clubs non-BDE à zéro"
#: apps/treasury/models.py:556
msgid "Total negative club"
msgstr "Nombre de clubs en négatif"
#: apps/treasury/models.py:560
msgid "Balance negative club"
msgstr "Solde des clubs en négatif"
#: apps/treasury/models.py:564
msgid "Total negative club nbde"
msgstr "Nombre de clubs non-BDE en négatif"
#: apps/treasury/models.py:568
msgid "Balance negative club nbde"
msgstr "Solde des clubs non-BDE en négatif"
#: apps/treasury/tables.py:20
msgid "Invoice #{:d}"
msgstr "Facture n°{:d}"
@ -3729,6 +3620,9 @@ msgstr ""
"d'adhésion. Vous devez également valider votre adresse email en suivant le "
"lien que vous avez reçu."
#~ msgid "Join BDA Club"
#~ msgstr "Adhérer au club BDA"
#, fuzzy
#~| msgid "People having you as a friend"
#~ msgid "You already have that person as a friend"

View File

@ -19,8 +19,6 @@ MAILTO=notekfet2020@lists.crans.org
00 5 * * 2 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --spam --negative-amount 1 -v 0
# Envoyer le rapport mensuel aux trésoriers et respos info
00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --report --add-years 1 -v 0
# Envoyer le recap de tresorerie
00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_summary_notes_report --negative-amount 2000
# Envoyer les rapports aux gens
55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0
# Mettre à jour les boutons mis en avant

View File

@ -77,6 +77,7 @@ INSTALLED_APPS = [
'scripts',
'treasury',
'wei',
'food',
]
MIDDLEWARE = [

View File

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