From eeba4c6f4bc7db142058c9f33e13149cde0c6437 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Tue, 28 Jan 2025 14:50:00 +0100 Subject: [PATCH] eligibility: Improve eligibility diagnosis data consistancy --- ...gibilitydiagnosis_author_kind_coherence.py | 36 +++++++++++++++++ ...gibilitydiagnosis_author_kind_coherence.py | 40 +++++++++++++++++++ itou/eligibility/models/geiq.py | 2 +- itou/eligibility/models/iae.py | 16 ++++++++ tests/companies/test_import_siae_command.py | 2 +- tests/eligibility/factories.py | 1 + tests/eligibility/test_admin.py | 11 +++-- tests/eligibility/test_geiq.py | 2 +- tests/siae_evaluations/test_models.py | 23 ++++++----- tests/www/apply/test_process.py | 2 +- tests/www/employees_views/test_detail.py | 4 +- 11 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 itou/eligibility/migrations/0010_add_eligibilitydiagnosis_author_kind_coherence.py create mode 100644 itou/eligibility/migrations/0011_update_geiqeligibilitydiagnosis_author_kind_coherence.py diff --git a/itou/eligibility/migrations/0010_add_eligibilitydiagnosis_author_kind_coherence.py b/itou/eligibility/migrations/0010_add_eligibilitydiagnosis_author_kind_coherence.py new file mode 100644 index 0000000000..6a3b3783e0 --- /dev/null +++ b/itou/eligibility/migrations/0010_add_eligibilitydiagnosis_author_kind_coherence.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1.5 on 2025-01-28 09:51 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("companies", "0014_company_fields_history_and_more"), + ("eligibility", "0009_fix_selectedadministrativecriteria_certification_period"), + ("prescribers", "0007_prescriberorganization_automatic_geocoding_update"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddConstraint( + model_name="eligibilitydiagnosis", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q( + ("author_kind", "employer"), + ("author_prescriber_organization__isnull", True), + ("author_siae__isnull", False), + ), + models.Q( + ("author_kind", "prescriber"), + ("author_prescriber_organization__isnull", False), + ("author_siae__isnull", True), + ), + _connector="OR", + ), + name="eligibility_iae_diagnosis_author_kind_coherence", + violation_error_message="La structure de l'auteur ne correspond pas à son type", + ), + ), + ] diff --git a/itou/eligibility/migrations/0011_update_geiqeligibilitydiagnosis_author_kind_coherence.py b/itou/eligibility/migrations/0011_update_geiqeligibilitydiagnosis_author_kind_coherence.py new file mode 100644 index 0000000000..f690687715 --- /dev/null +++ b/itou/eligibility/migrations/0011_update_geiqeligibilitydiagnosis_author_kind_coherence.py @@ -0,0 +1,40 @@ +# Generated by Django 5.1.5 on 2025-01-28 09:52 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("companies", "0014_company_fields_history_and_more"), + ("eligibility", "0010_add_eligibilitydiagnosis_author_kind_coherence"), + ("prescribers", "0007_prescriberorganization_automatic_geocoding_update"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="geiqeligibilitydiagnosis", + name="author_kind_coherence", + ), + migrations.AddConstraint( + model_name="geiqeligibilitydiagnosis", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q( + ("author_geiq__isnull", False), + ("author_kind", "geiq"), + ("author_prescriber_organization__isnull", True), + ), + models.Q( + ("author_geiq__isnull", True), + ("author_kind", "prescriber"), + ("author_prescriber_organization__isnull", False), + ), + _connector="OR", + ), + name="author_kind_coherence", + violation_error_message="La structure de l'auteur ne correspond pas à son type", + ), + ), + ] diff --git a/itou/eligibility/models/geiq.py b/itou/eligibility/models/geiq.py index 2fb9072f48..8d0cbc1b2f 100644 --- a/itou/eligibility/models/geiq.py +++ b/itou/eligibility/models/geiq.py @@ -91,7 +91,7 @@ class Meta: constraints = [ models.CheckConstraint( name="author_kind_coherence", - violation_error_message="Le diagnostic d'éligibilité GEIQ ne peut avoir 2 structures pour auteur", + violation_error_message="La structure de l'auteur ne correspond pas à son type", condition=models.Q( author_kind=AuthorKind.GEIQ, author_geiq__isnull=False, diff --git a/itou/eligibility/models/iae.py b/itou/eligibility/models/iae.py index f2fabff376..04d695cd6e 100644 --- a/itou/eligibility/models/iae.py +++ b/itou/eligibility/models/iae.py @@ -146,6 +146,22 @@ class Meta: verbose_name = "diagnostic d'éligibilité IAE" verbose_name_plural = "diagnostics d'éligibilité IAE" ordering = ["-created_at"] + constraints = [ + models.CheckConstraint( + name="eligibility_iae_diagnosis_author_kind_coherence", + violation_error_message="La structure de l'auteur ne correspond pas à son type", + condition=models.Q( + author_kind=AuthorKind.EMPLOYER, + author_siae__isnull=False, + author_prescriber_organization__isnull=True, + ) + | models.Q( + author_kind=AuthorKind.PRESCRIBER, + author_prescriber_organization__isnull=False, + author_siae__isnull=True, + ), + ), + ] @property def author_organization(self): diff --git a/tests/companies/test_import_siae_command.py b/tests/companies/test_import_siae_command.py index a38f0fb932..3d46c58a71 100644 --- a/tests/companies/test_import_siae_command.py +++ b/tests/companies/test_import_siae_command.py @@ -258,7 +258,7 @@ def test_with_eligibility_diagnosis(self): assert could_siae_be_deleted(company) # Approval with eligibility diagnosis authored by SIAE - ApprovalFactory(eligibility_diagnosis__author_siae=company) + ApprovalFactory(eligibility_diagnosis__author_siae=company, eligibility_diagnosis__from_employer=True) assert not could_siae_be_deleted(company) def test_with_job_app(self): diff --git a/tests/eligibility/factories.py b/tests/eligibility/factories.py index 67bfd8a869..35a90da125 100644 --- a/tests/eligibility/factories.py +++ b/tests/eligibility/factories.py @@ -103,6 +103,7 @@ class Params: from_employer = factory.Trait( author_kind=AuthorKind.EMPLOYER, author_siae=factory.SubFactory(CompanyFactory, subject_to_eligibility=True, with_membership=True), + author_prescriber_organization=None, author=factory.LazyAttribute(lambda obj: obj.author_siae.members.first()), ) with_certifiable_criteria = factory.Trait(romes=factory.PostGeneration(_get_iae_certifiable_criteria)) diff --git a/tests/eligibility/test_admin.py b/tests/eligibility/test_admin.py index b40a1638af..2103dfcb79 100644 --- a/tests/eligibility/test_admin.py +++ b/tests/eligibility/test_admin.py @@ -260,10 +260,13 @@ def test_add_eligibility_not_both_org_and_company(self, admin_client, kind, user response = admin_client.post(self.get_add_url(kind), data=post_data) assert response.status_code == 200 - expected_errors = [["Vous ne pouvez pas saisir une entreprise et une organisation prescriptrice."]] - if kind == "geiq": - # Additional error thanks to the db constraint - expected_errors[0].append("Le diagnostic d'éligibilité GEIQ ne peut avoir 2 structures pour auteur") + expected_errors = [ + [ + "Vous ne pouvez pas saisir une entreprise et une organisation prescriptrice.", + "La structure de l'auteur ne correspond pas à son type", + ] + ] + assert response.context["errors"] == expected_errors assert not self.get_diag_model(kind).objects.exists() diff --git a/tests/eligibility/test_geiq.py b/tests/eligibility/test_geiq.py index 0038e95002..47c69f0eb6 100644 --- a/tests/eligibility/test_geiq.py +++ b/tests/eligibility/test_geiq.py @@ -134,7 +134,7 @@ def test_geiq_eligibility_diagnosis_validation(): with pytest.raises( ValidationError, - match="Le diagnostic d'éligibilité GEIQ ne peut avoir 2 structures pour auteur", + match="La structure de l'auteur ne correspond pas à son type", ): GEIQEligibilityDiagnosis( author_geiq=geiq, diff --git a/tests/siae_evaluations/test_models.py b/tests/siae_evaluations/test_models.py index e87b408b05..65ab42a277 100644 --- a/tests/siae_evaluations/test_models.py +++ b/tests/siae_evaluations/test_models.py @@ -39,6 +39,7 @@ InstitutionWith2MembershipFactory, ) from tests.job_applications.factories import JobApplicationFactory +from tests.prescribers.factories import PrescriberMembershipFactory from tests.siae_evaluations.factories import ( EvaluatedAdministrativeCriteriaFactory, EvaluatedJobApplicationFactory, @@ -54,7 +55,7 @@ def create_batch_of_job_applications(company): for _ in range(evaluation_enums.EvaluationJobApplicationsBoundariesNumber.MIN): approval = ApprovalFactory( start_at=start, - eligibility_diagnosis__author_kind=AuthorKind.EMPLOYER, + eligibility_diagnosis__from_employer=True, eligibility_diagnosis__author_siae=company, ) JobApplicationFactory.create( @@ -244,6 +245,10 @@ def test_eligibility_diag_not_made_by_employer(self, campaign_eligible_job_app_o evaluation_campaign = EvaluationCampaignFactory() diag = campaign_eligible_job_app_objects["diag"] diag.author_kind = AuthorKind.PRESCRIBER + membership = PrescriberMembershipFactory() + diag.author_prescriber_organization = membership.organization + diag.author_siae = None + diag.author = membership.user diag.save() assert [] == list(evaluation_campaign.eligible_job_applications()) assert _eligible_to_siae_evaluations(campaign_eligible_job_app_objects["job_app"]) == "non" @@ -347,18 +352,18 @@ def test_eligible_siaes(self): evaluation_campaign = EvaluationCampaignFactory() # company_1 got 1 job application - company_1 = CompanyFactory(department="14") + company_1 = CompanyFactory(department="14", with_membership=True) JobApplicationFactory( with_approval=True, to_company=company_1, sender_company=company_1, - eligibility_diagnosis__author_kind=AuthorKind.EMPLOYER, + eligibility_diagnosis__from_employer=True, eligibility_diagnosis__author_siae=company_1, hiring_start_at=timezone.localdate() - relativedelta(months=2), ) # company_2 got 2 job applications - company_2 = CompanyFactory(department="14") + company_2 = CompanyFactory(department="14", with_membership=True) create_batch_of_job_applications(company_2) eligible_siaes_res = evaluation_campaign.eligible_siaes() @@ -387,7 +392,7 @@ def test_eligible_siae_approval_from_past_year(self): before_evaluated_period = datetime.date(2022, 4, 4) approval1 = ApprovalFactory( start_at=before_evaluated_period, # Before evaluated period. - eligibility_diagnosis__author_kind=AuthorKind.EMPLOYER, + eligibility_diagnosis__from_employer=True, eligibility_diagnosis__author_siae=company, ) job_app_approval1_args = { @@ -406,7 +411,7 @@ def test_eligible_siae_approval_from_past_year(self): within_evaluated_period = datetime.date(2023, 2, 1) approval2 = ApprovalFactory( start_at=within_evaluated_period, - eligibility_diagnosis__author_kind=AuthorKind.EMPLOYER, + eligibility_diagnosis__from_employer=True, eligibility_diagnosis__author_siae=company, ) JobApplicationFactory.create( @@ -424,13 +429,13 @@ def test_number_of_siaes_to_select(self): assert 0 == evaluation_campaign.number_of_siaes_to_select() for _ in range(3): - company = CompanyFactory(department="14") + company = CompanyFactory(department="14", with_membership=True) create_batch_of_job_applications(company) assert 1 == evaluation_campaign.number_of_siaes_to_select() for _ in range(3): - company = CompanyFactory(department="14") + company = CompanyFactory(department="14", with_membership=True) create_batch_of_job_applications(company) assert 2 == evaluation_campaign.number_of_siaes_to_select() @@ -439,7 +444,7 @@ def test_eligible_siaes_under_ratio(self): evaluation_campaign = EvaluationCampaignFactory() for _ in range(6): - company = CompanyFactory(department="14") + company = CompanyFactory(department="14", with_membership=True) create_batch_of_job_applications(company) assert 2 == evaluation_campaign.eligible_siaes_under_ratio().count() diff --git a/tests/www/apply/test_process.py b/tests/www/apply/test_process.py index 11eaf45502..02fecfdb08 100644 --- a/tests/www/apply/test_process.py +++ b/tests/www/apply/test_process.py @@ -1840,7 +1840,7 @@ def create_job_application(self, *args, **kwargs): kwargs = { "eligibility_diagnosis__with_certifiable_criteria": True, "eligibility_diagnosis__author_siae": self.company, - "eligibility_diagnosis__author_kind": AuthorKind.EMPLOYER, + "eligibility_diagnosis__from_employer": True, } | kwargs return JobApplicationSentByJobSeekerFactory(**kwargs) diff --git a/tests/www/employees_views/test_detail.py b/tests/www/employees_views/test_detail.py index 298a06efd9..a1a3448bdd 100644 --- a/tests/www/employees_views/test_detail.py +++ b/tests/www/employees_views/test_detail.py @@ -41,7 +41,7 @@ def test_detail_view(self, client, snapshot): ) assert job_application.is_sent_by_authorized_prescriber IAEEligibilityDiagnosisFactory( - from_prescriber=True, job_seeker=approval.user, author_siae=job_application.to_company + from_employer=True, job_seeker=approval.user, author_siae=job_application.to_company ) # Another job applcation on the same SIAE, by a non authorized prescriber @@ -90,7 +90,7 @@ def test_detail_view_no_job_application(self, client): employer = company.members.first() # Make sure the job seeker infos can be edited by the siae member approval = ApprovalFactory(user__created_by=employer) - IAEEligibilityDiagnosisFactory(from_prescriber=True, job_seeker=approval.user, author_siae=company) + IAEEligibilityDiagnosisFactory(from_prescriber=True, job_seeker=approval.user) client.force_login(employer)