diff --git a/apps/food/admin.py b/apps/food/admin.py
index d5005adc..2bb1e302 100644
--- a/apps/food/admin.py
+++ b/apps/food/admin.py
@@ -4,28 +4,31 @@
from django.contrib import admin
from note_kfet.admin import admin_site
-from .models import QR_code, BasicFood, TransformedFood, Allergen
+from .models import Allergen, BasicFood, QRCode, TransformedFood
-@admin.register(QR_code, site = admin_site)
-class QR_codeAdmin(admin.ModelAdmin):
+@admin.register(QRCode, site=admin_site)
+class QRCodeAdmin(admin.ModelAdmin):
"""
TEMPORARY
"""
-@admin.register(BasicFood, site = admin_site)
-class Basic_foodAdmin(admin.ModelAdmin):
+
+@admin.register(BasicFood, site=admin_site)
+class BasicFoodAdmin(admin.ModelAdmin):
"""
TEMPORARY
"""
-@admin.register(TransformedFood, site = admin_site)
-class Transformed_foodAdmin(admin.ModelAdmin):
+
+@admin.register(TransformedFood, site=admin_site)
+class TransformedFoodAdmin(admin.ModelAdmin):
"""
TEMPORARY
"""
-@admin.register(Allergen, site = admin_site)
+
+@admin.register(Allergen, site=admin_site)
class AllergenAdmin(admin.ModelAdmin):
"""
TEMPORARY
diff --git a/apps/food/apps.py b/apps/food/apps.py
index a423e253..62ede85f 100644
--- a/apps/food/apps.py
+++ b/apps/food/apps.py
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
+from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
diff --git a/apps/food/fixtures/initial.json b/apps/food/fixtures/initial.json
new file mode 100644
index 00000000..c91a2dec
--- /dev/null
+++ b/apps/food/fixtures/initial.json
@@ -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"
+ }
+ }
+]
diff --git a/apps/food/forms.py b/apps/food/forms.py
index 5855c936..ad308727 100644
--- a/apps/food/forms.py
+++ b/apps/food/forms.py
@@ -7,11 +7,11 @@ 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, DatePickerInput, DateTimePickerInput
+from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
-from .models import QR_code, Allergen, BasicFood, TransformedFood
+from .models import BasicFood, TransformedFood
class BasicFoodForms(forms.ModelForm):
@@ -23,33 +23,25 @@ class BasicFoodForms(forms.ModelForm):
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['name'].required = True
self.fields['owner'].required = True
- self.fields['label'].help_text = _('The lot number must be contained in the picture')
# 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]) + ", ..."
- def clean_dlm_or_dlc(self):
- is_dlc = self.cleaned_data["is_DLC"]
- is_ddm = self.cleaned_data["is_DDM"]
- if is_dlc and is_ddm:
- self.add_error("is_ddm", _("the product cannot be a DLC and a DDM"))
- return is_ddm
class Meta:
model = BasicFood
- fields = ('name', 'owner', 'is_DLC', 'is_DDM', 'expiry_date', 'label')
+ fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens')
widgets = {
"owner": Autocomplete(
- model = Club,
- attrs = {"api_url": "/api/members/club/"},
+ model=Club,
+ attrs={"api_url": "/api/members/club/"},
),
- 'expiry_date': DatePickerInput(),
+ 'expiry_date': DateTimePickerInput(),
}
-
class TransformedFoodForms(forms.ModelForm):
"""
Form for add transformed food
@@ -69,24 +61,13 @@ class TransformedFoodForms(forms.ModelForm):
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',)
+ fields = ('name', 'creation_date', 'owner', 'is_active', 'allergens')
widgets = {
"owner": Autocomplete(
- model = Club,
- attrs = {"api_url": "/api/members/club/"},
+ model=Club,
+ attrs={"api_url": "/api/members/club/"},
),
'creation_date': DateTimePickerInput(),
}
-class AllergenForms(forms.ModelForm):
- """
- Form for allergen
- """
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
-
- class Meta:
- model = Allergen
- exclude = ['basic_food', 'transformed_food']
diff --git a/apps/food/migrations/0001_initial.py b/apps/food/migrations/0001_initial.py
index b51c8a59..7d7d6745 100644
--- a/apps/food/migrations/0001_initial.py
+++ b/apps/food/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 2.2.28 on 2024-05-25 20:32
+# Generated by Django 2.2.28 on 2024-07-03 07:40
from django.db import migrations, models
import django.db.models.deletion
@@ -11,87 +11,62 @@ class Migration(migrations.Migration):
dependencies = [
('member', '0011_profile_vss_charter_read'),
+ ('contenttypes', '0002_remove_content_type_name'),
]
operations = [
- migrations.CreateModel(
- name='BasicFood',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=255, verbose_name='name')),
- ('is_DLC', models.BooleanField(default=False, verbose_name='is DLC')),
- ('is_DDM', models.BooleanField(default=False, verbose_name='is DDM')),
- ('arrival_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='arrival date')),
- ('expiry_date', models.DateTimeField(blank=True, null=True, verbose_name='expiry date')),
- ('label', models.ImageField(max_length=255, upload_to='label/', verbose_name='food label')),
- ('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
- ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')),
- ],
- options={
- 'verbose_name': 'Basic food',
- 'verbose_name_plural': 'Basic foods',
- },
- ),
- migrations.CreateModel(
- name='TransformedFood',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=255, verbose_name='name')),
- ('creation_date', models.DateTimeField(verbose_name='creation date')),
- ('expiry_date', models.DateTimeField(verbose_name='expiry date')),
- ('is_active', models.BooleanField(default=True, verbose_name='is active')),
- ('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
- ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')),
- ('transformed_ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.TransformedFood', verbose_name='transformed ingredient')),
- ],
- options={
- 'verbose_name': 'Transformed food',
- 'verbose_name_plural': 'Transformed foods',
- },
- ),
- migrations.CreateModel(
- name='QR_code',
- 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')),
- ('basic_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.BasicFood', verbose_name='basic food')),
- ('transformed_food_container', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.TransformedFood', verbose_name='transformed food container')),
- ],
- options={
- 'verbose_name': 'QR-code',
- 'verbose_name_plural': 'QR-codes',
- },
- ),
- migrations.AddField(
- model_name='basicfood',
- name='transformed_food',
- field=models.ManyToManyField(blank=True, related_name='BasicFood', to='food.TransformedFood', verbose_name='transformed food'),
- ),
migrations.CreateModel(
name='Allergen',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('gluten', models.BooleanField(default=False, verbose_name='gluten')),
- ('nut', models.BooleanField(default=False, verbose_name='nut')),
- ('crustecean', models.BooleanField(default=False, verbose_name='crustacean')),
- ('celery', models.BooleanField(default=False, verbose_name='celery')),
- ('egg', models.BooleanField(default=False, verbose_name='egg')),
- ('mustard', models.BooleanField(default=False, verbose_name='mustard')),
- ('fish', models.BooleanField(default=False, verbose_name='fish')),
- ('soy', models.BooleanField(default=False, verbose_name='soy')),
- ('milk', models.BooleanField(default=False, verbose_name='milk')),
- ('sulphite', models.BooleanField(default=False, verbose_name='sulphite')),
- ('lupine', models.BooleanField(default=False, verbose_name='lupine')),
- ('mollusc', models.BooleanField(default=False, verbose_name='mollusc')),
- ('groundnut', models.BooleanField(default=False, verbose_name='groundnut')),
- ('sesame', models.BooleanField(default=False, verbose_name='sesame')),
- ('alcohol', models.BooleanField(default=False, verbose_name='alcohol')),
- ('basic_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Allergen', to='food.BasicFood', verbose_name='basic food')),
- ('transformed_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Allergen', to='food.TransformedFood', verbose_name='transformed food')),
+ ('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',),
+ ),
]
diff --git a/apps/food/migrations/0002_auto_20240703_1549.py b/apps/food/migrations/0002_auto_20240703_1549.py
new file mode 100644
index 00000000..fd3732c2
--- /dev/null
+++ b/apps/food/migrations/0002_auto_20240703_1549.py
@@ -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',
+ },
+ ),
+ ]
diff --git a/apps/food/models.py b/apps/food/models.py
index 44a9f595..367e47f9 100644
--- a/apps/food/models.py
+++ b/apps/food/models.py
@@ -1,17 +1,11 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-from datetime import date
-
-from django.conf import settings
-from django.contrib.auth.models import User
-from django.core.exceptions import ValidationError
-from django.core.validators import MinValueValidator
from django.db import models, transaction
-from django.db.models import Q
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
@@ -20,30 +14,21 @@ from member.models import Club
# - check on transformed food
#################################################################
-class QR_code(models.Model):
+
+class QRCode(models.Model):
"""
- An QR_code model
+ An QRCode model
"""
qr_code_number = models.PositiveIntegerField(
verbose_name=_("QR-code number"),
)
- transformed_food_container = models.ForeignKey(
- 'TransformedFood',
- on_delete = models.PROTECT,
- related_name = 'QR_code',
- null = True,
- blank = True,
- verbose_name = _('transformed food container'),
- )
-
- basic_food = models.ForeignKey(
- 'BasicFood',
- on_delete = models.PROTECT,
- related_name = 'QR_code',
- null = True,
- blank = True,
- verbose_name = _('basic food'),
+ food_container = models.ForeignKey(
+ 'Food',
+ on_delete=models.PROTECT,
+ related_name='QR_code',
+ unique=True,
+ verbose_name=_('food container'),
)
class Meta:
@@ -53,102 +38,14 @@ class QR_code(models.Model):
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
"""
-
- gluten = models.BooleanField(
- default = False,
- verbose_name = _('gluten'),
- )
-
- nut = models.BooleanField(
- default = False,
- verbose_name = _('nut'),
- )
-
- crustecean = models.BooleanField(
- default = False,
- verbose_name = _('crustacean'),
- )
-
- celery = models.BooleanField(
- default = False,
- verbose_name = _('celery'),
- )
-
- egg = models.BooleanField(
- default = False,
- verbose_name = _('egg'),
- )
-
- mustard = models.BooleanField(
- default = False,
- verbose_name = _('mustard'),
- )
-
- fish = models.BooleanField(
- default = False,
- verbose_name = _('fish'),
- )
-
- soy = models.BooleanField(
- default = False,
- verbose_name = _('soy'),
- )
-
- milk = models.BooleanField(
- default = False,
- verbose_name = _('milk'),
- )
-
- sulphite = models.BooleanField(
- default = False,
- verbose_name = _('sulphite'),
- )
-
- lupine = models.BooleanField(
- default = False,
- verbose_name = _('lupine'),
- )
-
- mollusc = models.BooleanField(
- default = False,
- verbose_name = _('mollusc'),
- )
-
- groundnut = models.BooleanField(
- default = False,
- verbose_name = _('groundnut'),
- )
-
- sesame = models.BooleanField(
- default = False,
- verbose_name = _('sesame'),
- )
-
- alcohol = models.BooleanField(
- default = False,
- verbose_name = _('alcohol'),
- )
-
- transformed_food = models.ForeignKey(
- 'TransformedFood',
- on_delete = models.CASCADE,
- related_name = 'Allergen',
- blank = True,
- null = True,
- verbose_name = _('transformed food'),
- )
-
- basic_food = models.ForeignKey(
- 'BasicFood',
- on_delete = models.CASCADE,
- related_name = 'Allergen',
- blank = True,
- null = True,
- verbose_name = _('basic food'),
+ name = models.CharField(
+ verbose_name=_('name'),
+ max_length=255,
)
class Meta:
@@ -156,134 +53,101 @@ class Allergen(models.Model):
verbose_name_plural = _('Allergens')
def __str__(self):
- return _('Allergens of #{id}').format(id=self.id)
-
-
+ return self.name
-class BasicFood(models.Model):
- """
- Food which has been directly buy on supermarket
- """
+class Food(PolymorphicModel):
name = models.CharField(
verbose_name=_('name'),
max_length=255,
)
- is_DLC = models.BooleanField(
- verbose_name=_("is DLC"),
- default=False,
- )
-
- is_DDM = models.BooleanField(
- verbose_name=_("is DDM"),
- default=False,
- )
-
- arrival_date = models.DateTimeField(
- verbose_name=_('arrival date'),
- default=timezone.now,
- )
-
- expiry_date = models.DateTimeField(
- verbose_name=_('expiry date'),
- blank=True,
- null = True,
- )
-
owner = models.ForeignKey(
Club,
on_delete=models.PROTECT,
- related_name= '+',
+ related_name='+',
verbose_name=_('owner'),
)
- label = models.ImageField(
- verbose_name=_('food label'),
- max_length=255,
- blank=False,
- null=False,
- upload_to='label/',
- )
-
- was_eaten = models.BooleanField(
- verbose_name=_('was eaten'),
- default = False,
- )
-
- transformed_food = models.ManyToManyField(
- 'TransformedFood',
- related_name= 'BasicFood',
- blank = True,
- verbose_name = _('transformed food'),
- )
-
-
- class Meta:
- verbose_name=_('Basic food')
- verbose_name_plural=_('Basic foods')
-
- def __str__(self):
- return self.name
-
- @transaction.atomic
- def save(self, force_insert=False, force_update=False, using= None, update_fields=None):
- # Check if is_DLC and is DDM are not both True
- if self.is_DLC and self.is_DDM:
- raise ValidationError("The product cannot be a DLC and a DDM")
- return super().save(force_insert, force_update, using, update_fields)
-
-
-class TransformedFood(models.Model):
- """
- Transformed food are a mix between basic food and meal
- """
- name = models.CharField(
- max_length = 255,
- verbose_name =_('name'),
- )
-
- creation_date = models.DateTimeField(
- verbose_name =_('creation date'),
+ allergens = models.ManyToManyField(
+ Allergen,
+ blank=True,
+ verbose_name=_('allergen'),
)
expiry_date = models.DateTimeField(
- verbose_name =_('expiry date'),
- )
-
- owner = models.ForeignKey(
- Club,
- on_delete = models.PROTECT,
- related_name = '+',
- verbose_name =_('owner'),
- )
-
- transformed_ingredient = models.ManyToManyField(
- "self",
- blank = True,
- symmetrical = False,
- related_name = 'transformed_ingredient_inv',
- verbose_name = _('transformed ingredient'),
- )
-
- is_active = models.BooleanField(
- default = True,
- verbose_name = _('is active'),
+ verbose_name=_('expiry date'),
)
was_eaten = models.BooleanField(
- default = False,
- verbose_name = _('was eaten'),
+ default=False,
+ verbose_name=_('was eaten'),
)
-
- class Meta:
- verbose_name = _('Transformed food')
- verbose_name_plural = _('Transformed foods')
-
def __str__(self):
return self.name
@transaction.atomic
- def save(self, force_insert=False, force_update=False, using= None, update_fields=None):
+ 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')
diff --git a/apps/food/static/food/js/food.js b/apps/food/static/food/js/food.js
new file mode 100644
index 00000000..509d9b48
--- /dev/null
+++ b/apps/food/static/food/js/food.js
@@ -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('' + gettext('This field is required and must contain a decimal positive number.') + '')
+ error = true
+ }
+
+ const amount = Math.round(100 * amount_field.val())
+ if (amount > 2147483647) {
+ amount_field.addClass('is-invalid')
+ $('#amount-required').html('' + gettext('The amount must stay under 21,474,836.47 €.') + '')
+ error = true
+ }
+
+ if (!reason_field.val() && $('#type_transfer').is(':checked')) {
+ reason_field.addClass('is-invalid')
+ $('#reason-required').html('' + gettext('This field is required.') + '')
+ 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
+ })
+ }
+})
diff --git a/apps/food/templates/food/basic_food_form.html b/apps/food/templates/food/basic_food_form.html
index 43cf1c43..dbfb49e3 100644
--- a/apps/food/templates/food/basic_food_form.html
+++ b/apps/food/templates/food/basic_food_form.html
@@ -10,11 +10,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
HTML not finished
{{ title }}
-
name : {{ food.name }}
+ Update +