mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 01:48:21 +02:00
Compare commits
3 Commits
6f67d2c629
...
summary_no
Author | SHA1 | Date | |
---|---|---|---|
5a0fe7a6f0 | |||
eda8460014 | |||
15c71ad31a |
@ -1,37 +0,0 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.db import transaction
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .models import Allergen, BasicFood, QRCode, TransformedFood
|
||||
|
||||
|
||||
@admin.register(QRCode, site=admin_site)
|
||||
class QRCodeAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(BasicFood, site=admin_site)
|
||||
class BasicFoodAdmin(admin.ModelAdmin):
|
||||
@transaction.atomic
|
||||
def save_related(self, *args, **kwargs):
|
||||
ans = super().save_related(*args, **kwargs)
|
||||
args[1].instance.update()
|
||||
return ans
|
||||
|
||||
|
||||
@admin.register(TransformedFood, site=admin_site)
|
||||
class TransformedFoodAdmin(admin.ModelAdmin):
|
||||
exclude = ["allergens", "expiry_date"]
|
||||
|
||||
@transaction.atomic
|
||||
def save_related(self, request, form, *args, **kwargs):
|
||||
super().save_related(request, form, *args, **kwargs)
|
||||
form.instance.update()
|
||||
|
||||
|
||||
@admin.register(Allergen, site=admin_site)
|
||||
class AllergenAdmin(admin.ModelAdmin):
|
||||
pass
|
@ -1,11 +0,0 @@
|
||||
# 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')
|
@ -1,107 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
@ -1,99 +0,0 @@
|
||||
# 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, QRCode, TransformedFood
|
||||
|
||||
|
||||
class AddIngredientForms(forms.ModelForm):
|
||||
"""
|
||||
Form for add an ingredient
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter(is_ready=False)
|
||||
|
||||
class Meta:
|
||||
model = TransformedFood
|
||||
fields = ('ingredient',)
|
||||
|
||||
|
||||
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 QRCodeForms(forms.ModelForm):
|
||||
"""
|
||||
Form for create QRCode
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(is_ready=False)
|
||||
|
||||
class Meta:
|
||||
model = QRCode
|
||||
fields = ('food_container',)
|
||||
|
||||
|
||||
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', 'shelf_life')
|
||||
widgets = {
|
||||
"owner": Autocomplete(
|
||||
model=Club,
|
||||
attrs={"api_url": "/api/members/club/"},
|
||||
),
|
||||
'creation_date': DateTimePickerInput(),
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
# Generated by Django 2.2.28 on 2024-07-05 08:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('member', '0011_profile_vss_charter_read'),
|
||||
]
|
||||
|
||||
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, 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')),
|
||||
('is_ready', models.BooleanField(default=False, verbose_name='is ready')),
|
||||
('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(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='QRCode',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('qr_code_number', models.PositiveIntegerField(unique=True, verbose_name='QR-code number')),
|
||||
('food_container', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.Food', verbose_name='food container')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'QR-code',
|
||||
'verbose_name_plural': 'QR-codes',
|
||||
},
|
||||
),
|
||||
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',),
|
||||
),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
# Generated by Django 2.2.28 on 2024-07-06 20:37
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('food', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transformedfood',
|
||||
name='shelf_life',
|
||||
field=models.DurationField(default=datetime.timedelta(days=3), verbose_name='shelf life'),
|
||||
),
|
||||
]
|
@ -1,217 +0,0 @@
|
||||
# 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 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
|
||||
|
||||
|
||||
class QRCode(models.Model):
|
||||
"""
|
||||
An QRCode model
|
||||
"""
|
||||
qr_code_number = models.PositiveIntegerField(
|
||||
verbose_name=_("QR-code number"),
|
||||
unique=True,
|
||||
)
|
||||
|
||||
food_container = models.OneToOneField(
|
||||
'Food',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='QR_code',
|
||||
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'),
|
||||
null=False,
|
||||
)
|
||||
|
||||
was_eaten = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('was eaten'),
|
||||
)
|
||||
|
||||
is_ready = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('is ready'),
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# label = models.ImageField(
|
||||
# verbose_name=_('food label'),
|
||||
# max_length=255,
|
||||
# blank=False,
|
||||
# null=False,
|
||||
# upload_to='label/',
|
||||
# )
|
||||
|
||||
@transaction.atomic
|
||||
def update_allergens(self):
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
parent.update_allergens()
|
||||
|
||||
@transaction.atomic
|
||||
def update_expiry_date(self):
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
parent.update_expiry_date()
|
||||
|
||||
@transaction.atomic
|
||||
def update(self):
|
||||
self.update_allergens()
|
||||
self.update_expiry_date()
|
||||
|
||||
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'),
|
||||
)
|
||||
|
||||
# Without microbiological analyzes, the storage time is 3 days
|
||||
shelf_life = models.DurationField(
|
||||
verbose_name=_("shelf life"),
|
||||
default=timedelta(days=3),
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def update_allergens(self):
|
||||
# When allergens are changed, simply update the parents' allergens
|
||||
old_allergens = list(self.allergens.all())
|
||||
self.allergens.clear()
|
||||
for ingredient in self.ingredient.iterator():
|
||||
self.allergens.set(self.allergens.union(ingredient.allergens.all()))
|
||||
|
||||
if old_allergens == list(self.allergens.all()):
|
||||
return
|
||||
super().save()
|
||||
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
parent.update_allergens()
|
||||
|
||||
@transaction.atomic
|
||||
def update_expiry_date(self):
|
||||
# When expiry_date is changed, simply update the parents' expiry_date
|
||||
old_expiry_date = self.expiry_date
|
||||
self.expiry_date = self.creation_date + self.shelf_life
|
||||
for ingredient in self.ingredient.iterator():
|
||||
self.expiry_date = min(self.expiry_date, ingredient.expiry_date)
|
||||
|
||||
if old_expiry_date == self.expiry_date:
|
||||
return
|
||||
super().save()
|
||||
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
parent.update_expiry_date()
|
||||
|
||||
@transaction.atomic
|
||||
def update(self):
|
||||
self.update_allergens()
|
||||
self.update_expiry_date()
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Transformed food')
|
||||
verbose_name_plural = _('Transformed foods')
|
@ -1,19 +0,0 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import A
|
||||
|
||||
from .models import TransformedFood
|
||||
|
||||
|
||||
class TransformedFoodTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'food:food_view',
|
||||
args=[A('pk'), ],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TransformedFood
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', )
|
@ -1,21 +0,0 @@
|
||||
{% 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 %}
|
@ -1,21 +0,0 @@
|
||||
{% 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 %}
|
@ -1,27 +0,0 @@
|
||||
{% 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>
|
||||
<p>arrival_date : {{ food.arrival_date }}</p>
|
||||
<p>expiry_date : {{ food.expiry_date }}</p>
|
||||
<p>allergens :</p>
|
||||
<ul>
|
||||
{% for allergen in food.allergens.iterator %}
|
||||
<li>{{ allergen.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url "food:basic_update" pk=food.pk %}">Update</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,21 +0,0 @@
|
||||
{% 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 %}
|
@ -1,24 +0,0 @@
|
||||
{% 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">
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:qrcode_basic_create" slug=slug %}" data-turbolinks="false">
|
||||
New basic food
|
||||
</a>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,24 +0,0 @@
|
||||
{% 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 %}
|
||||
<a href="{% url "food:add_ingredient" pk=qrcode.food_container.pk %}">Add the ingrdient</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,21 +0,0 @@
|
||||
{% 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 %}
|
@ -1,33 +0,0 @@
|
||||
{% 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>
|
||||
<p>creation_date : {{ food.creation_date }}</p>
|
||||
<p>expiry_date : {{ food.expiry_date }}</p>
|
||||
<p>allergens :</p>
|
||||
<ul>
|
||||
{% for allergen in food.allergens.iterator %}
|
||||
<li>{{ allergen.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>ingredients :</p>
|
||||
<ul>
|
||||
{% for ingredient in food.ingredient.iterator %}
|
||||
<li><a href="{% url "food:food_view" pk=ingredient.pk %}">{{ ingredient.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url "food:transformed_update" pk=food.pk %}">Update</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,24 +0,0 @@
|
||||
{% 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="card bg-light mb-3">
|
||||
<div class="card-footer">
|
||||
<a class="btn btn-sm btn-success" href="{% url 'food:transformed_create' %}" data-turbolinks="false">
|
||||
New transformed food
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="card-header text-center">
|
||||
In preparation
|
||||
</h3>
|
||||
{% render_table table %}
|
||||
<h3 class="card-header text-center">
|
||||
Open
|
||||
</h3>
|
||||
{% render_table open_table %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,3 +0,0 @@
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -1,24 +0,0 @@
|
||||
# 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('', views.TransfomedListView.as_view(), name='food_list'),
|
||||
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('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'),
|
||||
|
||||
path('add/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
|
||||
]
|
@ -1,278 +0,0 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.db import transaction
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django_tables2.views import SingleTableView
|
||||
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 AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms
|
||||
from .models import BasicFood, Food, QRCode, TransformedFood
|
||||
from .tables import TransformedFoodTable
|
||||
|
||||
|
||||
class AddIngredientView(ProtectQuerysetMixin, UpdateView):
|
||||
"""
|
||||
A view to add an ingredient
|
||||
"""
|
||||
model = Food
|
||||
template_name = 'food/add_ingredient_form.html'
|
||||
extra_context = {"title": _("Add the ingredient")}
|
||||
form_class = AddIngredientForms
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["pk"] = self.kwargs["pk"]
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
food = Food.objects.get(pk=self.kwargs['pk'])
|
||||
add_ingredient_form = AddIngredientForms(data=self.request.POST)
|
||||
if not food.is_ready:
|
||||
form.add_error(None, _("The product isn't ready"))
|
||||
return self.form_invalid(form)
|
||||
if not add_ingredient_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Save the aliment and the allergens associed
|
||||
for transformed_pk in self.request.POST.getlist('ingredient'):
|
||||
transformed = TransformedFood.objects.get(pk=transformed_pk)
|
||||
if not transformed.is_ready:
|
||||
transformed.ingredient.add(food)
|
||||
transformed.update()
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse('food:food_list')
|
||||
|
||||
|
||||
class BasicFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
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)
|
||||
|
||||
ans = super().form_valid(form)
|
||||
form.instance.update()
|
||||
return ans
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
self.object.refresh_from_db()
|
||||
return reverse('food:food_view', kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class FoodCreateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
A view to add a new aliment
|
||||
"""
|
||||
template_name = 'food/create_food_form.html'
|
||||
extra_context = {"title": _("Add a new aliment")}
|
||||
|
||||
|
||||
class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
A view to see a food
|
||||
"""
|
||||
model = Food
|
||||
extra_context = {"title": _("Details")}
|
||||
context_object_name = "food"
|
||||
|
||||
|
||||
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 with a qrcode
|
||||
"""
|
||||
model = BasicFood
|
||||
form_class = BasicFoodForms
|
||||
template_name = 'food/basic_food_form.html'
|
||||
extra_context = {"title": _("Add a new basic food with QRCode")}
|
||||
|
||||
@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.is_ready = True
|
||||
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:qrcode_view', kwargs={"slug": self.kwargs['slug']})
|
||||
|
||||
def get_sample_object(self):
|
||||
return BasicFood(
|
||||
name="",
|
||||
expiry_date=timezone.now(),
|
||||
)
|
||||
|
||||
|
||||
class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
"""
|
||||
A view to add a new qrcode
|
||||
"""
|
||||
model = QRCode
|
||||
template_name = 'food/create_qrcode_form.html'
|
||||
form_class = QRCodeForms
|
||||
extra_context = {"title": _("Add a new QRCode")}
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
qrcode = kwargs["slug"]
|
||||
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
|
||||
return HttpResponseRedirect(reverse("food:qrcode_view", kwargs=kwargs))
|
||||
else:
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["slug"] = self.kwargs["slug"]
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
qrcode_food_form = QRCodeForms(data=self.request.POST)
|
||||
if not qrcode_food_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Save the qrcode
|
||||
qrcode = form.save(commit=False)
|
||||
qrcode.qr_code_number = self.kwargs["slug"]
|
||||
qrcode._force_save = True
|
||||
qrcode.save()
|
||||
qrcode.refresh_from_db()
|
||||
|
||||
qrcode.food_container.is_ready = True
|
||||
qrcode.food_container.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
self.object.refresh_from_db()
|
||||
return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
|
||||
|
||||
def get_sample_object(self):
|
||||
return QRCode(
|
||||
qr_code_number=self.kwargs["slug"],
|
||||
)
|
||||
|
||||
|
||||
class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
A view to see a qrcode
|
||||
"""
|
||||
model = QRCode
|
||||
extra_context = {"title": _("QRCode")}
|
||||
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 TransformedFoodFormView(ProtectQuerysetMixin):
|
||||
"""
|
||||
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)
|
||||
transformed_food.expiry_date = transformed_food.creation_date
|
||||
transformed_food._force_save = True
|
||||
transformed_food.save()
|
||||
transformed_food.refresh_from_db()
|
||||
ans = super().form_valid(form)
|
||||
transformed_food.update()
|
||||
return ans
|
||||
|
||||
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 TransfomedListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
Displays not ready TransformedFood
|
||||
"""
|
||||
model = TransformedFood
|
||||
table_class = TransformedFoodTable
|
||||
ordering = ('name',)
|
||||
extra_context = {"title": _("Transformed food")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs)\
|
||||
.filter(is_ready=False)\
|
||||
.distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['open_table'] = TransformedFoodTable(
|
||||
TransformedFood.objects.filter(
|
||||
was_eaten=False,
|
||||
expiry_date__lt=timezone.now()
|
||||
),
|
||||
prefix="open-")
|
||||
return context
|
18
apps/member/migrations/0013_auto_20240807_1409.py
Normal file
18
apps/member/migrations/0013_auto_20240807_1409.py
Normal file
@ -0,0 +1,18 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -18,7 +18,6 @@ def create_special_notes(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('note', '0001_initial'),
|
||||
('logs', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
65
apps/note/templates/note/mails/summary_notes_report.html
Normal file
65
apps/note/templates/note/mails/summary_notes_report.html
Normal file
@ -0,0 +1,65 @@
|
||||
{% 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>
|
33
apps/note/templates/note/mails/summary_notes_report.txt
Normal file
33
apps/note/templates/note/mails/summary_notes_report.txt
Normal file
@ -0,0 +1,33 @@
|
||||
{% 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" %}
|
Submodule apps/scripts updated: f580f9b9e9...f76acb3248
@ -5,13 +5,13 @@ from django.contrib import admin
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .forms import ProductForm
|
||||
from .models import RemittanceType, Remittance, SogeCredit, Invoice, Product
|
||||
from .models import Invoice, NoteSummary, Product, RemittanceType, Remittance, SogeCredit
|
||||
|
||||
|
||||
@admin.register(RemittanceType, site=admin_site)
|
||||
class RemittanceTypeAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for RemiitanceType
|
||||
Admin customisation for RemittanceType
|
||||
"""
|
||||
list_display = ('note', )
|
||||
|
||||
@ -55,3 +55,19 @@ 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',
|
||||
)
|
||||
|
49
apps/treasury/migrations/0009_notesummary.py
Normal file
49
apps/treasury/migrations/0009_notesummary.py
Normal file
@ -0,0 +1,49 @@
|
||||
# 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',
|
||||
},
|
||||
),
|
||||
]
|
@ -460,3 +460,117 @@ 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)
|
||||
|
@ -1,83 +0,0 @@
|
||||
Application Food
|
||||
================
|
||||
|
||||
L'application ``food`` s'occupe de la traçabilité et permet notamment l'obtention de la liste des allergènes.
|
||||
|
||||
Modèles
|
||||
-------
|
||||
|
||||
L'application comporte 5 modèles : Allergen, QRCode, Food, BasicFood, TransformedFood.
|
||||
|
||||
Food
|
||||
~~~~
|
||||
|
||||
Ce modèle est un PolymorphicModel et ne sert uniquement à créer BasicFood et TransformedFood.
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* Nom du produit
|
||||
* Propriétaire (doit-être un Club)
|
||||
* Allergènes (ManyToManyField)
|
||||
* date d'expiration
|
||||
* a été mangé (booléen)
|
||||
* est prêt (booléen)
|
||||
|
||||
BasicFood
|
||||
~~~~~~~~~
|
||||
|
||||
Les BasicFood correspondent aux produits non modifiés à la Kfet. Ils peuvent correspondre à la fois à des produits achetés en magasin ou à des produits Terre à Terre. Ces produits seront les ingrédients de tous les plats préparés et en conséquent sont les seuls produits à nécessité une saisie manuelle des allergènes.
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* Type de date (DLC = date limite de consommation, DDM = date de durabilité minimale)
|
||||
* Date d'arrivée
|
||||
* Champs de Food
|
||||
|
||||
TransformedFood
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Les TransformedFood correspondent aux produits préparés à la Kfet. Ils peuvent être composés de BasicFood et/ou de TransformedFood. La date d'expiration et les allergènes sont automatiquement mis à jour par update (qui doit être exécuté après modification des ingrédients dans les forms par exemple).
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* Durée de consommation (par défaut 3 jours)
|
||||
* Ingrédients (ManyToManyField vers Food)
|
||||
* Date de création
|
||||
* Champs de Food
|
||||
|
||||
Allergen
|
||||
~~~~~~~~
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* Nom
|
||||
|
||||
QRCode
|
||||
~~~~~~
|
||||
|
||||
Le modèle regroupe :
|
||||
|
||||
* nombre (unique, entier positif)
|
||||
* food (OneToOneField vers Food)
|
||||
|
||||
Création de BasicFood
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Un BasicFood a toujours besoin d'un QRCode (depuis l'interface web). Il convient donc de coller le QRCode puis de le scanner et de compléter le formulaire.
|
||||
|
||||
Création de TransformedFood
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Pour créer un TransformedFood, il suffit d'aller dans l'onglet ``traçabilité`` et de cliquer sur l'onglet.
|
||||
|
||||
Ajouter un ingrédient
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Un ingrédient a forcément un QRCode. Il convient donc de scanner le QRCode de l'ingrédient et de sélectionner le produit auquel il doit être ajouté.
|
||||
|
||||
Remarque : Un produit fini doit avoir un QRCode et inversement.
|
||||
|
||||
Terminer un plat
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Il suffit de coller le QRCode sur le plat, de le scanner et de sélectionner le produit.
|
@ -1701,6 +1701,8 @@ 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
|
||||
@ -2262,6 +2264,7 @@ msgid "Address"
|
||||
msgstr "Adresse"
|
||||
|
||||
#: apps/treasury/models.py:69 apps/treasury/models.py:202
|
||||
#: apps/treasury/models.py:472
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
|
||||
@ -2380,6 +2383,102 @@ 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}"
|
||||
|
@ -19,6 +19,8 @@ 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
|
||||
|
@ -69,7 +69,6 @@ INSTALLED_APPS = [
|
||||
# Note apps
|
||||
'api',
|
||||
'activity',
|
||||
'food',
|
||||
'logs',
|
||||
'member',
|
||||
'note',
|
||||
|
@ -66,10 +66,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
{% url 'food:food_list' as url %}
|
||||
<a data-turbolinks="false" class="nav-link" href="{{ url }}">Food</a>
|
||||
</li>
|
||||
{% if user.is_authenticated and user|is_member:"Kfet" %}
|
||||
<li class="nav-item">
|
||||
{% url 'note:transfer' as url %}
|
||||
|
@ -21,7 +21,6 @@ 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')),
|
||||
|
Reference in New Issue
Block a user