diff --git a/Dockerfile b/Dockerfile
index 33649ae3b..78726f92a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -49,7 +49,8 @@ RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install -r requirements.txt
# Switch to the non-privileged user to run the application.
-USER appuser
+# this is commented out because running as root allows to upload files locally
+# USER appuser
# Copy the source code into the container.
COPY . .
diff --git a/api/serializers/declaration.py b/api/serializers/declaration.py
index 6dd9a067f..efe09b7ce 100644
--- a/api/serializers/declaration.py
+++ b/api/serializers/declaration.py
@@ -90,23 +90,34 @@ def update(self, instance, validated_data):
ADDABLE_ELEMENT_FIELDS = (
+ "new",
+ "new_name",
+ "new_description",
"authorization_mode",
"fr_reason",
"fr_details",
"eu_reference_country",
"eu_legal_source",
"eu_details",
- "new_description",
- "new",
- "request_private_notes",
"request_status",
+ "request_private_notes",
)
+DECLARED_ELEMENT_SHARED_FIELDS = ADDABLE_ELEMENT_FIELDS + ("type",)
+
class DeclaredElementNestedField:
+ # DRF ne gère pas automatiquement la création des nested-fields :
+ # https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
def create(self, validated_data):
- # DRF ne gère pas automatiquement la création des nested-fields :
- # https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
+ self._set_element(validated_data)
+ return super().create(validated_data)
+
+ def update(self, instance, validated_data):
+ self._set_element(validated_data)
+ return super().update(instance, validated_data)
+
+ def _set_element(self, validated_data):
element = validated_data.pop(self.nested_field_name, None)
if element:
id = element.get("id")
@@ -119,8 +130,6 @@ def create(self, validated_data):
}
)
- return super().create(validated_data)
-
class DeclaredIngredientCommonSerializer(DeclaredElementNestedField, PrivateFieldsSerializer):
private_fields = ("request_private_notes", "request_status")
@@ -137,17 +146,15 @@ class DeclaredPlantSerializer(DeclaredIngredientCommonSerializer):
class Meta:
model = DeclaredPlant
- fields = ADDABLE_ELEMENT_FIELDS + (
+ fields = DECLARED_ELEMENT_SHARED_FIELDS + (
"id",
"declaration",
"element",
- "new_name",
"active",
"used_part",
- "unit",
"quantity",
+ "unit",
"preparation",
- "type",
)
@@ -160,18 +167,16 @@ class DeclaredMicroorganismSerializer(DeclaredIngredientCommonSerializer):
class Meta:
model = DeclaredMicroorganism
- fields = ADDABLE_ELEMENT_FIELDS + (
+ fields = DECLARED_ELEMENT_SHARED_FIELDS + (
"id",
"declaration",
"element",
- "new_name",
- "new_species",
- "new_genre",
"active",
"activated",
+ "new_species",
+ "new_genre",
"strain",
"quantity",
- "type",
)
@@ -185,16 +190,14 @@ class DeclaredIngredientSerializer(DeclaredIngredientCommonSerializer):
class Meta:
model = DeclaredIngredient
- fields = ADDABLE_ELEMENT_FIELDS + (
+ fields = DECLARED_ELEMENT_SHARED_FIELDS + (
"id",
"declaration",
"element",
- "new_name",
- "new_type",
"active",
+ "new_type",
"quantity",
"unit",
- "type",
)
@@ -207,15 +210,13 @@ class DeclaredSubstanceSerializer(DeclaredIngredientCommonSerializer):
class Meta:
model = DeclaredSubstance
- fields = ADDABLE_ELEMENT_FIELDS + (
+ fields = DECLARED_ELEMENT_SHARED_FIELDS + (
"id",
"declaration",
"element",
- "new_name",
"active",
"quantity",
"unit",
- "type",
)
@@ -295,6 +296,14 @@ class Meta:
read_only_fields = fields
+def add_enum_or_personnalized_value(item, custom_value):
+ if item:
+ if "(à préciser)" not in str(item).lower():
+ return item.name
+ else:
+ return "Autre : " + str(custom_value)
+
+
class OpenDataDeclarationSerializer(serializers.ModelSerializer):
id = serializers.SerializerMethodField()
decision = serializers.SerializerMethodField()
@@ -386,17 +395,17 @@ def get_article_procedure(self, obj):
Unify all types of Articles 15 categories.
If not part of Article 15, then return display name
"""
- article = Declaration.Article(obj.article).label
- if "Article 15" in article:
+ if obj.article in [
+ Declaration.Article.ARTICLE_15,
+ Declaration.Article.ARTICLE_15_WARNING,
+ Declaration.Article.ARTICLE_15_HIGH_RISK_POPULATION,
+ ]:
return "Article 15"
- else:
- return article
+ elif obj.article:
+ return obj.article.label
def get_forme_galenique(self, obj):
- if obj.galenic_formulation:
- return obj.galenic_formulation.name
- else:
- return None
+ return add_enum_or_personnalized_value(obj.galenic_formulation, obj.other_galenic_formulation)
def get_dose_journaliere(self, obj):
return obj.daily_recommended_dose
@@ -408,13 +417,19 @@ def get_mises_en_garde(self, obj):
return obj.warning
def get_objectif_effet(self, obj):
- return [effect.name for effect in obj.effects.all()]
+ effects = []
+ for effect in obj.effects.all():
+ effects.append(add_enum_or_personnalized_value(effect, obj.other_effects))
+ return effects
def get_aromes(self, obj):
return obj.flavor
def get_facteurs_risques(self, obj):
- return [condition.name for condition in obj.conditions_not_recommended.all()]
+ risk_factors = []
+ for risk_factor in obj.conditions_not_recommended.all():
+ risk_factors.append(add_enum_or_personnalized_value(risk_factor, obj.other_effects))
+ return risk_factors
def get_populations_cibles(self, obj):
return [population.name for population in obj.populations.all()]
diff --git a/api/tests/test_declaration.py b/api/tests/test_declaration.py
index 2c8b008d7..9338680f9 100644
--- a/api/tests/test_declaration.py
+++ b/api/tests/test_declaration.py
@@ -38,7 +38,15 @@
VisaRoleFactory,
PlantSynonymFactory,
)
-from data.models import Attachment, Declaration, DeclaredMicroorganism, DeclaredPlant, Snapshot
+from data.models import (
+ Attachment,
+ Declaration,
+ DeclaredMicroorganism,
+ DeclaredPlant,
+ DeclaredSubstance,
+ Snapshot,
+ IngredientType,
+)
from .utils import authenticate
@@ -1335,9 +1343,9 @@ def test_filter_by_article(self):
Les déclarations peuvent être filtrées par article
"""
InstructionRoleFactory(user=authenticate.user)
- art_15 = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
- AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_16)
- AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ANSES_REFERAL)
+ art_15 = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
+ AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_16)
+ AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ANSES_REFERAL)
# Filtrage pour obtenir les déclarations en article 15
url = f"{reverse('api:list_all_declarations')}?article=ART_15"
@@ -1579,7 +1587,7 @@ def test_update_article(self):
art_15.refresh_from_db()
self.assertEqual(art_15.article, Declaration.Article.ARTICLE_16)
self.assertEqual(art_15.calculated_article, Declaration.Article.ARTICLE_15)
- self.assertEqual(art_15.overriden_article, Declaration.Article.ARTICLE_16)
+ self.assertEqual(art_15.overridden_article, Declaration.Article.ARTICLE_16)
@authenticate
def test_update_article_unauthorized(self):
@@ -1924,43 +1932,201 @@ def test_replace_declared_plant(self):
)
self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
declared_plant.refresh_from_db()
+ self.assertEqual(declared_plant.plant, plant)
self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REPLACED)
self.assertFalse(declared_plant.new)
- self.assertEqual(declared_plant.plant, plant)
@authenticate
- def test_cannot_replace_element_different_type(self):
+ def test_replace_inactive_ingredient_with_active(self):
"""
- Pour reduire le scope de changements, temporairement bloque le remplacement d'une demande
- avec un element d'un type different
+ Vérifier que c'est possible de remplacer un ingrédient par un autre
+ Le front est responsable d'envoyer les bonnes valeurs pour `active`, `quantity`, `unit`
+ selon la logique gérée là-bas
"""
InstructionRoleFactory(user=authenticate.user)
declaration = DeclarationFactory()
- declared_plant = DeclaredPlantFactory(declaration=declaration)
+ declared_ingredient = DeclaredIngredientFactory(
+ declaration=declaration,
+ new=True,
+ new_type=IngredientType.NON_ACTIVE_INGREDIENT.label,
+ active=False,
+ quantity=None,
+ unit=None,
+ ingredient=None,
+ )
+ self.assertNotEqual(declared_ingredient.request_status, DeclaredPlant.AddableStatus.REPLACED)
+ active_ingredient = IngredientFactory(ingredient_type=IngredientType.ACTIVE_INGREDIENT)
+ unit = SubstanceUnitFactory()
+
+ response = self.client.post(
+ reverse("api:declared_element_replace", kwargs={"pk": declared_ingredient.id, "type": "other-ingredient"}),
+ {
+ "element": {"id": active_ingredient.id, "type": "other-ingredient"},
+ "additional_fields": {"active": True, "quantity": 20, "unit": unit.id},
+ },
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
+ declared_ingredient.refresh_from_db()
+ self.assertEqual(declared_ingredient.ingredient, active_ingredient)
+ self.assertEqual(declared_ingredient.new_type, IngredientType.NON_ACTIVE_INGREDIENT.label) # pas changé
+ self.assertTrue(declared_ingredient.active)
+ self.assertEqual(declared_ingredient.quantity, 20)
+ self.assertEqual(declared_ingredient.unit, unit)
+ self.assertEqual(declared_ingredient.request_status, DeclaredPlant.AddableStatus.REPLACED)
+ self.assertFalse(declared_ingredient.new)
+
+ @authenticate
+ def test_can_replace_plant_request_with_microorganism(self):
+ """
+ Test de remplacement cross-type : plante vers microorganisme
+ Verifier que les données sont copiées, et les nouvelles données sont sauvegardées.
+ """
+ InstructionRoleFactory(user=authenticate.user)
+
+ declaration = DeclarationFactory()
+ declared_plant = DeclaredPlantFactory(
+ declaration=declaration, new_name="Test plant", new_description="Test description", new=True, quantity=10
+ )
self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REQUESTED)
microorganism = MicroorganismFactory()
response = self.client.post(
reverse("api:declared_element_replace", kwargs={"pk": declared_plant.id, "type": "plant"}),
- {"element": {"id": microorganism.id, "type": "microorganism"}},
+ {
+ "element": {"id": microorganism.id, "type": "microorganism"},
+ "additional_fields": {
+ "strain": "Test strain",
+ "activated": False,
+ "quantity": 90,
+ },
+ },
format="json",
)
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.json())
- declared_plant.refresh_from_db()
- self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REQUESTED)
- self.assertNotEqual(declared_plant.plant, microorganism)
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
+ with self.assertRaises(DeclaredPlant.DoesNotExist):
+ DeclaredPlant.objects.get(id=declared_plant.id)
+
+ self.assertEqual(DeclaredMicroorganism.objects.count(), 1)
+ declared_microorganism = DeclaredMicroorganism.objects.get(declaration=declaration)
+
+ self.assertEqual(declared_microorganism.microorganism, microorganism)
+ self.assertEqual(declared_microorganism.request_status, DeclaredMicroorganism.AddableStatus.REPLACED)
+ self.assertFalse(declared_microorganism.new)
+
+ # est-ce que le nom est copié dans le champ espèce ?
+ self.assertEqual(declared_microorganism.new_species, "Test plant")
+ self.assertEqual(declared_microorganism.new_genre, "")
+ # est-ce que les nouveaux champs sont sauvegardés ?
+ self.assertEqual(declared_microorganism.strain, "Test strain")
+ self.assertEqual(declared_microorganism.activated, False)
+ self.assertEqual(declared_microorganism.quantity, 90)
+ # est-ce que les anciens champs sont sauvegardés ?
+ self.assertEqual(declared_microorganism.new_description, "Test description")
+
+ @authenticate
+ def test_can_replace_microorganism_request_with_plant(self):
+ """
+ Test de remplacement cross-type : microoganisme vers plante
+ Verifier que les données sont copiées, et les nouvelles données sont sauvegardées.
+ """
+ InstructionRoleFactory(user=authenticate.user)
+
+ declaration = DeclarationFactory()
+ declared_microorganism = DeclaredMicroorganismFactory(
+ declaration=declaration,
+ new_species="test",
+ new_genre="testing",
+ new_description="Test description",
+ new=True,
+ quantity=10,
+ )
+ self.assertEqual(declared_microorganism.request_status, DeclaredMicroorganism.AddableStatus.REQUESTED)
+ plant = PlantFactory()
+ used_part = PlantPartFactory()
+ unit = SubstanceUnitFactory()
+
+ response = self.client.post(
+ reverse("api:declared_element_replace", kwargs={"pk": declared_microorganism.id, "type": "microorganism"}),
+ {
+ "element": {"id": plant.id, "type": "plant"},
+ "additional_fields": {
+ "used_part": used_part.id,
+ "unit": unit.id,
+ "quantity": 90,
+ },
+ },
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
+ with self.assertRaises(DeclaredMicroorganism.DoesNotExist):
+ DeclaredMicroorganism.objects.get(id=declared_microorganism.id)
+
+ self.assertEqual(DeclaredPlant.objects.count(), 1)
+ declared_plant = DeclaredPlant.objects.get(declaration=declaration)
+
+ self.assertEqual(declared_plant.plant, plant)
+ self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REPLACED)
+ self.assertFalse(declared_plant.new)
+
+ # est-ce que l'espèce + genre sont copiés dans le champ nom ?
+ self.assertEqual(declared_plant.new_name, "test testing")
+ # est-ce que les nouveaux champs sont sauvegardés ?
+ self.assertEqual(declared_plant.used_part, used_part)
+ self.assertEqual(declared_plant.unit, unit)
+ self.assertEqual(declared_plant.quantity, 90)
+ # est-ce que les anciens champs sont sauvegardés ?
+ self.assertEqual(declared_plant.new_description, "Test description")
+
+ @authenticate
+ def test_can_replace_substance_request_with_plant(self):
+ """
+ Test de remplacement cross-type : microoganisme vers plante
+ Verifier que les données complexes, comme unit, sont bien gardées si elles ne sont pas spécifiées.
+ """
+ InstructionRoleFactory(user=authenticate.user)
+
+ declaration = DeclarationFactory()
+ unit = SubstanceUnitFactory()
+ declared_substance = DeclaredSubstanceFactory(
+ declaration=declaration, new_description="Test description", new=True, unit=unit
+ )
+ self.assertEqual(declared_substance.request_status, DeclaredSubstance.AddableStatus.REQUESTED)
+ plant = PlantFactory()
+
+ response = self.client.post(
+ reverse("api:declared_element_replace", kwargs={"pk": declared_substance.id, "type": "substance"}),
+ {
+ "element": {"id": plant.id, "type": "plant"},
+ },
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
+ with self.assertRaises(DeclaredSubstance.DoesNotExist):
+ DeclaredSubstance.objects.get(id=declared_substance.id)
+
+ self.assertEqual(DeclaredPlant.objects.count(), 1)
+ declared_plant = DeclaredPlant.objects.get(declaration=declaration)
+
+ self.assertEqual(declared_plant.plant, plant)
+ self.assertEqual(declared_plant.request_status, DeclaredPlant.AddableStatus.REPLACED)
+ self.assertFalse(declared_plant.new)
+
+ # est-ce que les anciens champs sont sauvegardés ?
+ self.assertEqual(declared_plant.new_description, "Test description")
+ self.assertEqual(declared_plant.unit, unit)
@authenticate
def test_can_add_synonym_on_replace(self):
"""
- C'est possible d'envoyer une liste avec un nouvel element pour
- ajouter un synonyme et laisser des synonymes existantes non-modifiées
+ C'est possible d'envoyer une liste avec un element pour ajouter un synonyme
+ et laisser des synonymes existantes non-modifiées
"""
InstructionRoleFactory(user=authenticate.user)
declaration = DeclarationFactory()
- declared_plant = DeclaredPlantFactory(declaration=declaration, new=True)
+ declared_plant = DeclaredPlantFactory(declaration=declaration)
plant = PlantFactory()
synonym = PlantSynonymFactory.create(name="Eucalyptus Plant", standard_name=plant)
@@ -1982,6 +2148,10 @@ def test_can_add_synonym_on_replace(self):
@authenticate
def test_cannot_provide_synonym_with_no_name(self):
+ """
+ Si on donne un nom vide, ignore-le.
+ Si on ne donne pas de nom, envoie un 400.
+ """
InstructionRoleFactory(user=authenticate.user)
declaration = DeclarationFactory()
@@ -2016,13 +2186,12 @@ def test_cannot_provide_synonym_with_no_name(self):
@authenticate
def test_cannot_add_duplicate_synonyms(self):
"""
- C'est possible d'envoyer une liste avec un nouvel element pour
- ajouter un synonyme et laisser des synonymes existantes non-modifiées
+ Ignorer les synonymes qui matchent des synonymes existantes
"""
InstructionRoleFactory(user=authenticate.user)
declaration = DeclarationFactory()
- declared_plant = DeclaredPlantFactory(declaration=declaration, new=True)
+ declared_plant = DeclaredPlantFactory(declaration=declaration)
plant = PlantFactory()
synonym = PlantSynonymFactory.create(name="Eucalyptus Plant", standard_name=plant)
@@ -2039,3 +2208,93 @@ def test_cannot_add_duplicate_synonyms(self):
self.assertEqual(plant.plantsynonym_set.count(), 2)
self.assertIsNotNone(plant.plantsynonym_set.get(name="New synonym"))
self.assertEqual(plant.plantsynonym_set.get(id=synonym.id).name, synonym.name)
+
+ @authenticate
+ def test_elements_unchanged_on_replace_fail(self):
+ """
+ Si on donne des mauvaises données à sauvegarder, annule tout l'action, y compris la MAJ des synonymes
+ """
+ InstructionRoleFactory(user=authenticate.user)
+
+ declaration = DeclarationFactory()
+ declared_microorganism = DeclaredMicroorganismFactory(declaration=declaration)
+ plant = PlantFactory()
+
+ response = self.client.post(
+ reverse("api:declared_element_replace", kwargs={"pk": declared_microorganism.id, "type": "microorganism"}),
+ {
+ "element": {"id": plant.id, "type": "plant"},
+ "additional_fields": {
+ "used_part": 99, # fail: id unrecognised
+ },
+ "synonyms": [{"name": "New synonym"}],
+ },
+ format="json",
+ )
+ body = response.json()
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, body)
+ self.assertEqual(
+ body["fieldErrors"]["usedPart"][0], "Clé primaire «\xa099\xa0» non valide - l'objet n'existe pas.", body
+ )
+ # assertion implicite - l'objet existe tjs
+ DeclaredMicroorganism.objects.get(id=declared_microorganism.id)
+ self.assertFalse(plant.plantsynonym_set.filter(name="New synonym").exists())
+ self.assertEqual(DeclaredPlant.objects.count(), 0)
+
+ @authenticate
+ def test_elements_unchanged_on_synonym_fail(self):
+ """
+ Si l'ajout de synonyme ne passe pas, on annule le remplacement complètement
+ """
+ InstructionRoleFactory(user=authenticate.user)
+
+ declaration = DeclarationFactory()
+ declared_microorganism = DeclaredMicroorganismFactory(declaration=declaration, quantity=10)
+ plant = PlantFactory()
+
+ response = self.client.post(
+ reverse("api:declared_element_replace", kwargs={"pk": declared_microorganism.id, "type": "microorganism"}),
+ {
+ "element": {"id": plant.id, "type": "plant"},
+ "additional_fields": {
+ "quantity": 99,
+ },
+ "synonyms": [{}],
+ },
+ format="json",
+ )
+ body = response.json()
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, body)
+ still_existing_declared_microorganism = DeclaredMicroorganism.objects.get(id=declared_microorganism.id)
+ self.assertEqual(still_existing_declared_microorganism.quantity, 10)
+ self.assertEqual(DeclaredPlant.objects.count(), 0)
+
+ @authenticate
+ def test_id_ignored_in_replace(self):
+ """
+ Vérifier que l'id d'un nouvel ingrédient déclaré est généré automatiquement, et non pas avec les données passées
+ """
+ InstructionRoleFactory(user=authenticate.user)
+
+ declaration = DeclarationFactory()
+ declared_microorganism = DeclaredMicroorganismFactory(id=66, declaration=declaration, new_species="test")
+ self.assertEqual(declared_microorganism.id, 66)
+ plant = PlantFactory()
+ unit = SubstanceUnitFactory()
+
+ response = self.client.post(
+ reverse("api:declared_element_replace", kwargs={"pk": declared_microorganism.id, "type": "microorganism"}),
+ {
+ "element": {"id": plant.id, "type": "plant"},
+ "additional_fields": {
+ "id": 99,
+ "unit": unit.id,
+ },
+ },
+ format="json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
+ new_declared_plant = DeclaredPlant.objects.first()
+ self.assertEqual(new_declared_plant.unit, unit)
+ self.assertNotEqual(new_declared_plant.id, 66)
+ self.assertNotEqual(new_declared_plant.id, 99)
diff --git a/api/tests/test_declaration_flow.py b/api/tests/test_declaration_flow.py
index 134213970..66d48c165 100644
--- a/api/tests/test_declaration_flow.py
+++ b/api/tests/test_declaration_flow.py
@@ -718,8 +718,8 @@ def test_visor_cant_modify_on_refuse(self):
declaration.refresh_from_db()
latest_snapshot = declaration.snapshots.latest("creation_date")
self.assertEqual(declaration.status, Declaration.DeclarationStatus.AWAITING_INSTRUCTION)
- self.assertEqual(latest_snapshot.comment, "À authoriser")
self.assertEqual(latest_snapshot.status, Declaration.DeclarationStatus.AWAITING_INSTRUCTION)
+ self.assertEqual(latest_snapshot.comment, "")
self.assertEqual(latest_snapshot.expiration_days, None)
self.assertEqual(latest_snapshot.blocking_reasons, None)
diff --git a/api/views/declaration/declaration.py b/api/views/declaration/declaration.py
index c6630f5d1..824d94bac 100644
--- a/api/views/declaration/declaration.py
+++ b/api/views/declaration/declaration.py
@@ -406,7 +406,7 @@ def post(self, request, pk):
if new_article not in Declaration.Article:
raise ProjectAPIException(global_error="Merci de spécifier un article valide")
- declaration.overriden_article = Declaration.Article(new_article)
+ declaration.overridden_article = Declaration.Article(new_article)
declaration.save()
declaration.refresh_from_db()
serializer = self.get_serializer(declaration)
@@ -637,7 +637,6 @@ def perform_snapshot_creation(self, request, declaration):
declaration.create_snapshot(
user=request.user,
action=self.get_snapshot_action(request, declaration),
- comment=declaration.post_validation_producer_message,
post_validation_status=self.get_snapshot_post_validation_status(request, declaration),
)
diff --git a/api/views/declaration/declared_element.py b/api/views/declaration/declared_element.py
index 0317b87be..3a8533cd1 100644
--- a/api/views/declaration/declared_element.py
+++ b/api/views/declaration/declared_element.py
@@ -1,4 +1,5 @@
import abc
+from django.db import transaction
from django.shortcuts import get_object_or_404
from rest_framework.exceptions import ParseError
from rest_framework.generics import ListAPIView, RetrieveAPIView
@@ -11,10 +12,6 @@
DeclaredIngredient,
DeclaredMicroorganism,
Declaration,
- Plant,
- Microorganism,
- Substance,
- Ingredient,
PlantSynonym,
MicroorganismSynonym,
SubstanceSynonym,
@@ -58,45 +55,43 @@ def get_queryset(self):
)
-class ElementMappingMixin:
- type_mapping = {
- "plant": {
- "model": DeclaredPlant,
- "element_model": Plant,
- "synonym_model": PlantSynonym,
- "serializer": DeclaredPlantSerializer,
- },
- "microorganism": {
- "model": DeclaredMicroorganism,
- "element_model": Microorganism,
- "synonym_model": MicroorganismSynonym,
- "serializer": DeclaredMicroorganismSerializer,
- },
- "substance": {
- "model": DeclaredSubstance,
- "element_model": Substance,
- "synonym_model": SubstanceSynonym,
- "serializer": DeclaredSubstanceSerializer,
- },
- "other-ingredient": {
- "model": DeclaredIngredient,
- "element_model": Ingredient,
- "synonym_model": IngredientSynonym,
- "serializer": DeclaredIngredientSerializer,
- },
- }
+TYPE_MAPPING = {
+ "plant": {
+ "model": DeclaredPlant,
+ "synonym_model": PlantSynonym,
+ "serializer": DeclaredPlantSerializer,
+ },
+ "microorganism": {
+ "model": DeclaredMicroorganism,
+ "synonym_model": MicroorganismSynonym,
+ "serializer": DeclaredMicroorganismSerializer,
+ },
+ "substance": {
+ "model": DeclaredSubstance,
+ "synonym_model": SubstanceSynonym,
+ "serializer": DeclaredSubstanceSerializer,
+ },
+ "other-ingredient": {
+ "model": DeclaredIngredient,
+ "synonym_model": IngredientSynonym,
+ "serializer": DeclaredIngredientSerializer,
+ "attribute": "ingredient",
+ },
+}
+
+class ElementMappingMixin:
@property
def element_type(self):
return self.kwargs["type"]
@property
def type_info(self):
- if self.element_type not in self.type_mapping:
- valid_type_list = list(self.type_mapping.keys())
+ if self.element_type not in TYPE_MAPPING:
+ valid_type_list = list(TYPE_MAPPING.keys())
raise ParseError(detail=f"Unknown type: '{self.element_type}' not in {valid_type_list}")
- return self.type_mapping[self.element_type]
+ return TYPE_MAPPING[self.element_type]
@property
def type_model(self):
@@ -106,10 +101,6 @@ def type_model(self):
def type_serializer(self):
return self.type_info["serializer"]
- @property
- def element_model(self):
- return self.type_info["element_model"]
-
@property
def synonym_model(self):
return self.type_info["synonym_model"]
@@ -129,13 +120,14 @@ class DeclaredElementActionAbstractView(APIView, ElementMappingMixin):
permission_classes = [(IsInstructor | IsVisor)]
__metaclass__ = abc.ABCMeta
+ @transaction.atomic
def post(self, request, pk, type):
element = get_object_or_404(self.type_model, pk=pk)
- self._update_element(element, request)
- element.save()
+ element_to_save = self._update_element(element, request)
+ element_to_save.save()
- return Response(self.type_serializer(element, context={"request": request}).data)
+ return Response(self.type_serializer(element_to_save, context={"request": request}).data)
@abc.abstractmethod
def _update_element(self, element, request):
@@ -146,40 +138,67 @@ class DeclaredElementRequestInfoView(DeclaredElementActionAbstractView):
def _update_element(self, element, request):
element.request_status = self.type_model.AddableStatus.INFORMATION
element.request_private_notes = request.data.get("request_private_notes")
+ return element
class DeclaredElementRejectView(DeclaredElementActionAbstractView):
def _update_element(self, element, request):
element.request_status = self.type_model.AddableStatus.REJECTED
element.request_private_notes = request.data.get("request_private_notes")
+ return element
class DeclaredElementReplaceView(DeclaredElementActionAbstractView):
- def _update_element(self, element, request):
+ def _update_element(self, declared_element, request):
try:
- existing_element_id = request.data["element"]["id"]
- existing_element_type = request.data["element"]["type"]
+ replacement_element_id = request.data["element"]["id"]
+ replacement_type = request.data["element"]["type"]
except KeyError:
raise ParseError(detail="Must provide a dict 'element' with id and type")
- if existing_element_type != self.element_type:
- raise ParseError(detail="Cannot replace element request with existing element of a different type")
+ # utiliser les valeurs serialisées pour MAJ ou créer l'element déclaré
+ element_data = self.type_serializer(declared_element).data
+ additional_fields = request.data.get("additional_fields", {})
+ element_data.update(additional_fields)
+ element_data.update(
+ {
+ "element": {"id": replacement_element_id},
+ "request_status": self.type_model.AddableStatus.REPLACED,
+ "new": False,
+ }
+ )
- try:
- existing_element = self.element_model.objects.get(pk=existing_element_id)
- except self.element_model.DoesNotExist:
- raise ParseError(detail=f"No {self.element_type} exists with id {existing_element_id}")
+ new_type = TYPE_MAPPING[replacement_type]
+ if replacement_type != self.element_type:
+ # gérer le difference en nom entre microorganismes et les autres types
+ if replacement_type == "microorganism":
+ element_data["new_species"] = declared_element.new_name
+ elif self.element_type == "microorganism":
+ element_data["new_name"] = declared_element.new_name
- setattr(element, self.element_type, existing_element)
- element.request_status = self.type_model.AddableStatus.REPLACED
- element.new = False
+ serializer = new_type["serializer"](data=element_data)
+ serializer.is_valid(raise_exception=True)
- synonyms = request.data.get("synonyms", [])
+ serializer.validated_data["declaration"] = declared_element.declaration
+ new_declared_element = serializer.create(serializer.validated_data)
+ declared_element.delete()
+ declared_element = new_declared_element
+ else:
+ serializer = self.type_serializer(declared_element, data=element_data, partial=True)
+ serializer.is_valid(raise_exception=True)
+ declared_element = serializer.save()
+
+ replacement_synonym_model = new_type["synonym_model"]
+ synonyms = request.data.get("synonyms", [])
+ replacement_type_attribute = new_type.get("attribute", replacement_type)
+ replacement_element = getattr(declared_element, replacement_type_attribute)
for synonym in synonyms:
try:
name = synonym["name"]
- if name and not self.synonym_model.objects.filter(name=name).exists():
- self.synonym_model.objects.create(standard_name=existing_element, name=name)
+ if name and not replacement_synonym_model.objects.filter(name=name).exists():
+ replacement_synonym_model.objects.create(standard_name=replacement_element, name=name)
except KeyError:
raise ParseError(detail="Must provide 'name' to create new synonym")
+
+ return declared_element
diff --git a/config/tests/test_automatic_approval.py b/config/tests/test_automatic_approval.py
index 347e6c6e4..94535982e 100644
--- a/config/tests/test_automatic_approval.py
+++ b/config/tests/test_automatic_approval.py
@@ -49,11 +49,11 @@ def test_awaiting_declaration_approved_art_15(self, _):
* aucune action d'instruction n'a été effectuée dessus
* son snapshot de soumission date de plus de trente jours.
"""
- declaration_15 = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration_15 = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
TestAutomaticApproval._create_submission_snapshot(declaration_15)
declaration_high_risk_population = AwaitingInstructionDeclarationFactory(
- overriden_article=Declaration.Article.ARTICLE_15_HIGH_RISK_POPULATION
+ overridden_article=Declaration.Article.ARTICLE_15_HIGH_RISK_POPULATION
)
TestAutomaticApproval._create_submission_snapshot(declaration_high_risk_population)
@@ -76,7 +76,7 @@ def test_double_submission_declaration_approved_art_15(self, _):
deux snapshots type SUBMIT sont créés. Le bot doit quand même approuver ces déclarations.
Plus d'infos : https://github.com/betagouv/complements-alimentaires/issues/1395
"""
- declaration_15 = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration_15 = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
# Double soumission
TestAutomaticApproval._create_submission_snapshot(declaration_15)
@@ -94,7 +94,7 @@ def test_non_submission_snapshots_not_approved_art_15(self, _):
Si au moins un snapshot est présent avec un type différent de "SUBMIT" la déclaration
ne devra pas être autorisée
"""
- declaration_15 = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration_15 = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
# Double soumission
TestAutomaticApproval._create_submission_snapshot(declaration_15)
@@ -117,7 +117,7 @@ def test_email_sent_declaration_approved(self, mocked_brevo):
"""
L'email d'approbation doit être envoyé lors d'une approbation automatique
"""
- declaration = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
TestAutomaticApproval._create_submission_snapshot(declaration)
approve_declarations()
@@ -137,7 +137,7 @@ def test_awaiting_declaration_not_approved_without_setting(self, mocked_brevo):
Le bot ne doit pas approuver des déclarations si le setting ENABLE_AUTO_VALIDATION
n'est pas True
"""
- declaration = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
TestAutomaticApproval._create_submission_snapshot(declaration)
approve_declarations()
@@ -150,7 +150,7 @@ def test_awaiting_declaration_not_approved_art_15_vig(self, mocked_brevo):
Une déclaration en attente d'instruction de doit pas se valider si elle a l'article
15 vigilance
"""
- declaration = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15_WARNING)
+ declaration = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15_WARNING)
TestAutomaticApproval._create_submission_snapshot(declaration)
approve_declarations()
@@ -162,7 +162,7 @@ def test_awaiting_declaration_not_approved_art_16(self, mocked_brevo):
"""
Une déclaration en attente d'instruction de doit pas se valider si elle a l'article 16
"""
- declaration = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_16)
+ declaration = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_16)
TestAutomaticApproval._create_submission_snapshot(declaration)
approve_declarations()
@@ -174,7 +174,7 @@ def test_awaiting_declaration_not_approved_art_anses(self, mocked_brevo):
"""
Une déclaration en attente d'instruction de doit pas se valider si elle a l'article ANSES
"""
- declaration = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ANSES_REFERAL)
+ declaration = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ANSES_REFERAL)
TestAutomaticApproval._create_submission_snapshot(declaration)
approve_declarations()
@@ -200,7 +200,7 @@ def test_awaiting_declaration_not_approved_instructed(self, mocked_brevo):
actions d'instruction ont été effectuées dessus, par exemple des observations,
objections, requêtes de visa, etc.
"""
- declaration = AwaitingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration = AwaitingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
TestAutomaticApproval._create_submission_snapshot(declaration)
# On crée un autre snapshot indiquant que la déclaration a subi des actions autres
@@ -224,7 +224,7 @@ def test_ongoing_declaration_not_approved(self, mocked_brevo):
Une déclaration dont l'instruction est en cours ne doit pas se valider
toute seule
"""
- declaration = OngoingInstructionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration = OngoingInstructionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
TestAutomaticApproval._create_submission_snapshot(declaration)
approve_declarations()
@@ -236,7 +236,7 @@ def test_observed_declaration_not_approved(self, mocked_brevo):
"""
Une déclaration en observation ne doit pas se valider toute seule
"""
- declaration = ObservationDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration = ObservationDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
TestAutomaticApproval._create_submission_snapshot(declaration)
approve_declarations()
@@ -248,7 +248,7 @@ def test_objected_declaration_not_approved(self, mocked_brevo):
"""
Une déclaration en objection ne doit pas se valider toute seule
"""
- declaration = ObjectionDeclarationFactory(overriden_article=Declaration.Article.ARTICLE_15)
+ declaration = ObjectionDeclarationFactory(overridden_article=Declaration.Article.ARTICLE_15)
TestAutomaticApproval._create_submission_snapshot(declaration)
approve_declarations()
@@ -261,7 +261,7 @@ def test_abandoned_declaration_not_approved(self, mocked_brevo):
Une déclaration en abandon ne doit pas se valider toute seule
"""
declaration = InstructionReadyDeclarationFactory(
- status=Declaration.DeclarationStatus.ABANDONED, overriden_article=Declaration.Article.ARTICLE_15
+ status=Declaration.DeclarationStatus.ABANDONED, overridden_article=Declaration.Article.ARTICLE_15
)
TestAutomaticApproval._create_submission_snapshot(declaration)
@@ -275,7 +275,7 @@ def test_refused_declaration_not_approved(self, mocked_brevo):
Une déclaration refusée ne doit pas se valider toute seule
"""
declaration = InstructionReadyDeclarationFactory(
- status=Declaration.DeclarationStatus.REJECTED, overriden_article=Declaration.Article.ARTICLE_15
+ status=Declaration.DeclarationStatus.REJECTED, overridden_article=Declaration.Article.ARTICLE_15
)
TestAutomaticApproval._create_submission_snapshot(declaration)
@@ -289,7 +289,7 @@ def test_withdrawn_declaration_not_approved(self, mocked_brevo):
Une déclaration retirée du marché ne doit pas se valider toute seule
"""
declaration = InstructionReadyDeclarationFactory(
- status=Declaration.DeclarationStatus.WITHDRAWN, overriden_article=Declaration.Article.ARTICLE_15
+ status=Declaration.DeclarationStatus.WITHDRAWN, overridden_article=Declaration.Article.ARTICLE_15
)
TestAutomaticApproval._create_submission_snapshot(declaration)
diff --git a/data/README.md b/data/README.md
index 7c2ec3706..a54200fc4 100644
--- a/data/README.md
+++ b/data/README.md
@@ -44,7 +44,7 @@ REF_ICA_FONCTION_INGREDIENT | | | | | |
|ICA_POPULATION_RISQUE_DECLAREE | | |
|ICA_SUBSTANCE_DECLAREE | | |
|ICA_USAGER | | 🕵️anonymisée (contient Foreign Key vers USR, ADM, ETAB) |
-|REF_ICA_TYPE_DECLARATION | |Enum ? ou obsolète ? (Art 1(, Art 1-, Simplifiée)) |
+|REF_ICA_TYPE_DECLARATION | |Enum ? ou obsolète ? (Art 15, Art 16, Simplifiée)) |
|REF_ICA_TYPE_HERITAGE | | Enum ? (Simplifié ou Nouvelle formule)|
|REF_ICA_TYPE_VERSION_DECLARATION | | |
|REF_ICA_FORME_GALENIQUE | | |
diff --git a/data/admin/declaration.py b/data/admin/declaration.py
index fe96a0091..3f87f037f 100644
--- a/data/admin/declaration.py
+++ b/data/admin/declaration.py
@@ -264,7 +264,7 @@ class DeclarationAdmin(SimpleHistoryAdmin):
"gamme",
"flavor",
"calculated_article",
- "overriden_article",
+ "overridden_article",
)
},
),
diff --git a/data/admin/ingredient.py b/data/admin/ingredient.py
index 178d86fe6..7da71d20d 100644
--- a/data/admin/ingredient.py
+++ b/data/admin/ingredient.py
@@ -38,8 +38,8 @@ class IngredientAdmin(ElementAdminWithChangeReason):
SubstanceInlineAdmin,
IngredientSynonymInline,
)
- list_display = ("name", "is_obsolete", "status", "is_risky", "novel_food")
- list_filter = ("is_obsolete", "status", "is_risky", "novel_food")
+ list_display = ("name", "is_obsolete", "status", "is_risky", "novel_food", "has_linked_substances")
+ list_filter = ("is_obsolete", "status", "is_risky", "novel_food", "ingredient_type")
show_facets = admin.ShowFacets.NEVER
readonly_fields = (
"name",
@@ -49,3 +49,6 @@ class IngredientAdmin(ElementAdminWithChangeReason):
"siccrf_private_comments",
)
search_fields = ["id", "name"]
+
+ def has_linked_substances(self, obj):
+ return "Oui" if obj.substances.exists() else "Non"
diff --git a/data/etl/extractor.py b/data/etl/extractor.py
index e001c3e90..2f6006561 100644
--- a/data/etl/extractor.py
+++ b/data/etl/extractor.py
@@ -60,7 +60,7 @@ def clean_dataset(self):
# ---------------------------------------------------
self.df = self.df[columns_to_keep]
self.filter_dataframe_with_schema_cols()
- self.df = self.df.replace({"\n": " ", "\r": " "}, regex=True)
+ self.df = self.df.replace({"\n": " ", "\r": " ", ";": ""}, regex=True)
def is_valid(self) -> bool:
files = prepare_file_validata_post_request(self.df)
@@ -99,7 +99,7 @@ class DECLARATIONS(EXTRACTOR):
def __init__(self):
super().__init__()
self.dataset_name = "declarations"
- self.schema = json.load(open("data/schemas/schema_declarations.json"))["schema"]
+ self.schema = json.load(open("data/schemas/schema_declarations.json"))
self.schema_url = (
"https://github.com/betagouv/complements-alimentaires/blob/staging/data/schemas/schema_declarations.json"
)
diff --git a/data/etl/teleicare_history/extractor.py b/data/etl/teleicare_history/extractor.py
index 9ca33f281..a2d917c7f 100644
--- a/data/etl/teleicare_history/extractor.py
+++ b/data/etl/teleicare_history/extractor.py
@@ -4,18 +4,46 @@
from datetime import date, datetime, timezone
from django.core.exceptions import MultipleObjectsReturned, ValidationError
-from django.db import IntegrityError
+from django.db import IntegrityError, transaction
from phonenumber_field.phonenumber import PhoneNumber
-from data.models import GalenicFormulation, SubstanceUnit
+from data.models import (
+ Condition,
+ Effect,
+ GalenicFormulation,
+ Ingredient,
+ Microorganism,
+ Plant,
+ PlantPart,
+ Population,
+ Preparation,
+ Substance,
+ SubstanceUnit,
+)
from data.models.company import ActivityChoices, Company
-from data.models.declaration import Declaration
+from data.models.declaration import (
+ ComputedSubstance,
+ Declaration,
+ DeclaredIngredient,
+ DeclaredMicroorganism,
+ DeclaredPlant,
+)
from data.models.teleicare_history.ica_declaration import (
IcaComplementAlimentaire,
IcaDeclaration,
+ IcaEffetDeclare,
+ IcaPopulationCibleDeclaree,
+ IcaPopulationRisqueDeclaree,
IcaVersionDeclaration,
)
+from data.models.teleicare_history.ica_declaration_composition import (
+ IcaIngredient,
+ IcaIngredientAutre,
+ IcaMicroOrganisme,
+ IcaPreparation,
+ IcaSubstanceDeclaree,
+)
from data.models.teleicare_history.ica_etablissement import IcaEtablissement
logger = logging.getLogger(__name__)
@@ -177,6 +205,144 @@ def convert_str_date(value, aware=False):
8: Declaration.DeclarationStatus.ABANDONED, # 'abandonné'
}
+DECLARATION_TYPE_TO_ARTICLE_MAPPING = {
+ 1: Declaration.Article.ARTICLE_15,
+ 2: Declaration.Article.ARTICLE_16,
+ 3: None, # Type "simplifié" dans Teleicare, normalement liées à des modifications de déclarations déjà instruites
+}
+
+
+MANY_TO_MANY_PRODUCT_MODELS_MATCHING = {
+ "effects": {"teleIcare_model": IcaEffetDeclare, "teleIcare_pk": "objeff_ident", "CA_model": Effect},
+ "conditions_not_recommended": {
+ "teleIcare_model": IcaPopulationRisqueDeclaree,
+ "teleIcare_pk": "poprs_ident",
+ "CA_model": Condition,
+ },
+ "populations": {
+ "teleIcare_model": IcaPopulationCibleDeclaree,
+ "teleIcare_pk": "popcbl_ident",
+ "CA_model": Population,
+ },
+}
+
+
+def create_declared_plant(declaration, teleIcare_plant, active):
+ declared_plant = DeclaredPlant(
+ declaration=declaration,
+ plant=Plant.objects.get(siccrf_id=teleIcare_plant.plte_ident),
+ active=active,
+ quantity=teleIcare_plant.prepa_qte if active else None,
+ unit=SubstanceUnit.objects.get(siccrf_id=teleIcare_plant.unt_ident) if active else None,
+ preparation=Preparation.objects.get(siccrf_id=teleIcare_plant.typprep_ident) if active else None,
+ used_part=PlantPart.objects.get(siccrf_id=teleIcare_plant.pplan_ident) if active else None,
+ )
+ return declared_plant
+
+
+def create_declared_microorganism(declaration, teleIcare_microorganism, active):
+ declared_microorganism = DeclaredMicroorganism(
+ declaration=declaration,
+ microorganism=Microorganism.objects.get(siccrf_id=teleIcare_microorganism.morg_ident),
+ active=active,
+ activated=True, # dans TeleIcare, le champ `activated` n'existait pas, les MO inactivés étaient des `ingrédients autres`
+ strain=teleIcare_microorganism.ingmorg_souche,
+ quantity=teleIcare_microorganism.ingmorg_quantite_par_djr,
+ )
+ return declared_microorganism
+
+
+def create_declared_ingredient(declaration, teleIcare_ingredient, active):
+ declared_ingredient = DeclaredIngredient(
+ declaration=declaration,
+ ingredient=Ingredient.objects.get(siccrf_id=teleIcare_ingredient.inga_ident),
+ active=active,
+ quantity=None, # dans TeleIcare, les ingrédients n'avaient pas de quantité associée
+ )
+ return declared_ingredient
+
+
+def create_computed_substance(declaration, teleIcare_substance):
+ computed_substance = ComputedSubstance(
+ declaration=declaration,
+ substance=Substance.objects.get(siccrf_id=teleIcare_substance.sbsact_ident),
+ quantity=teleIcare_substance.sbsactdecl_quantite_par_djr,
+ # le champ de TeleIcare 'sbsact_commentaires' n'est pas repris
+ )
+ return computed_substance
+
+
+@transaction.atomic
+def add_product_info_from_teleicare_history(declaration, vrsdecl_ident):
+ """
+ Cette function importe les champs ManyToMany des déclarations, relatifs à l'onglet "Produit"
+ Il est nécessaire que les objets soient enregistrés en base (et aient obtenu un id) grâce à la fonction
+ `create_declaration_from_teleicare_history` pour updater leurs champs ManyToMany.
+ """
+ # TODO: other_conditions=conditions_not_recommended,
+ # TODO: other_effects=
+ for CA_field_name, struct in MANY_TO_MANY_PRODUCT_MODELS_MATCHING.items():
+ # par exemple Declaration.populations
+ CA_field = getattr(declaration, CA_field_name)
+ # TODO: utiliser les dataclass ici
+ pk_field = struct["teleIcare_pk"]
+ CA_model = struct["CA_model"]
+ teleIcare_model = struct["teleIcare_model"]
+ CA_field.set(
+ [
+ CA_model.objects.get(siccrf_id=getattr(TI_object, pk_field))
+ for TI_object in (teleIcare_model.objects.filter(vrsdecl_ident=vrsdecl_ident))
+ ]
+ )
+
+
+@transaction.atomic
+def add_composition_from_teleicare_history(declaration, vrsdecl_ident):
+ """
+ Cette function importe les champs ManyToMany des déclarations, relatifs à l'onglet "Composition"
+ Il est nécessaire que les objets soient enregistrés en base (et aient obtenu un id) grâce à la fonction
+ `create_declaration_from_teleicare_history` pour updater leurs champs ManyToMany.
+ """
+ bulk_ingredients = {DeclaredPlant: [], DeclaredMicroorganism: [], DeclaredIngredient: [], ComputedSubstance: []}
+ for ingredient in IcaIngredient.objects.filter(vrsdecl_ident=vrsdecl_ident):
+ if ingredient.tying_ident == 1:
+ declared_plant = create_declared_plant(
+ declaration=declaration,
+ teleIcare_plant=IcaPreparation.objects.get(
+ ingr_ident=ingredient.ingr_ident,
+ ),
+ active=ingredient.fctingr_ident == 1,
+ )
+ bulk_ingredients[DeclaredPlant].append(declared_plant)
+ elif ingredient.tying_ident == 2:
+ declared_microorganism = create_declared_microorganism(
+ declaration=declaration,
+ teleIcare_microorganism=IcaMicroOrganisme.objects.get(
+ ingr_ident=ingredient.ingr_ident,
+ ),
+ active=ingredient.fctingr_ident == 1,
+ )
+ bulk_ingredients[DeclaredMicroorganism].append(declared_microorganism)
+ elif ingredient.tying_ident == 3:
+ declared_ingredient = create_declared_ingredient(
+ declaration=declaration,
+ teleIcare_ingredient=IcaIngredientAutre.objects.get(
+ ingr_ident=ingredient.ingr_ident,
+ ),
+ active=ingredient.fctingr_ident == 1,
+ )
+ bulk_ingredients[DeclaredIngredient].append(declared_ingredient)
+ # dans TeleIcare les `declared_substances` étaient des ingrédients
+ # donc on ne rempli pas le champ declaration.declared_substances grâce à l'historique
+ for teleIcare_substance in IcaSubstanceDeclaree.objects.filter(vrsdecl_ident=vrsdecl_ident):
+ computed_substance = create_computed_substance(
+ declaration=declaration, teleIcare_substance=teleIcare_substance
+ )
+ bulk_ingredients[ComputedSubstance].append(computed_substance)
+
+ for model, bulk_of_objects in bulk_ingredients.items():
+ model.objects.bulk_create(bulk_of_objects)
+
def create_declaration_from_teleicare_history():
"""
@@ -248,12 +414,8 @@ def create_declaration_from_teleicare_history():
minimum_duration=latest_ica_version_declaration.vrsdecl_durabilite,
instructions=latest_ica_version_declaration.vrsdecl_mode_emploi or "",
warning=latest_ica_version_declaration.vrsdecl_mise_en_garde or "",
- # TODO: ces champs proviennent de tables pas encore importées
- # populations=
- # conditions_not_recommended=
- # other_conditions=
- # effects=
- # other_effects=
+ calculated_article=DECLARATION_TYPE_TO_ARTICLE_MAPPING[latest_ica_declaration.tydcl_ident],
+ # TODO: ces champs sont à importer
# address=
# postal_code=
# city=
@@ -266,6 +428,12 @@ def create_declaration_from_teleicare_history():
try:
with suppress_autotime(declaration, ["creation_date", "modification_date"]):
declaration.save()
+ add_product_info_from_teleicare_history(
+ declaration, latest_ica_version_declaration.vrsdecl_ident
+ )
+ add_composition_from_teleicare_history(
+ declaration, latest_ica_version_declaration.vrsdecl_ident
+ )
nb_created_declarations += 1
except IntegrityError:
# cette Déclaration a déjà été créée
diff --git a/data/etl/teleicare_history/sql/company_table_creation.sql b/data/etl/teleicare_history/sql/company_table_creation.sql
new file mode 100644
index 000000000..4c7548087
--- /dev/null
+++ b/data/etl/teleicare_history/sql/company_table_creation.sql
@@ -0,0 +1,43 @@
+-- Le but de ce fichier est de permettre la recréation des tables Teleicare correspondant
+-- au modèle de Declaration Compl'Alim
+
+
+-- int int smallint -> integer
+-- vachar -> text
+-- bit -> boolean
+-- datetime -> text (pour une conversion ensuite)
+
+CREATE TABLE ICA_ETABLISSEMENT (
+ ETAB_IDENT integer PRIMARY KEY,
+ COG_IDENT integer NULL,
+ ETAB_IDENT_PARENT integer NULL,
+ PAYS_IDENT integer NOT NULL,
+ ETAB_SIRET text NULL,
+ ETAB_NUMERO_TVA_INTRA text NULL,
+ ETAB_RAISON_SOCIALE text NOT NULL,
+ ETAB_ENSEIGNE text NULL,
+ ETAB_ADRE_VILLE text NULL,
+ ETAB_ADRE_CP text NULL,
+ ETAB_ADRE_VOIE text NULL,
+ ETAB_ADRE_COMP text NULL,
+ ETAB_ADRE_COMP2 text NULL,
+ ETAB_ADRE_DIST text NULL,
+ ETAB_TELEPHONE text NULL,
+ ETAB_FAX text NULL,
+ ETAB_COURRIEL text NULL,
+ ETAB_SITE_INTERNET text NULL,
+ ETAB_ICA_FACONNIER boolean NULL,
+ ETAB_ICA_FABRICANT boolean NULL,
+ ETAB_ICA_CONSEIL boolean NULL,
+ ETAB_ICA_IMPORTATEUR boolean NULL,
+ ETAB_ICA_INTRODUCTEUR boolean NULL,
+ ETAB_ICA_DISTRIBUTEUR boolean NULL,
+ ETAB_ICA_ENSEIGNE text NULL,
+ ETAB_ADRE_REGION text NULL,
+ ETAB_DT_AJOUT_IDENT_PARENT text NULL,
+ ETAB_NUM_ADH_TELE_PROC text NULL,
+ ETAB_COMMENTAIRE_IDENT_PARENT text NULL,
+ ETAB_NOM_DOMAINE text NULL,
+ ETAB_DATE_ADHESION text NULL,
+ ETAB_NB_COMPTE_AUTORISE integer NOT NULL
+);
diff --git a/data/etl/teleicare_history/sql/declaration_composition_table_creation.sql b/data/etl/teleicare_history/sql/declaration_composition_table_creation.sql
new file mode 100644
index 000000000..8ebb619e3
--- /dev/null
+++ b/data/etl/teleicare_history/sql/declaration_composition_table_creation.sql
@@ -0,0 +1,44 @@
+CREATE TABLE ICA_INGREDIENT (
+ INGR_IDENT integer PRIMARY KEY,
+ VRSDECL_IDENT integer NOT NULL,
+ FCTINGR_IDENT integer NOT NULL,
+ TYING_IDENT integer NOT NULL,
+ INGR_COMMENTAIRES text NULL
+);
+
+
+
+
+CREATE TABLE ICA_MICRO_ORGANISME (
+ INGR_IDENT integer PRIMARY KEY,
+ MORG_IDENT integer NOT NULL,
+ INGMORG_SOUCHE text NULL,
+ INGMORG_QUANTITE_PAR_DJR bigint NULL
+);
+
+
+
+CREATE TABLE ICA_INGREDIENT_AUTRE (
+ INGR_IDENT integer PRIMARY KEY,
+ INGA_IDENT integer NOT NULL
+);
+
+
+
+CREATE TABLE ICA_PREPARATION (
+ INGR_IDENT integer PRIMARY KEY,
+ PLTE_IDENT integer NOT NULL,
+ PPLAN_IDENT integer NULL,
+ UNT_IDENT integer NULL,
+ TYPPREP_IDENT integer NULL,
+ PREPA_QTE float NULL
+);
+
+
+CREATE TABLE ICA_SUBSTANCE_DECLAREE(
+ VRSDECL_IDENT integer NOT NULL,
+ SBSACT_IDENT integer NOT NULL,
+ SBSACT_COMMENTAIRES text NULL,
+ SBSACTDECL_QUANTITE_PAR_DJR float NULL,
+ PRIMARY KEY (VRSDECL_IDENT, SBSACT_IDENT)
+);
diff --git a/data/etl/teleicare_history/sql/company_declaration_table_creation.sql b/data/etl/teleicare_history/sql/declaration_table_creation.sql
similarity index 71%
rename from data/etl/teleicare_history/sql/company_declaration_table_creation.sql
rename to data/etl/teleicare_history/sql/declaration_table_creation.sql
index 9e5e6bdec..1dfb63054 100644
--- a/data/etl/teleicare_history/sql/company_declaration_table_creation.sql
+++ b/data/etl/teleicare_history/sql/declaration_table_creation.sql
@@ -1,48 +1,3 @@
--- Le but de ce fichier est de permettre la recréation des tables Teleicare correspondant
--- au modèle de Declaration Compl'Alim
-
-
--- int int smallint -> integer
--- vachar -> text
--- bit -> boolean
--- datetime -> text (pour une conversion ensuite)
-
-CREATE TABLE ICA_ETABLISSEMENT (
- ETAB_IDENT integer PRIMARY KEY,
- COG_IDENT integer NULL,
- ETAB_IDENT_PARENT integer NULL,
- PAYS_IDENT integer NOT NULL,
- ETAB_SIRET text NULL,
- ETAB_NUMERO_TVA_INTRA text NULL,
- ETAB_RAISON_SOCIALE text NOT NULL,
- ETAB_ENSEIGNE text NULL,
- ETAB_ADRE_VILLE text NULL,
- ETAB_ADRE_CP text NULL,
- ETAB_ADRE_VOIE text NULL,
- ETAB_ADRE_COMP text NULL,
- ETAB_ADRE_COMP2 text NULL,
- ETAB_ADRE_DIST text NULL,
- ETAB_TELEPHONE text NULL,
- ETAB_FAX text NULL,
- ETAB_COURRIEL text NULL,
- ETAB_SITE_INTERNET text NULL,
- ETAB_ICA_FACONNIER boolean NULL,
- ETAB_ICA_FABRICANT boolean NULL,
- ETAB_ICA_CONSEIL boolean NULL,
- ETAB_ICA_IMPORTATEUR boolean NULL,
- ETAB_ICA_INTRODUCTEUR boolean NULL,
- ETAB_ICA_DISTRIBUTEUR boolean NULL,
- ETAB_ICA_ENSEIGNE text NULL,
- ETAB_ADRE_REGION text NULL,
- ETAB_DT_AJOUT_IDENT_PARENT text NULL,
- ETAB_NUM_ADH_TELE_PROC text NULL,
- ETAB_COMMENTAIRE_IDENT_PARENT text NULL,
- ETAB_NOM_DOMAINE text NULL,
- ETAB_DATE_ADHESION text NULL,
- ETAB_NB_COMPTE_AUTORISE integer NOT NULL
-);
-
-
CREATE TABLE ICA_COMPLEMENTALIMENTAIRE (
CPLALIM_IDENT integer PRIMARY KEY,
FRMGAL_IDENT integer NULL,
@@ -144,3 +99,30 @@ CREATE TABLE ICA_VERSIONDECLARATION (
-- when 1 then 'nouvelle'
-- when 2 then 'compléments d'information'
-- when 3 then 'observation'
+
+
+
+CREATE TABLE ICA_POPULATION_CIBLE_DECLAREE (
+ VRSDECL_IDENT integer NOT NULL,
+ POPCBL_IDENT integer NOT NULL,
+ VRSPCB_POPCIBLE_AUTRE text NULL,
+ PRIMARY KEY (VRSDECL_IDENT, POPCBL_IDENT)
+);
+
+
+
+
+CREATE TABLE ICA_POPULATION_RISQUE_DECLAREE (
+ VRSDECL_IDENT integer NOT NULL,
+ POPRS_IDENT integer NOT NULL,
+ VRSPRS_POPRISQUE_AUTRE text NULL,
+ PRIMARY KEY (VRSDECL_IDENT, POPRS_IDENT)
+);
+
+
+CREATE TABLE ICA_EFFET_DECLARE (
+ VRSDECL_IDENT integer NOT NULL,
+ OBJEFF_IDENT integer NOT NULL,
+ VRS_AUTRE_OBJECTIF text NULL,
+ PRIMARY KEY (VRSDECL_IDENT, OBJEFF_IDENT)
+);
diff --git a/data/etl/transformer_loader.py b/data/etl/transformer_loader.py
index 407186c7c..8e5f3f309 100644
--- a/data/etl/transformer_loader.py
+++ b/data/etl/transformer_loader.py
@@ -22,7 +22,7 @@ def _load_data_csv(self, filename):
index=False,
na_rep="",
encoding="utf_8_sig",
- quoting=csv.QUOTE_NONE,
+ quoting=csv.QUOTE_NONNUMERIC,
escapechar="\\",
)
diff --git a/data/factories/teleicare_history/__init__.py b/data/factories/teleicare_history/__init__.py
index a446b86cd..932a8a042 100644
--- a/data/factories/teleicare_history/__init__.py
+++ b/data/factories/teleicare_history/__init__.py
@@ -68,7 +68,8 @@ class Meta:
dcl_ident = factory.Sequence(lambda n: n + 1)
cplalim = factory.SubFactory(ComplementAlimentaireFactory)
- tydcl_ident = factory.Faker("pyint", min_value=0, max_value=20)
+ # 3 valeurs possibles dans TeleIcare {1: "Article 15", 2: "Article 16", 3: "Simplifiée"}
+ tydcl_ident = factory.Faker("pyint", min_value=1, max_value=3)
etab = factory.SubFactory(EtablissementFactory)
etab_ident_rmm_declarant = factory.Faker("pyint", min_value=0, max_value=20)
dcl_date = datetime.strftime(random_date(start=datetime(2016, 1, 1)), "%m/%d/%Y %H:%M:%S %p")
diff --git a/data/migrations/0118_icaeffetdeclare_icapopulationcibledeclaree_and_more.py b/data/migrations/0118_icaeffetdeclare_icapopulationcibledeclaree_and_more.py
new file mode 100644
index 000000000..792d87e52
--- /dev/null
+++ b/data/migrations/0118_icaeffetdeclare_icapopulationcibledeclaree_and_more.py
@@ -0,0 +1,49 @@
+# Generated by Django 5.1.5 on 2025-01-24 09:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('data', '0117_remove_blogpost_body'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='IcaEffetDeclare',
+ fields=[
+ ('vrsdecl_ident', models.IntegerField(primary_key=True, serialize=False)),
+ ('objeff_ident', models.IntegerField()),
+ ('vrs_autre_objectif', models.TextField(blank=True, null=True)),
+ ],
+ options={
+ 'db_table': 'ica_effet_declare',
+ 'managed': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='IcaPopulationCibleDeclaree',
+ fields=[
+ ('vrsdecl_ident', models.IntegerField(primary_key=True, serialize=False)),
+ ('popcbl_ident', models.IntegerField()),
+ ('vrspcb_popcible_autre', models.TextField(blank=True, null=True)),
+ ],
+ options={
+ 'db_table': 'ica_population_cible_declaree',
+ 'managed': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='IcaPopulationRisqueDeclaree',
+ fields=[
+ ('vrsdecl_ident', models.IntegerField(primary_key=True, serialize=False)),
+ ('poprs_ident', models.IntegerField()),
+ ('vrsprs_poprisque_autre', models.TextField(blank=True, null=True)),
+ ],
+ options={
+ 'db_table': 'ica_population_risque_declaree',
+ 'managed': False,
+ },
+ ),
+ ]
diff --git a/data/migrations/0118_rename_overriden_article_declaration_overridden_article_and_more.py b/data/migrations/0118_rename_overriden_article_declaration_overridden_article_and_more.py
new file mode 100644
index 000000000..6aa199e7b
--- /dev/null
+++ b/data/migrations/0118_rename_overriden_article_declaration_overridden_article_and_more.py
@@ -0,0 +1,43 @@
+# Generated by Django 5.1.5 on 2025-01-24 16:59
+
+import django.db.models.functions.comparison
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('data', '0117_remove_blogpost_body'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='declaration',
+ old_name='overriden_article',
+ new_name='overridden_article',
+ ),
+ migrations.RenameField(
+ model_name='historicaldeclaration',
+ old_name='overriden_article',
+ new_name='overridden_article',
+ ),
+ # ValueError: Modifying GeneratedFields is not supported - the field data.Declaration.article must be removed and re-added with the new definition.
+ migrations.RemoveField(
+ model_name="declaration",
+ name="article",
+ ),
+ migrations.RemoveField(
+ model_name="historicaldeclaration",
+ name="article",
+ ),
+ migrations.AddField(
+ model_name='declaration',
+ name='article',
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.functions.comparison.Coalesce(models.Case(models.When(overridden_article='', then=models.Value(None)), default='overridden_article'), models.Case(models.When(calculated_article='', then=models.Value(None)), default='calculated_article'), models.Value(None)), output_field=models.TextField(null=True, verbose_name='article')),
+ ),
+ migrations.AddField(
+ model_name='historicaldeclaration',
+ name='article',
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.functions.comparison.Coalesce(models.Case(models.When(overridden_article='', then=models.Value(None)), default='overridden_article'), models.Case(models.When(calculated_article='', then=models.Value(None)), default='calculated_article'), models.Value(None)), output_field=models.TextField(null=True, verbose_name='article')),
+ ),
+ ]
diff --git a/data/migrations/0119_merge_20250128_1101.py b/data/migrations/0119_merge_20250128_1101.py
new file mode 100644
index 000000000..897e484c3
--- /dev/null
+++ b/data/migrations/0119_merge_20250128_1101.py
@@ -0,0 +1,14 @@
+# Generated by Django 5.1.5 on 2025-01-28 10:01
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('data', '0118_icaeffetdeclare_icapopulationcibledeclaree_and_more'),
+ ('data', '0118_rename_overriden_article_declaration_overridden_article_and_more'),
+ ]
+
+ operations = [
+ ]
diff --git a/data/models/declaration.py b/data/models/declaration.py
index fae619db6..513227123 100644
--- a/data/models/declaration.py
+++ b/data/models/declaration.py
@@ -179,11 +179,11 @@ class Article(models.TextChoices):
other_effects = models.TextField(blank=True, verbose_name="autres objectifs ou effets non listés")
calculated_article = models.TextField("article calculé automatiquement", blank=True, choices=Article)
- # TODO: les Article.choice pour overriden_article ne devraient pas inclure les choices calculés automatiquement
- overriden_article = models.TextField("article manuellement spécifié", blank=True, choices=Article)
+ # TODO: les Article.choice pour overridden_article ne devraient pas inclure les choices calculés automatiquement
+ overridden_article = models.TextField("article manuellement spécifié", blank=True, choices=Article)
article = models.GeneratedField(
expression=Coalesce(
- Case(When(overriden_article="", then=Value(None)), default="overriden_article"),
+ Case(When(overridden_article="", then=Value(None)), default="overridden_article"),
Case(When(calculated_article="", then=Value(None)), default="calculated_article"),
Value(None),
),
diff --git a/data/models/teleicare_history/ica_declaration.py b/data/models/teleicare_history/ica_declaration.py
index f235b3099..b6bc813bf 100644
--- a/data/models/teleicare_history/ica_declaration.py
+++ b/data/models/teleicare_history/ica_declaration.py
@@ -35,13 +35,14 @@ class IcaDeclaration(models.Model):
# dcl_ident et cplalim_ident ne sont pas égaux
dcl_ident = models.IntegerField(primary_key=True)
cplalim = models.ForeignKey(IcaComplementAlimentaire, on_delete=models.CASCADE, db_column="cplalim_ident")
- tydcl_ident = models.IntegerField()
+ tydcl_ident = models.IntegerField() # Article 15 ou 16
etab = models.ForeignKey(
IcaEtablissement, on_delete=models.CASCADE, db_column="etab_ident"
) # correspond à l'entreprise gestionnaire de la déclaration
etab_ident_rmm_declarant = models.IntegerField()
dcl_date = models.TextField()
dcl_saisie_administration = models.BooleanField(null=True) # rendu nullable pour simplifier les Factories
+ # l'identifiant Teleicare est constitué ainsi {dcl_annee}-{dcl_mois}-{dcl_numero}
dcl_annee = models.IntegerField(null=True) # rendu nullable pour simplifier les Factories
dcl_mois = models.IntegerField(null=True) # rendu nullable pour simplifier les Factories
dcl_numero = models.IntegerField(null=True) # rendu nullable pour simplifier les Factories
@@ -55,7 +56,8 @@ class Meta:
class IcaVersionDeclaration(models.Model):
vrsdecl_ident = models.IntegerField(primary_key=True)
ag_ident = models.IntegerField(blank=True, null=True)
- typvrs_ident = models.IntegerField(null=True) # rendu nullable pour simplifier les Factories
+ typvrs_ident = models.IntegerField(null=True) # 1: Nouvelle 2: Complément d'information 3: Observations
+ # rendu nullable pour simplifier les Factories
unt_ident = models.IntegerField(blank=True, null=True)
pays_ident_adre = models.IntegerField(blank=True, null=True)
etab = models.ForeignKey(
@@ -95,3 +97,36 @@ class IcaVersionDeclaration(models.Model):
class Meta:
managed = False
db_table = "ica_versiondeclaration"
+
+
+class IcaPopulationCibleDeclaree(models.Model):
+ vrsdecl_ident = models.IntegerField()
+ popcbl_ident = models.IntegerField()
+ vrspcb_popcible_autre = models.TextField(blank=True, null=True)
+
+ class Meta:
+ managed = False
+ db_table = "ica_population_cible_declaree"
+ unique_together = (("vrsdecl_ident", "popcbl_ident"),)
+
+
+class IcaPopulationRisqueDeclaree(models.Model):
+ vrsdecl_ident = models.IntegerField()
+ poprs_ident = models.IntegerField()
+ vrsprs_poprisque_autre = models.TextField(blank=True, null=True)
+
+ class Meta:
+ managed = False
+ db_table = "ica_population_risque_declaree"
+ unique_together = (("vrsdecl_ident", "poprs_ident"),)
+
+
+class IcaEffetDeclare(models.Model):
+ vrsdecl_ident = models.IntegerField()
+ objeff_ident = models.IntegerField()
+ vrs_autre_objectif = models.TextField(blank=True, null=True)
+
+ class Meta:
+ managed = False
+ db_table = "ica_effet_declare"
+ unique_together = (("vrsdecl_ident", "objeff_ident"),)
diff --git a/data/models/teleicare_history/ica_declaration_composition.py b/data/models/teleicare_history/ica_declaration_composition.py
new file mode 100644
index 000000000..1fbd4ec0d
--- /dev/null
+++ b/data/models/teleicare_history/ica_declaration_composition.py
@@ -0,0 +1,64 @@
+# This is an auto-generated Django model module.
+# Feel free to rename the models, but don't rename db_table values or field names.
+# Pour plus de simplicité, le nom de ces modèles suivent le nom de la table de données provenant du SICCRF,
+# initialement stockée dans un système MSSQL,
+# et non la nomenclature habituellement utilisée par Compl'Alim
+
+from django.db import models
+
+
+class IcaIngredient(models.Model):
+ ingr_ident = models.IntegerField(primary_key=True)
+ vrsdecl_ident = models.IntegerField()
+ fctingr_ident = models.IntegerField()
+ tying_ident = models.IntegerField()
+ ingr_commentaires = models.TextField(blank=True, null=True)
+
+ class Meta:
+ managed = False
+ db_table = "ica_ingredient"
+
+
+class IcaIngredientAutre(models.Model):
+ ingr_ident = models.IntegerField(primary_key=True)
+ inga_ident = models.IntegerField()
+
+ class Meta:
+ managed = False
+ db_table = "ica_ingredient_autre"
+
+
+class IcaMicroOrganisme(models.Model):
+ ingr_ident = models.IntegerField(primary_key=True)
+ morg_ident = models.IntegerField()
+ ingmorg_souche = models.TextField(blank=True, null=True)
+ ingmorg_quantite_par_djr = models.BigIntegerField(blank=True, null=True)
+
+ class Meta:
+ managed = False
+ db_table = "ica_micro_organisme"
+
+
+class IcaPreparation(models.Model):
+ ingr_ident = models.IntegerField(primary_key=True)
+ plte_ident = models.IntegerField()
+ pplan_ident = models.IntegerField(blank=True, null=True)
+ unt_ident = models.IntegerField(blank=True, null=True)
+ typprep_ident = models.IntegerField(blank=True, null=True)
+ prepa_qte = models.FloatField(blank=True, null=True)
+
+ class Meta:
+ managed = False
+ db_table = "ica_preparation"
+
+
+class IcaSubstanceDeclaree(models.Model):
+ vrsdecl_ident = models.IntegerField()
+ sbsact_ident = models.IntegerField()
+ sbsact_commentaires = models.TextField(blank=True, null=True)
+ sbsactdecl_quantite_par_djr = models.FloatField(blank=True, null=True)
+
+ class Meta:
+ managed = False
+ db_table = "ica_substance_declaree"
+ unique_together = (("vrsdecl_ident", "sbsact_ident"),)
diff --git a/data/schemas/CHANGELOG_declarations.md b/data/schemas/CHANGELOG_declarations.md
index e4b5a7c3b..9a4ce57e5 100644
--- a/data/schemas/CHANGELOG_declarations.md
+++ b/data/schemas/CHANGELOG_declarations.md
@@ -2,6 +2,12 @@
Tous les changements notables apportés aux jeux de données exposés sur data.gouv.fr vont être documentés ici.
+## 2025-01-23
+
+## Modification
+ - Remplacer dans la colonne `Facteurs Risques` les valeurs 'Autres (à préciser)' par les valeurs renseignées par l'utilisateur
+ - Remplacer dans la colonne `Objectif Effets` les valeurs 'Autres (à préciser)' par les valeurs renseignées par l'utilisateur
+
## 2025-01-16
## Ajout
diff --git a/data/schemas/schema_declarations.json b/data/schemas/schema_declarations.json
index 310598270..bf7b2c478 100644
--- a/data/schemas/schema_declarations.json
+++ b/data/schemas/schema_declarations.json
@@ -1,221 +1,222 @@
{
+ "$schema": "https://frictionlessdata.io/schemas/table-schema.json",
"dialect": {
"csv": {
"delimiter": ";"
}
},
"encoding": "utf-8-sig",
+ "fields": [
+ {
+ "constraints": {
+ "required": true
+ },
+ "description": "Identifiant unique permettant de référencer une déclaration d'un complément alimentaire",
+ "example": "3211",
+ "name": "id",
+ "title": "Identifiant",
+ "type": "integer"
+ },
+ {
+ "constraints": {
+ "required": true
+ },
+ "description": "Décision prise suite à la déclaration du complément alimentaire. Indique s'il est autorisé ou refusé à la vente",
+ "example": "autorisé",
+ "name": "decision",
+ "title": "Décision",
+ "type": "string"
+ },
+ {
+ "constraints": {
+ "required": true
+ },
+ "description": "Date de décision sur la déclaration du complément alimentaire.",
+ "example": "2024-01-27",
+ "name": "date_decision",
+ "title": "Date décision",
+ "type": "date"
+ },
+ {
+ "constraints": {
+ "required": true
+ },
+ "description": "Nom commercial du produit (fourni par le fabricant)",
+ "example": "Compl'Alimentaires",
+ "name": "nom_commercial",
+ "title": "Nom commercial",
+ "type": "string"
+ },
+ {
+ "description": "Marque du fabricant sous laquelle est vendue le complément alimentaire (fournie par le fabricant)",
+ "example": "Nutra Power",
+ "name": "marque",
+ "title": "Marque",
+ "type": "string"
+ },
+ {
+ "description": "Gamme de la marque du fabricant dans laquelle le produit est vendu (fournie par le fabricant)",
+ "example": "Premium",
+ "name": "gamme",
+ "title": "Gamme",
+ "type": "string"
+ },
+ {
+ "constraints": {
+ "required": true
+ },
+ "description": "Nom de l'entreprise responsable de la mise sur le marché du complément alimentaire",
+ "example": "Compl Corp",
+ "name": "responsable_mise_sur_marche",
+ "title": "Responsable de la mise sur le marché",
+ "type": "string"
+ },
+ {
+ "constraints": {
+ "pattern": "^[0-9]{14}$",
+ "required": true
+ },
+ "description": "Siret de l'entreprise responsable de la mise sur le marché du complément alimentaire",
+ "example": "11007001800012",
+ "name": "siret_responsable_mise_sur_marche",
+ "title": "Siret responsable de la mise sur le marché",
+ "type": "integer"
+ },
+ {
+ "constraints": {
+ "enum": [
+ "15",
+ "16",
+ "17",
+ "18"
+ ],
+ "required": true
+ },
+ "description": "Réference de l'article juridique qui encadre la déclaration du produit. La déclaration du complément ne suit pas la même procédure en fonction de l'article dans lequel il tombe. Voir sur : https://www.legifrance.gouv.fr/loda/id/JORFTEXT000000638341",
+ "example": "15",
+ "name": "article_procedure",
+ "title": "Article de procédure",
+ "type": "integer"
+ },
+ {
+ "constraints": {
+ "required": true
+ },
+ "description": "La forme galénique correspond à la forme sous laquelle le complément alimentaire se présente (comprimé, gélule, sirop...). Elle est spécialement conçue pour la voie d’administration à laquelle le complément est destiné. (source : sante.gouv.fr)",
+ "example": "Sirop",
+ "name": "forme_galenique",
+ "title": "Forme galénique",
+ "type": "string"
+ },
+ {
+ "constraints": {
+ "required": true
+ },
+ "description": "Dose journalière recommandée (DJR) (fournie par le fabricant). Les quantités des composants sont renseignées par DJR.",
+ "example": "16 gouttes",
+ "name": "dose_journaliere",
+ "title": "Dose journalière",
+ "type": "string"
+ },
+ {
+ "description": "Recommandation d'emploi (fournies par le fabricant)",
+ "example": "4 gouttes 4 fois par jour dans un verre d'eau",
+ "name": "mode_emploi",
+ "title": "Mode Emploi",
+ "type": "string"
+ },
+ {
+ "description": "Mise en garde et avertissement (fournies par le fabricant). Certains avertissements doivent obligatoirement être notés sur l'étiquettage.",
+ "example": "Ne pas dépasser la dose journalière recommandée. Tenir hors de portée des jeunes enfants. Ne peut se substituer à un régime alimentaire varié.",
+ "name": "mises_en_garde",
+ "title": "Mises en garde",
+ "type": "string"
+ },
+ {
+ "description": "Objectifs du complément alimentaire (fourni par le fabricant)",
+ "name": "objectif_effet",
+ "type": "array"
+ },
+ {
+ "description": "Arômes (fourni par le fabricant)",
+ "example": "Orange",
+ "name": "aromes",
+ "title": "Arômes",
+ "type": "string"
+ },
+ {
+ "description": "La consommation du complément alimentaire est déconseillée pour les populations présentant ce ou ces facteurs",
+ "example": "Hypertension, Maladie rénale",
+ "name": "facteurs_risques",
+ "title": "facteurs_risques",
+ "type": "array"
+ },
+ {
+ "description": "Les populations ciblées par le complément alimentaire déclaré. Le fabricant choisi parmi une liste fournie par Compl'Alim",
+ "example": "Femmes enceintes, Femmes allaitantes",
+ "name": "populations_cibles",
+ "title": "Population cibles",
+ "type": "array"
+ },
+ {
+ "description": "Liste des plantes actives composants le complément alimentaire. Pour chaque plante nous retrouvons son nom en latin, sa famille, la partie utilisée ainsi que la quantité par DJR.",
+ "example": "{'nom': 'Juglans regia L.', 'partie': 'Fleur', 'preparation': 'Alcoolat', 'quantité_par_djr': '2', 'unité': 'ml'}",
+ "name": "plantes",
+ "title": "Plantes",
+ "type": "array"
+ },
+ {
+ "description": "Liste des plantes/autres ingrédients inactifs composants le complément alimentaire. Pour chaque plante nous retrouvons son nom en latin, sa famille et la partie utilisée",
+ "example": "carotte, miel, fibre",
+ "name": "ingredients_inactifs",
+ "title": "Ingrédients inactifs",
+ "type": "array"
+ },
+ {
+ "description": "Micro organismes composants le complément alimentaire. Si le micro-organisme a été inactivé, il n'est pas nécessaire de renseigner la quantité.",
+ "example": "{'genre': 'Lactobacillus', 'espèce': 'lactis', 'souche': '', 'quantité_par_djr': '2', 'unité': 'cfu', 'inactivé': 'True'}",
+ "name": "micro_organismes",
+ "type": "array"
+ },
+ {
+ "description": "Additifs",
+ "example": "E416",
+ "name": "additifs",
+ "type": "array"
+ },
+ {
+ "description": "Nutriments (ou forme d'apports)",
+ "example": "Cyanocobalamine",
+ "name": "nutriments",
+ "type": "array"
+ },
+ {
+ "description": "Autres ingrédients actifs",
+ "example": "dextrine",
+ "name": "autres_ingredients_actifs",
+ "title": "Autres ingredients actifs",
+ "type": "array"
+ },
+ {
+ "description": "Substances (composant actif)",
+ "example": "['nom': 'cafféine', 'quantité_par_djr': '2', 'unité': 'ml']",
+ "name": "substances",
+ "title": "Substances",
+ "type": "array"
+ },
+ {
+ "description": "Lien vers une image de l'étiquette (limité à 4Mb)",
+ "example": "www.lienverslimage.com",
+ "name": "etiquette",
+ "title": "Etiquette",
+ "type": "string"
+ }
+ ],
"format": "csv",
+ "homepage": "https://github.com/betagouv/complements-alimentaires",
"mediatype": "text/csv",
"name": "liste-des-complements-alimentaires-declares",
- "schema": {
- "fields": [
- {
- "constraints": {
- "required": "True"
- },
- "description": "Identifiant unique permettant de référencer une déclaration d'un complément alimentaire",
- "example": "3211",
- "name": "id",
- "title": "Identifiant",
- "type": "integer"
- },
- {
- "constraints": {
- "required": "True"
- },
- "description": "Décision prise suite à la déclaration du complément alimentaire. Indique s'il est autorisé ou refusé à la vente",
- "example": "autorisé",
- "name": "decision",
- "title": "Décision",
- "type": "string"
- },
- {
- "constraints": {
- "required": "True"
- },
- "description": "Date de décision sur la déclaration du complément alimentaire.",
- "example": "2024-01-27",
- "name": "date_decision",
- "title": "Date décision",
- "type": "date"
- },
- {
- "constraints": {
- "required": "True"
- },
- "description": "Nom commercial du produit (fourni par le fabricant)",
- "example": "Compl'Alimentaires",
- "name": "nom_commercial",
- "title": "Nom commercial",
- "type": "string"
- },
- {
- "description": "Marque du fabricant sous laquelle est vendue le complément alimentaire (fournie par le fabricant)",
- "example": "Nutra Power",
- "name": "marque",
- "title": "Marque",
- "type": "string"
- },
- {
- "description": "Gamme de la marque du fabricant dans laquelle le produit est vendu (fournie par le fabricant)",
- "example": "Premium",
- "name": "gamme",
- "title": "Gamme",
- "type": "string"
- },
- {
- "constraints": {
- "required": "True"
- },
- "description": "Nom de l'entreprise responsable de la mise sur le marché du complément alimentaire",
- "example": "Compl Corp",
- "name": "responsable_mise_sur_marche",
- "title": "Responsable de la mise sur le marché",
- "type": "string"
- },
- {
- "constraints": {
- "pattern": "^[0-9]{14}$",
- "required": true
- },
- "description": "Siret de l'entreprise responsable de la mise sur le marché du complément alimentaire",
- "example": "11007001800012",
- "name": "siret_responsable_mise_sur_marche",
- "title": "Siret responsable de la mise sur le marché",
- "type": "integer"
- },
- {
- "constraints": {
- "enum": [
- "15",
- "16",
- "17",
- "18"
- ],
- "required": "True"
- },
- "description": "Réference de l'article juridique qui encadre la déclaration du produit. La déclaration du complément ne suit pas la même procédure en fonction de l'article dans lequel il tombe. Voir sur : https://www.legifrance.gouv.fr/loda/id/JORFTEXT000000638341",
- "example": "15",
- "name": "article_procedure",
- "title": "Article de procédure",
- "type": "integer"
- },
- {
- "constraints": {
- "required": "True"
- },
- "description": "La forme galénique correspond à la forme sous laquelle le complément alimentaire se présente (comprimé, gélule, sirop...). Elle est spécialement conçue pour la voie d’administration à laquelle le complément est destiné. (source : sante.gouv.fr)",
- "example": "Sirop",
- "name": "forme_galenique",
- "title": "Forme galénique",
- "type": "string"
- },
- {
- "constraints": {
- "required": "True"
- },
- "description": "Dose journalière recommandée (DJR) (fournie par le fabricant). Les quantités des composants sont renseignées par DJR.",
- "example": "16 gouttes",
- "name": "dose_journaliere",
- "title": "Dose journalière",
- "type": "string"
- },
- {
- "description": "Recommandation d'emploi (fournies par le fabricant)",
- "example": "4 gouttes 4 fois par jour dans un verre d'eau",
- "name": "mode_emploi",
- "title": "Mode Emploi",
- "type": "string"
- },
- {
- "description": "Mise en garde et avertissement (fournies par le fabricant). Certains avertissements doivent obligatoirement être notés sur l'étiquettage.",
- "example": "Ne pas dépasser la dose journalière recommandée. Tenir hors de portée des jeunes enfants. Ne peut se substituer à un régime alimentaire varié.",
- "name": "mises_en_garde",
- "title": "Mises en garde",
- "type": "string"
- },
- {
- "description": "Objectifs du complément alimentaire (fourni par le fabricant)",
- "name": "objectif_effet",
- "type": "array"
- },
- {
- "description": "Arômes (fourni par le fabricant)",
- "example": "Orange",
- "name": "aromes",
- "title": "Arômes",
- "type": "string"
- },
- {
- "description": "La consommation du complément alimentaire est déconseillée pour les populations présentant ce ou ces facteurs",
- "example": "Hypertension, Maladie rénale",
- "name": "facteurs_risques",
- "title": "facteurs_risques",
- "type": "array"
- },
- {
- "description": "Les populations ciblées par le complément alimentaire déclaré. Le fabricant choisi parmi une liste fournie par Compl'Alim",
- "example": "Femmes enceintes, Femmes allaitantes",
- "name": "populations_cibles",
- "title": "Population cibles",
- "type": "array"
- },
- {
- "description": "Liste des plantes actives composants le complément alimentaire. Pour chaque plante nous retrouvons son nom en latin, sa famille, la partie utilisée ainsi que la quantité par DJR.",
- "example": "{'nom': 'Juglans regia L.', 'partie': 'Fleur', 'preparation': 'Alcoolat', 'quantité_par_djr': '2', 'unité': 'ml'}",
- "name": "plantes",
- "title": "Plantes",
- "type": "array"
- },
- {
- "description": "Liste des plantes/autres ingrédients inactifs composants le complément alimentaire. Pour chaque plante nous retrouvons son nom en latin, sa famille et la partie utilisée",
- "example": "carotte, miel, fibre",
- "name": "ingredients_inactifs",
- "title": "Ingrédients inactifs",
- "type": "array"
- },
- {
- "description": "Micro organismes composants le complément alimentaire. Si le micro-organisme a été inactivé, il n'est pas nécessaire de renseigner la quantité.",
- "example": "{'genre': 'Lactobacillus', 'espèce': 'lactis', 'souche': '', 'quantité_par_djr': '2', 'unité': 'cfu', 'inactivé': 'True'}",
- "name": "micro_organismes",
- "type": "array"
- },
- {
- "description": "Additifs",
- "example": "E416",
- "name": "additifs",
- "type": "array"
- },
- {
- "description": "Nutriments (ou forme d'apports)",
- "example": "Cyanocobalamine",
- "name": "nutriments",
- "type": "array"
- },
- {
- "description": "Autres ingrédients actifs",
- "example": "dextrine",
- "name": "autres_ingredients_actifs",
- "title": "Autres ingredients actifs",
- "type": "array"
- },
- {
- "description": "Substances (composant actif)",
- "example": "['nom': 'cafféine', 'quantité_par_djr': '2', 'unité': 'ml']",
- "name": "substances",
- "title": "Substances",
- "type": "array"
- },
- {
- "description": "Lien vers une image de l'étiquette (limité à 4Mb)",
- "example": "www.lienverslimage.com",
- "name": "etiquette",
- "title": "Etiquette",
- "type": "string"
- }
- ]
- },
+ "path": "https://raw.githubusercontent.com/betagouv/complements-alimentaires/refs/heads/staging/data/schemas/schema_declarations.json",
"scheme": "file",
"type": "table"
}
diff --git a/data/tests/test_declaration.py b/data/tests/test_declaration.py
index 5d87cc1d5..91e25be4e 100644
--- a/data/tests/test_declaration.py
+++ b/data/tests/test_declaration.py
@@ -135,7 +135,7 @@ def test_article_empty(self):
self.assertIsNone(declaration.article)
self.assertEqual(declaration.calculated_article, "")
- self.assertEqual(declaration.overriden_article, "")
+ self.assertEqual(declaration.overridden_article, "")
def test_article_15(self):
declaration = InstructionReadyDeclarationFactory(
@@ -152,7 +152,7 @@ def test_article_15(self):
self.assertEqual(declaration.article, Declaration.Article.ARTICLE_15)
self.assertEqual(declaration.calculated_article, Declaration.Article.ARTICLE_15)
- self.assertEqual(declaration.overriden_article, "")
+ self.assertEqual(declaration.overridden_article, "")
def test_article_15_warning(self):
"""
@@ -173,7 +173,7 @@ def test_article_15_warning(self):
declaration_with_risky_substance.refresh_from_db()
self.assertEqual(declaration_with_risky_substance.article, Declaration.Article.ARTICLE_15_WARNING)
self.assertEqual(declaration_with_risky_substance.calculated_article, Declaration.Article.ARTICLE_15_WARNING)
- self.assertEqual(declaration_with_risky_substance.overriden_article, "")
+ self.assertEqual(declaration_with_risky_substance.overridden_article, "")
declaration_with_risky_prepared_plant = InstructionReadyDeclarationFactory(
declared_plants=[],
@@ -188,7 +188,7 @@ def test_article_15_warning(self):
self.assertEqual(
declaration_with_risky_prepared_plant.calculated_article, Declaration.Article.ARTICLE_15_WARNING
)
- self.assertEqual(declaration_with_risky_prepared_plant.overriden_article, "")
+ self.assertEqual(declaration_with_risky_prepared_plant.overridden_article, "")
risky_galenic_formulation = GalenicFormulationFactory(is_risky=True)
declaration_with_risky_galenic_formulation = InstructionReadyDeclarationFactory(
@@ -202,7 +202,7 @@ def test_article_15_warning(self):
self.assertEqual(
declaration_with_risky_galenic_formulation.calculated_article, Declaration.Article.ARTICLE_15_WARNING
)
- self.assertEqual(declaration_with_risky_galenic_formulation.overriden_article, "")
+ self.assertEqual(declaration_with_risky_galenic_formulation.overridden_article, "")
risky_target_population = PopulationFactory(is_defined_by_anses=True)
declaration_with_risky_population = InstructionReadyDeclarationFactory(
@@ -218,7 +218,7 @@ def test_article_15_warning(self):
self.assertEqual(
declaration_with_risky_population.calculated_article, Declaration.Article.ARTICLE_15_HIGH_RISK_POPULATION
)
- self.assertEqual(declaration_with_risky_population.overriden_article, "")
+ self.assertEqual(declaration_with_risky_population.overridden_article, "")
def test_article_15_override(self):
declaration = InstructionReadyDeclarationFactory(
@@ -230,14 +230,14 @@ def test_article_15_override(self):
)
# La PlantFactory utilisée dans DeclaredPlantFactory a par défaut un status = AUTHORIZED
DeclaredPlantFactory(new=False, declaration=declaration)
- declaration.overriden_article = Declaration.Article.ARTICLE_16
+ declaration.overridden_article = Declaration.Article.ARTICLE_16
declaration.assign_calculated_article()
declaration.save()
declaration.refresh_from_db()
self.assertEqual(declaration.article, Declaration.Article.ARTICLE_16)
self.assertEqual(declaration.calculated_article, Declaration.Article.ARTICLE_15)
- self.assertEqual(declaration.overriden_article, Declaration.Article.ARTICLE_16)
+ self.assertEqual(declaration.overridden_article, Declaration.Article.ARTICLE_16)
def test_article_16(self):
"""
@@ -259,7 +259,7 @@ def test_article_16(self):
self.assertEqual(declaration_new.article, Declaration.Article.ARTICLE_16)
self.assertEqual(declaration_new.calculated_article, Declaration.Article.ARTICLE_16)
- self.assertEqual(declaration_new.overriden_article, "")
+ self.assertEqual(declaration_new.overridden_article, "")
declaration_not_autorized = InstructionReadyDeclarationFactory(
declared_plants=[],
@@ -277,7 +277,7 @@ def test_article_16(self):
self.assertEqual(declaration_not_autorized.article, Declaration.Article.ARTICLE_16)
self.assertEqual(declaration_not_autorized.calculated_article, Declaration.Article.ARTICLE_16)
- self.assertEqual(declaration_not_autorized.overriden_article, "")
+ self.assertEqual(declaration_not_autorized.overridden_article, "")
def test_article_anses_referal(self):
SUBSTANCE_MAX_QUANTITY = 1.0
@@ -298,7 +298,7 @@ def test_article_anses_referal(self):
self.assertEqual(
declaration_with_computed_substance_max_exceeded.calculated_article, Declaration.Article.ANSES_REFERAL
)
- self.assertEqual(declaration_with_computed_substance_max_exceeded.overriden_article, "")
+ self.assertEqual(declaration_with_computed_substance_max_exceeded.overridden_article, "")
# La déclaration ne doit pas passer en saisine ANSES si la dose est exactement égale à la dose maximale
declaration_with_computed_substance_equals_max = InstructionReadyDeclarationFactory(
@@ -317,7 +317,7 @@ def test_article_anses_referal(self):
self.assertEqual(
declaration_with_computed_substance_equals_max.calculated_article, Declaration.Article.ARTICLE_15
)
- self.assertEqual(declaration_with_computed_substance_equals_max.overriden_article, "")
+ self.assertEqual(declaration_with_computed_substance_equals_max.overridden_article, "")
declaration_with_declared_substance_max_exceeded = InstructionReadyDeclarationFactory(
computed_substances=[],
@@ -335,7 +335,7 @@ def test_article_anses_referal(self):
self.assertEqual(
declaration_with_declared_substance_max_exceeded.calculated_article, Declaration.Article.ANSES_REFERAL
)
- self.assertEqual(declaration_with_declared_substance_max_exceeded.overriden_article, "")
+ self.assertEqual(declaration_with_declared_substance_max_exceeded.overridden_article, "")
def test_visa_refused(self):
"""
diff --git a/data/tests/test_teleicare_history_importer.py b/data/tests/test_teleicare_history_importer.py
index e3d0e4909..81a17121d 100644
--- a/data/tests/test_teleicare_history_importer.py
+++ b/data/tests/test_teleicare_history_importer.py
@@ -123,7 +123,9 @@ def test_create_new_companies(self):
self.assertEqual(created_company.postal_code, etablissement_to_create_as_company.etab_adre_cp)
self.assertEqual(created_company.city, etablissement_to_create_as_company.etab_adre_ville)
- def test_create_declaration_from_history(self):
+ @patch("data.etl.teleicare_history.extractor.add_composition_from_teleicare_history")
+ @patch("data.etl.teleicare_history.extractor.add_product_info_from_teleicare_history")
+ def test_create_declaration_from_history(self, mocked_add_composition_function, mocked_add_product_function):
"""
Les déclarations sont créées à partir d'object historiques des modèles Ica_
"""
@@ -136,7 +138,7 @@ def test_create_declaration_from_history(self):
CA_to_create_as_declaration = ComplementAlimentaireFactory(
etab=etablissement_to_create_as_company, frmgal_ident=galenic_formulation_id
)
- declaration_to_create_as_declaration = DeclarationFactory(cplalim=CA_to_create_as_declaration)
+ declaration_to_create_as_declaration = DeclarationFactory(cplalim=CA_to_create_as_declaration, tydcl_ident=1)
version_declaration_to_create_as_declaration = VersionDeclarationFactory(
dcl=declaration_to_create_as_declaration,
stadcl_ident=8,
@@ -157,6 +159,7 @@ def test_create_declaration_from_history(self):
self.assertEqual(created_declaration.galenic_formulation, galenic_formulation)
self.assertEqual(created_declaration.unit_quantity, 32)
self.assertEqual(created_declaration.unit_measurement, unit)
+ self.assertEqual(created_declaration.article, Declaration.Article.ARTICLE_15)
self.assertEqual(
created_declaration.conditioning, version_declaration_to_create_as_declaration.vrsdecl_conditionnement
)
@@ -165,5 +168,6 @@ def test_create_declaration_from_history(self):
str(version_declaration_to_create_as_declaration.vrsdecl_poids_uc),
)
self.assertEqual(
- created_declaration.minimum_duration, str(version_declaration_to_create_as_declaration.vrsdecl_durabilite)
+ created_declaration.minimum_duration,
+ str(version_declaration_to_create_as_declaration.vrsdecl_durabilite),
)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b5d0054da..2376e1009 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -5,20 +5,20 @@
"packages": {
"": {
"dependencies": {
- "@gouvminint/vue-dsfr": "^8.1.0",
+ "@gouvminint/vue-dsfr": "^8.1.1",
"@vue/cli": "^5.0.8",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
- "@vueuse/core": "^12.4.0",
+ "@vueuse/core": "^12.5.0",
"core-js": "^3.40.0",
- "pinia": "^2.3.0",
+ "pinia": "^2.3.1",
"vue": "^3.5.13",
"vue-matomo": "^4.2.0",
"vue-router": "^4.5.0",
"webpack-bundle-tracker": "^3.1.1"
},
"devDependencies": {
- "@babel/core": "^7.26.0",
+ "@babel/core": "^7.26.7",
"@babel/eslint-parser": "^7.26.5",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
@@ -244,9 +244,10 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz",
- "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==",
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+ "license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
"js-tokens": "^4.0.0",
@@ -257,28 +258,30 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.0.tgz",
- "integrity": "sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==",
+ "version": "7.26.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz",
+ "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
- "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
+ "version": "7.26.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz",
+ "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==",
+ "license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.26.0",
- "@babel/generator": "^7.26.0",
- "@babel/helper-compilation-targets": "^7.25.9",
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.5",
+ "@babel/helper-compilation-targets": "^7.26.5",
"@babel/helper-module-transforms": "^7.26.0",
- "@babel/helpers": "^7.26.0",
- "@babel/parser": "^7.26.0",
+ "@babel/helpers": "^7.26.7",
+ "@babel/parser": "^7.26.7",
"@babel/template": "^7.25.9",
- "@babel/traverse": "^7.25.9",
- "@babel/types": "^7.26.0",
+ "@babel/traverse": "^7.26.7",
+ "@babel/types": "^7.26.7",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -329,12 +332,13 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz",
- "integrity": "sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==",
+ "version": "7.26.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
+ "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
+ "license": "MIT",
"dependencies": {
- "@babel/parser": "^7.26.0",
- "@babel/types": "^7.26.0",
+ "@babel/parser": "^7.26.5",
+ "@babel/types": "^7.26.5",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
@@ -366,11 +370,12 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
- "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
+ "version": "7.26.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz",
+ "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==",
+ "license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.25.9",
+ "@babel/compat-data": "^7.26.5",
"@babel/helper-validator-option": "^7.25.9",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
@@ -666,23 +671,25 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
- "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+ "version": "7.26.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
+ "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
+ "license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
- "@babel/types": "^7.26.0"
+ "@babel/types": "^7.26.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.26.1",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz",
- "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==",
+ "version": "7.26.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
+ "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.26.0"
+ "@babel/types": "^7.26.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2083,15 +2090,16 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz",
- "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==",
+ "version": "7.26.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
+ "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/generator": "^7.25.9",
- "@babel/parser": "^7.25.9",
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.26.5",
+ "@babel/parser": "^7.26.7",
"@babel/template": "^7.25.9",
- "@babel/types": "^7.25.9",
+ "@babel/types": "^7.26.7",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -2100,9 +2108,10 @@
}
},
"node_modules/@babel/types": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
- "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+ "version": "7.26.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
+ "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
+ "license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
@@ -2215,9 +2224,10 @@
}
},
"node_modules/@gouvminint/vue-dsfr": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/@gouvminint/vue-dsfr/-/vue-dsfr-8.1.0.tgz",
- "integrity": "sha512-eF7lVhZjsKg8qSJsCxySfSAxjYS4DZU2wtjcnPUMDPxL7PFbGSvYFHqyJZ3PVMMYh5DE+9zkleqnqAJmJbhRyg==",
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@gouvminint/vue-dsfr/-/vue-dsfr-8.1.1.tgz",
+ "integrity": "sha512-L4FRp4mex7DGKRIn+ljsHynW/kvj2/dbOPkRWnxTGFrppBj3otn5jh55tRL4hK3Kn3LjEs62/6ldOcWb5Zn1kQ==",
+ "license": "MIT",
"dependencies": {
"@gouvfr/dsfr": "~1.12.1",
"focus-trap": "^7.6.2",
@@ -3897,13 +3907,14 @@
}
},
"node_modules/@vueuse/core": {
- "version": "12.4.0",
- "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.4.0.tgz",
- "integrity": "sha512-XnjQYcJwCsyXyIafyA6SvyN/OBtfPnjvJmbxNxQjCcyWD198urwm5TYvIUUyAxEAN0K7HJggOgT15cOlWFyLeA==",
+ "version": "12.5.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.5.0.tgz",
+ "integrity": "sha512-GVyH1iYqNANwcahAx8JBm6awaNgvR/SwZ1fjr10b8l1HIgDp82ngNbfzJUgOgWEoxjL+URAggnlilAEXwCOZtg==",
+ "license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
- "@vueuse/metadata": "12.4.0",
- "@vueuse/shared": "12.4.0",
+ "@vueuse/metadata": "12.5.0",
+ "@vueuse/shared": "12.5.0",
"vue": "^3.5.13"
},
"funding": {
@@ -3911,17 +3922,19 @@
}
},
"node_modules/@vueuse/metadata": {
- "version": "12.4.0",
- "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.4.0.tgz",
- "integrity": "sha512-AhPuHs/qtYrKHUlEoNO6zCXufu8OgbR8S/n2oMw1OQuBQJ3+HOLQ+EpvXs+feOlZMa0p8QVvDWNlmcJJY8rW2g==",
+ "version": "12.5.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.5.0.tgz",
+ "integrity": "sha512-Ui7Lo2a7AxrMAXRF+fAp9QsXuwTeeZ8fIB9wsLHqzq9MQk+2gMYE2IGJW48VMJ8ecvCB3z3GsGLKLbSasQ5Qlg==",
+ "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
- "version": "12.4.0",
- "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.4.0.tgz",
- "integrity": "sha512-9yLgbHVIF12OSCojnjTIoZL1+UA10+O4E1aD6Hpfo/DKVm5o3SZIwz6CupqGy3+IcKI8d6Jnl26EQj/YucnW0Q==",
+ "version": "12.5.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.5.0.tgz",
+ "integrity": "sha512-vMpcL1lStUU6O+kdj6YdHDixh0odjPAUM15uJ9f7MY781jcYkIwFA4iv2EfoIPO6vBmvutI1HxxAwmf0cx5ISQ==",
+ "license": "MIT",
"dependencies": {
"vue": "^3.5.13"
},
@@ -12224,9 +12237,10 @@
}
},
"node_modules/pinia": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz",
- "integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
+ "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+ "license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
diff --git a/frontend/package.json b/frontend/package.json
index 3f60cf6c9..df28f5881 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -8,20 +8,20 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
- "@gouvminint/vue-dsfr": "^8.1.0",
+ "@gouvminint/vue-dsfr": "^8.1.1",
"@vue/cli": "^5.0.8",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
- "@vueuse/core": "^12.4.0",
+ "@vueuse/core": "^12.5.0",
"core-js": "^3.40.0",
- "pinia": "^2.3.0",
+ "pinia": "^2.3.1",
"vue": "^3.5.13",
"vue-matomo": "^4.2.0",
"vue-router": "^4.5.0",
"webpack-bundle-tracker": "^3.1.1"
},
"devDependencies": {
- "@babel/core": "^7.26.0",
+ "@babel/core": "^7.26.7",
"@babel/eslint-parser": "^7.26.5",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 35ca57b09..558594103 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,22 +1,24 @@
- Compl'Alim Compl'Alim