diff --git a/data/etl/teleicare_history/extractor.py b/data/etl/teleicare_history/extractor.py index b96e8c9ec..acdec9f03 100644 --- a/data/etl/teleicare_history/extractor.py +++ b/data/etl/teleicare_history/extractor.py @@ -156,6 +156,7 @@ def create_declaration_from_teleicare_history(): * télédéclarante de la déclaration (cette relation n'est pour le moment pas conservée, car le BEPIAS ne sait pas ce qu'elle signifie) """ nb_created_declarations = 0 + for ica_complement_alimentaire in IcaComplementAlimentaire.objects.all(): # retrouve la déclaration la plus à jour correspondant à ce complément alimentaire all_ica_declarations = IcaDeclaration.objects.filter(cplalim_id=ica_complement_alimentaire.cplalim_ident) @@ -207,10 +208,15 @@ def create_declaration_from_teleicare_history(): # other_conditions= # effects= # other_effects= + # address= + # postal_code= + # city= + # country= status=Declaration.DeclarationStatus.WITHDRAWN if latest_ica_declaration.dcl_date_fin_commercialisation else DECLARATION_STATUS_MAPPING[latest_ica_version_declaration.stattdcl_ident], ) + try: declaration.save() nb_created_declarations += 1 diff --git a/data/factories/teleicare_history/__init__.py b/data/factories/teleicare_history/__init__.py index 3494cff77..a446b86cd 100644 --- a/data/factories/teleicare_history/__init__.py +++ b/data/factories/teleicare_history/__init__.py @@ -3,14 +3,32 @@ import factory import faker + from phonenumber_field.phonenumber import PhoneNumber +from datetime import datetime, timedelta +from random import randrange from data.choices import CountryChoices from data.models.teleicare_history.ica_etablissement import IcaEtablissement +from data.models.teleicare_history.ica_declaration import ( + IcaComplementAlimentaire, + IcaDeclaration, + IcaVersionDeclaration, +) from data.utils.string_utils import make_random_str from data.factories.company import _make_siret, _make_vat, _make_phone_number +def random_date(start, end=datetime.now()): + """ + Retourne une date random entre une date de début et une date de fin + """ + delta = end - start + int_delta = (delta.days * 24 * 60 * 60) + delta.seconds + random_second = randrange(int_delta) + return start + timedelta(seconds=random_second) + + class EtablissementFactory(factory.django.DjangoModelFactory): class Meta: model = IcaEtablissement @@ -28,3 +46,71 @@ class Meta: etab_adre_ville = factory.Faker("city", locale="FR") etab_adre_cp = factory.Faker("postcode", locale="FR") etab_adre_voie = factory.Faker("street_address", locale="FR") + + +class ComplementAlimentaireFactory(factory.django.DjangoModelFactory): + class Meta: + model = IcaComplementAlimentaire + + cplalim_ident = factory.Sequence(lambda n: n + 1) + frmgal_ident = factory.Faker("pyint", min_value=0, max_value=20) + etab = factory.SubFactory(EtablissementFactory) + cplalim_marque = factory.Faker("text", max_nb_chars=20) + cplalim_gamme = factory.Faker("text", max_nb_chars=20) + cplalim_nom = factory.Faker("text", max_nb_chars=20) + dclencours_gout_arome_parfum = factory.Faker("text", max_nb_chars=20) + cplalim_forme_galenique_autre = factory.Faker("text", max_nb_chars=20) + + +class DeclarationFactory(factory.django.DjangoModelFactory): + class Meta: + model = IcaDeclaration + + dcl_ident = factory.Sequence(lambda n: n + 1) + cplalim = factory.SubFactory(ComplementAlimentaireFactory) + tydcl_ident = factory.Faker("pyint", min_value=0, max_value=20) + 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") + dcl_date_fin_commercialisation = factory.LazyFunction( + lambda: datetime.strftime(random_date(start=datetime(2016, 1, 1)), "%m/%d/%Y %H:%M:%S %p") + if random.random() > 0.3 + else None + ) + + +class VersionDeclarationFactory(factory.django.DjangoModelFactory): + class Meta: + model = IcaVersionDeclaration + + vrsdecl_ident = factory.Sequence(lambda n: n + 1) + ag_ident = factory.Faker("pyint", min_value=0, max_value=20) + typvrs_ident = factory.Faker("pyint", min_value=0, max_value=20) + unt_ident = factory.Faker("pyint", min_value=0, max_value=20) + pays_ident_adre = factory.Faker("pyint", min_value=0, max_value=8) + etab = factory.SubFactory(EtablissementFactory) + pays_ident_pays_de_reference = factory.Faker("pyint", min_value=0, max_value=8) + dcl = factory.SubFactory(DeclarationFactory) + stattdcl_ident = factory.Faker("pyint", min_value=0, max_value=8) + stadcl_ident = factory.Faker("pyint", min_value=0, max_value=8) + vrsdecl_numero = factory.Faker("pyint", min_value=0, max_value=20) + vrsdecl_commentaires = factory.Faker("text", max_nb_chars=20) + vrsdecl_mise_en_garde = factory.Faker("text", max_nb_chars=20) + vrsdecl_durabilite = factory.Faker("pyint", min_value=0, max_value=8) + vrsdecl_mode_emploi = factory.Faker("text", max_nb_chars=20) + vrsdecl_djr = factory.fuzzy.FuzzyText(length=4, chars=string.ascii_uppercase + string.digits) + vrsdecl_conditionnement = factory.Faker("text", max_nb_chars=20) + vrsdecl_poids_uc = factory.Faker("pyfloat") + vrsdecl_forme_galenique_autre = factory.Faker("text", max_nb_chars=20) + vrsdecl_date_limite_reponse_pro = factory.Faker("text", max_nb_chars=20) + vrsdecl_observations_ac = factory.Faker("text", max_nb_chars=20) + vrsdecl_observations_pro = factory.Faker("text", max_nb_chars=20) + vrsdecl_numero_dossiel = factory.Faker("text", max_nb_chars=20) + vrsdecl_adre_ville = factory.Faker("text", max_nb_chars=20) + vrsdecl_adre_cp = factory.Faker("text", max_nb_chars=20) + vrsdecl_adre_voie = factory.Faker("text", max_nb_chars=20) + vrsdecl_adre_comp = factory.Faker("text", max_nb_chars=20) + vrsdecl_adre_comp2 = factory.Faker("text", max_nb_chars=20) + vrsdecl_adre_dist = factory.Faker("text", max_nb_chars=20) + vrsdecl_adre_region = factory.Faker("text", max_nb_chars=20) + vrsdecl_adre_raison_sociale = factory.Faker("text", max_nb_chars=20) diff --git a/data/models/teleicare_history/ica_declaration.py b/data/models/teleicare_history/ica_declaration.py index 47c68d89e..f235b3099 100644 --- a/data/models/teleicare_history/ica_declaration.py +++ b/data/models/teleicare_history/ica_declaration.py @@ -41,10 +41,10 @@ class IcaDeclaration(models.Model): ) # correspond à l'entreprise gestionnaire de la déclaration etab_ident_rmm_declarant = models.IntegerField() dcl_date = models.TextField() - dcl_saisie_administration = models.BooleanField() - dcl_annee = models.IntegerField() - dcl_mois = models.IntegerField() - dcl_numero = models.IntegerField() + dcl_saisie_administration = models.BooleanField(null=True) # rendu nullable pour simplifier les Factories + 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 dcl_date_fin_commercialisation = models.TextField(blank=True, null=True) class Meta: @@ -55,20 +55,20 @@ 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() + typvrs_ident = models.IntegerField(null=True) # 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( IcaEtablissement, on_delete=models.CASCADE, db_column="etab_ident" ) # correspond à l'entreprise télédéclarante - ex_ident = models.IntegerField() + ex_ident = models.IntegerField(null=True) # rendu nullable pour simplifier les Factories pays_ident_pays_de_reference = models.IntegerField(blank=True, null=True) dcl = models.ForeignKey( IcaDeclaration, on_delete=models.CASCADE, db_column="dcl_ident" ) # dcl_ident est aussi une foreign_key vers IcaComplementAlimentaire stattdcl_ident = models.IntegerField(blank=True, null=True) stadcl_ident = models.IntegerField(blank=True, null=True) - vrsdecl_numero = models.IntegerField() + vrsdecl_numero = models.IntegerField(null=True) vrsdecl_commentaires = models.TextField(blank=True, null=True) vrsdecl_mise_en_garde = models.TextField(blank=True, null=True) vrsdecl_durabilite = models.IntegerField(blank=True, null=True) @@ -80,9 +80,9 @@ class IcaVersionDeclaration(models.Model): vrsdecl_date_limite_reponse_pro = models.TextField(blank=True, null=True) vrsdecl_observations_ac = models.TextField(blank=True, null=True) vrsdecl_observations_pro = models.TextField(blank=True, null=True) - vrsdecl_mode_json = models.BooleanField() + vrsdecl_mode_json = models.BooleanField(null=True) # rendu nullable pour simplifier les Factories vrsdecl_numero_dossiel = models.TextField(blank=True, null=True) - vrsdecl_mode_sans_verif = models.BooleanField() + vrsdecl_mode_sans_verif = models.BooleanField(null=True) # rendu nullable pour simplifier les Factories vrsdecl_adre_ville = models.TextField(blank=True, null=True) vrsdecl_adre_cp = models.TextField(blank=True, null=True) vrsdecl_adre_voie = models.TextField(blank=True, null=True) diff --git a/data/tests/test_teleicare_history_importer.py b/data/tests/test_teleicare_history_importer.py index 724798a29..e3d0e4909 100644 --- a/data/tests/test_teleicare_history_importer.py +++ b/data/tests/test_teleicare_history_importer.py @@ -3,10 +3,26 @@ from django.db import connection from django.test import TestCase -from data.etl.teleicare_history.extractor import match_companies_on_siret_or_vat +from data.etl.teleicare_history.extractor import ( + create_declaration_from_teleicare_history, + match_companies_on_siret_or_vat, +) from data.factories.company import CompanyFactory, _make_siret, _make_vat -from data.factories.teleicare_history import EtablissementFactory +from data.factories.galenic_formulation import GalenicFormulationFactory +from data.factories.teleicare_history import ( + ComplementAlimentaireFactory, + DeclarationFactory, + EtablissementFactory, + VersionDeclarationFactory, +) +from data.factories.unit import SubstanceUnitFactory from data.models.company import Company +from data.models.declaration import Declaration +from data.models.teleicare_history.ica_declaration import ( + IcaComplementAlimentaire, + IcaDeclaration, + IcaVersionDeclaration, +) from data.models.teleicare_history.ica_etablissement import IcaEtablissement @@ -20,21 +36,24 @@ def setUp(self): Adapted from: https://stackoverflow.com/a/49800437 """ super().setUp() - with connection.schema_editor() as schema_editor: - schema_editor.create_model(IcaEtablissement) + for table in [IcaEtablissement, IcaComplementAlimentaire, IcaDeclaration, IcaVersionDeclaration]: + with connection.schema_editor() as schema_editor: + schema_editor.create_model(table) - if IcaEtablissement._meta.db_table not in connection.introspection.table_names(): - raise ValueError( - "Table `{table_name}` is missing in test database.".format( - table_name=IcaEtablissement._meta.db_table + if table._meta.db_table not in connection.introspection.table_names(): + raise ValueError( + "Table `{table_name}` is missing in test database.".format(table_name=table._meta.db_table) ) - ) def tearDown(self): super().tearDown() - - with connection.schema_editor() as schema_editor: - schema_editor.delete_model(IcaEtablissement) + for table in [IcaVersionDeclaration, IcaComplementAlimentaire, IcaDeclaration, IcaEtablissement]: + table.objects.all().delete() + # la suppression des modèles fail avec l'erreur + # django.db.utils.OperationalError: cannot DROP TABLE "ica_versiondeclaration" because it has pending trigger events + # même avec un sleep(15) + # with connection.schema_editor() as schema_editor: + # schema_editor.delete_model(table) def test_match_companies_on_siret_or_vat(self): """ @@ -90,15 +109,61 @@ def test_create_new_companies(self): """ etablissement_to_create_as_company = EtablissementFactory(etab_siret=None, etab_ica_importateur=True) - # ne sera pas créé car le numéro de téléphone est mal formatté + # devrait être créée malgré le numéro de téléphone mal formaté _ = EtablissementFactory(etab_siret=None, etab_ica_importateur=True, etab_telephone="0345") self.assertEqual(Company.objects.filter(siccrf_id=etablissement_to_create_as_company.etab_ident).count(), 0) match_companies_on_siret_or_vat(create_if_not_exist=True) - self.assertEqual(Company.objects.filter(siccrf_id=etablissement_to_create_as_company.etab_ident).count(), 1) + self.assertTrue(Company.objects.filter(siccrf_id=etablissement_to_create_as_company.etab_ident).exists()) + self.assertEqual(Company.objects.exclude(siccrf_id=None).count(), 2) created_company = Company.objects.get(siccrf_id=etablissement_to_create_as_company.etab_ident) self.assertEqual(created_company.siccrf_id, etablissement_to_create_as_company.etab_ident) self.assertEqual(created_company.address, etablissement_to_create_as_company.etab_adre_voie) 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): + """ + Les déclarations sont créées à partir d'object historiques des modèles Ica_ + """ + galenic_formulation_id = 1 + galenic_formulation = GalenicFormulationFactory(siccrf_id=galenic_formulation_id) + unit_id = 1 + unit = SubstanceUnitFactory(siccrf_id=unit_id) + etablissement_to_create_as_company = EtablissementFactory(etab_siret=None, etab_ica_importateur=True) + + 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) + version_declaration_to_create_as_declaration = VersionDeclarationFactory( + dcl=declaration_to_create_as_declaration, + stadcl_ident=8, + stattdcl_ident=2, + unt_ident=unit_id, + vrsdecl_djr="32 kg of ppo", + ) + + match_companies_on_siret_or_vat(create_if_not_exist=True) + create_declaration_from_teleicare_history() + + version_declaration_to_create_as_declaration.refresh_from_db() + created_declaration = Declaration.objects.get(siccrf_id=CA_to_create_as_declaration.cplalim_ident) + self.assertEqual(created_declaration.name, CA_to_create_as_declaration.cplalim_nom) + self.assertEqual(created_declaration.brand, CA_to_create_as_declaration.cplalim_marque) + self.assertEqual(created_declaration.gamme, CA_to_create_as_declaration.cplalim_gamme) + self.assertEqual(created_declaration.flavor, CA_to_create_as_declaration.dclencours_gout_arome_parfum) + 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.conditioning, version_declaration_to_create_as_declaration.vrsdecl_conditionnement + ) + self.assertEqual( + created_declaration.daily_recommended_dose, + 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) + ) diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index 4441e0741..2309f3d2c 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -40,6 +40,10 @@ const navItems = [ to: "/blog", text: "Ressources", }, + { + to: "/faq", + text: "FAQ", + }, ] const loggedOnlyNavItems = [ { diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 92dd853fe..ec98fb297 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -35,6 +35,7 @@ import ContactForm from "@/views/ContactForm" import CompliancePage from "@/views/CompliancePage" import DeclaredElementPage from "@/views/DeclaredElementPage" import MandatedCompaniesPage from "@/views/MandatedCompaniesPage" +import FaqPage from "@/views/FaqPage" import { ref } from "vue" const routes = [ @@ -372,6 +373,14 @@ const routes = [ title: "Conformité au droit alimentaire", }, }, + { + path: "/faq", + name: "FaqPage", + component: FaqPage, + meta: { + title: "Foire aux questions", + }, + }, { path: "/:catchAll(.*)*", // https://stackoverflow.com/a/70343919/2255491 component: NotFound, diff --git a/frontend/src/views/BlogPostPage.vue b/frontend/src/views/BlogPostPage.vue index 368ce9889..22548418c 100644 --- a/frontend/src/views/BlogPostPage.vue +++ b/frontend/src/views/BlogPostPage.vue @@ -10,9 +10,11 @@ />
-

{{ blogPost.title }}

-

{{ author }}

-

{{ date }}

+
+

{{ blogPost.title }}

+

{{ author }}

+

{{ date }}

+
diff --git a/frontend/src/views/FaqPage.vue b/frontend/src/views/FaqPage.vue new file mode 100644 index 000000000..9b3748fb5 --- /dev/null +++ b/frontend/src/views/FaqPage.vue @@ -0,0 +1,441 @@ + + + +