Repository: gip-inclusion/les-emplois
Pull request #6126: Éligibilité : Correction de la durée de validité de la certification des critères
Author: login: francoisfreitag
Labels: name: modifié
description: Modifié dans le changelog.
name: bug
Created: 2025-05-13T07:44:37Z | Merged: 2025-05-20T15:18:26Z

## :thinking: Pourquoi ?

Au départ, l’API particulier donnait une période d’éligibilité pour la certification des critères. Il s’est avéré que la réponse de l’API correspond à la question « est-ce que cette personne est bénéficiaire aujourd’hui ? », et que la date de début correspond à l’ouverture des droits actuels. La date de fin s’est révélée fausse et a été changée pour ne jamais être spécifiée par l’API (voir 9d44cd13f602550e2aae38afc6a3487897abc704).

Nous avons décidé de fixer la date de fin à 92 jours, qui correspond à la fin d’acceptabilité d’un justificatif daté (justificatif de moins de trois mois). La période de certification inclut donc maintenant le délai de trois mois (`is_considered_valid`), mais nous ne sommes pas revenus sur `with_is_considered_valid`, qui octroie également ce délai. Ainsi, le délai de 92 jours est octroyé deux fois, soit un total de 184 jours au lieu des 92 jours attendus.

## :cake: Comment ? <!-- optionnel -->

Retrait de l’annotation en base de données qui empêchait de profiter des données préchargées (`prefetch_related`).

## :desert_island: Comment tester ?

1. Créer un `SelectedAdministrativeCriteria(certified=True, …)` avec une période de certification se terminant il y a 150 jours (~5 mois).
2. Vérifier que le critère n’apparaît plus comme certifié, par exemple lors du parcours d’embauche.



Diff:
diff --git a/itou/eligibility/models/common.py b/itou/eligibility/models/common.py
index e6454515754..5d061683b57 100644
--- a/itou/eligibility/models/common.py
+++ b/itou/eligibility/models/common.py
@@ -14,7 +14,6 @@
 from itou.eligibility.tasks import async_certify_criteria, certify_criteria
 from itou.job_applications.enums import SenderKind
 from itou.utils.models import InclusiveDateRangeField
-from itou.utils.types import InclusiveDateRange
 
 
 logger = logging.getLogger(__name__)
@@ -104,9 +103,6 @@ def certify_criteria(self):
             logger.info("Could not certify criteria synchronously.", exc_info=True)
             async_certify_criteria(self._meta.model_name, self.pk)
 
-    def get_criteria_display_qs(self, hiring_start_at=None):
-        return self.selected_administrative_criteria.with_is_considered_certified(hiring_start_at=hiring_start_at)
-
 
 class AdministrativeCriteriaQuerySet(models.QuerySet):
     def level1(self):
@@ -170,22 +166,6 @@ def is_certifiable(self):
         return self.kind in CERTIFIABLE_ADMINISTRATIVE_CRITERIA_KINDS
 
 
-class SelectedAdministrativeCriteriaQuerySet(models.QuerySet):
-    def with_is_considered_certified(self, hiring_start_at=None):
-        if not hiring_start_at:
-            # could be:
-            # is_certified = models.Q(certification_period__contains=timezone.now())
-            # but not validated by UX for the moment.
-            is_certified = models.Value(False)
-        else:
-            validity_period = InclusiveDateRange(
-                hiring_start_at - datetime.timedelta(days=self.model.CERTIFICATION_GRACE_PERIOD_DAYS),
-                hiring_start_at,
-            )
-            is_certified = models.Q(certification_period__overlap=validity_period, certified=True)
-        return self.annotate(is_considered_certified=is_certified)
-
-
 class AbstractSelectedAdministrativeCriteria(models.Model):
     CERTIFICATION_GRACE_PERIOD_DAYS = 92
 
@@ -199,5 +179,3 @@ class AbstractSelectedAdministrativeCriteria(models.Model):
 
     class Meta:
         abstract = True
-
-    objects = SelectedAdministrativeCriteriaQuerySet.as_manager()
diff --git a/itou/eligibility/models/geiq.py b/itou/eligibility/models/geiq.py
index d3f5319bfe8..1608329b540 100644
--- a/itou/eligibility/models/geiq.py
+++ b/itou/eligibility/models/geiq.py
@@ -162,13 +162,6 @@ def allowance_amount(self) -> int:
     def author_structure(self):
         return self.author_geiq or self.author_prescriber_organization
 
-    def get_criteria_display_qs(self, hiring_start_at=None):
-        return (
-            super()
-            .get_criteria_display_qs(hiring_start_at)
-            .exclude(administrative_criteria__annex=AdministrativeCriteriaAnnex.NO_ANNEX)
-        )
-
     @classmethod
     def _expiration_date(cls, author=None):
         return timezone.localdate() + relativedelta(months=cls.EXPIRATION_DELAY_MONTHS)
diff --git a/itou/eligibility/utils.py b/itou/eligibility/utils.py
index fd642be4567..37730df3d6d 100644
--- a/itou/eligibility/utils.py
+++ b/itou/eligibility/utils.py
@@ -1,6 +1,10 @@
 from collections import Counter
 
-from itou.eligibility.enums import ADMINISTRATIVE_CRITERIA_LEVEL_2_REQUIRED_FOR_SIAE_KIND, AdministrativeCriteriaLevel
+from itou.eligibility.enums import (
+    ADMINISTRATIVE_CRITERIA_LEVEL_2_REQUIRED_FOR_SIAE_KIND,
+    AdministrativeCriteriaAnnex,
+    AdministrativeCriteriaLevel,
+)
 
 
 def iae_has_required_criteria(criteria, company_kind):
@@ -12,6 +16,27 @@ def iae_has_required_criteria(criteria, company_kind):
     return level_2_count >= ADMINISTRATIVE_CRITERIA_LEVEL_2_REQUIRED_FOR_SIAE_KIND[company_kind]
 
 
+def _criteria_for_display(selected_criteria, hiring_start_at):
+    for criterion in selected_criteria:
+        criterion.is_considered_certified = False
+        if hiring_start_at and criterion.certified:
+            criterion.is_considered_certified = hiring_start_at in criterion.certification_period
+    return selected_criteria
+
+
+def iae_criteria_for_display(eligibility_diagnosis, hiring_start_at=None):
+    return _criteria_for_display(eligibility_diagnosis.selected_administrative_criteria.all(), hiring_start_at)
+
+
+def geiq_criteria_for_display(eligibility_diagnosis, hiring_start_at=None):
+    return _criteria_for_display(
+        eligibility_diagnosis.selected_administrative_criteria.exclude(
+            administrative_criteria__annex=AdministrativeCriteriaAnnex.NO_ANNEX
+        ),
+        hiring_start_at,
+    )
+
+
 def geiq_allowance_amount(is_authorized_prescriber, administrative_criteria) -> int:
     """Amount of granted allowance for job seeker.
 
diff --git a/itou/www/apply/views/process_views.py b/itou/www/apply/views/process_views.py
index cfe5b1db63f..1431187e887 100644
--- a/itou/www/apply/views/process_views.py
+++ b/itou/www/apply/views/process_views.py
@@ -23,6 +23,7 @@
 from itou.companies.models import Company
 from itou.eligibility.models import EligibilityDiagnosis
 from itou.eligibility.models.geiq import GEIQEligibilityDiagnosis
+from itou.eligibility.utils import geiq_criteria_for_display, iae_criteria_for_display
 from itou.job_applications import enums as job_applications_enums
 from itou.job_applications.models import JobApplication, JobApplicationWorkflow, PriorAction
 from itou.rdv_insertion.api import get_api_credentials, get_invitation_status
@@ -135,13 +136,15 @@ def details_for_jobseeker(request, job_application_id, template_name="apply/proc
         and _get_geiq_eligibility_diagnosis(job_application, only_prescriber=False)
     )
     if geiq_eligibility_diagnosis:
-        geiq_eligibility_diagnosis.criteria_display = geiq_eligibility_diagnosis.get_criteria_display_qs(
-            hiring_start_at=job_application.hiring_start_at
+        geiq_eligibility_diagnosis.criteria_display = geiq_criteria_for_display(
+            geiq_eligibility_diagnosis,
+            hiring_start_at=job_application.hiring_start_at,
         )
     eligibility_diagnosis = job_application.get_eligibility_diagnosis()
     if eligibility_diagnosis:
-        eligibility_diagnosis.criteria_display = eligibility_diagnosis.get_criteria_display_qs(
-            hiring_start_at=job_application.hiring_start_at
+        eligibility_diagnosis.criteria_display = iae_criteria_for_display(
+            eligibility_diagnosis,
+            hiring_start_at=job_application.hiring_start_at,
         )
 
     context = {
@@ -230,14 +233,16 @@ def details_for_company(request, job_application_id, template_name="apply/proces
         and _get_geiq_eligibility_diagnosis(job_application, only_prescriber=False)
     )
     if geiq_eligibility_diagnosis:
-        geiq_eligibility_diagnosis.criteria_display = geiq_eligibility_diagnosis.get_criteria_display_qs(
-            hiring_start_at=job_application.hiring_start_at
+        geiq_eligibility_diagnosis.criteria_display = geiq_criteria_for_display(
+            geiq_eligibility_diagnosis,
+            hiring_start_at=job_application.hiring_start_at,
         )
 
     eligibility_diagnosis = job_application.get_eligibility_diagnosis()
     if eligibility_diagnosis:
-        eligibility_diagnosis.criteria_display = eligibility_diagnosis.get_criteria_display_qs(
-            hiring_start_at=job_application.hiring_start_at
+        eligibility_diagnosis.criteria_display = iae_criteria_for_display(
+            eligibility_diagnosis,
+            hiring_start_at=job_application.hiring_start_at,
         )
 
     can_be_cancelled = job_application.state.is_accepted and job_application.can_be_cancelled
@@ -318,14 +323,16 @@ def details_for_prescriber(request, job_application_id, template_name="apply/pro
         and _get_geiq_eligibility_diagnosis(job_application, only_prescriber=True)
     )
     if geiq_eligibility_diagnosis:
-        geiq_eligibility_diagnosis.criteria_display = geiq_eligibility_diagnosis.get_criteria_display_qs(
-            hiring_start_at=job_application.hiring_start_at
+        geiq_eligibility_diagnosis.criteria_display = geiq_criteria_for_display(
+            geiq_eligibility_diagnosis,
+            hiring_start_at=job_application.hiring_start_at,
         )
 
     eligibility_diagnosis = job_application.get_eligibility_diagnosis()
     if eligibility_diagnosis:
-        eligibility_diagnosis.criteria_display = eligibility_diagnosis.get_criteria_display_qs(
-            hiring_start_at=job_application.hiring_start_at
+        eligibility_diagnosis.criteria_display = iae_criteria_for_display(
+            eligibility_diagnosis,
+            hiring_start_at=job_application.hiring_start_at,
         )
 
     # Refused applications information is providen to prescribers
diff --git a/itou/www/apply/views/submit_views.py b/itou/www/apply/views/submit_views.py
index 418f9b6a42a..2ef6fd6793d 100644
--- a/itou/www/apply/views/submit_views.py
+++ b/itou/www/apply/views/submit_views.py
@@ -17,6 +17,7 @@
 from itou.companies.models import Company, JobDescription
 from itou.eligibility.models import EligibilityDiagnosis
 from itou.eligibility.models.geiq import GEIQEligibilityDiagnosis
+from itou.eligibility.utils import geiq_criteria_for_display, iae_criteria_for_display
 from itou.files.models import File
 from itou.gps.models import FollowUpGroup
 from itou.job_applications.models import JobApplication
@@ -839,13 +840,13 @@ def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
 
         if self.geiq_eligibility_diagnosis:
-            self.geiq_eligibility_diagnosis.criteria_display = (
-                self.geiq_eligibility_diagnosis.get_criteria_display_qs()
+            self.geiq_eligibility_diagnosis.criteria_display = geiq_criteria_for_display(
+                self.geiq_eligibility_diagnosis
             )
         if self.eligibility_diagnosis:
             # The job_seeker object already contains a lot of information: no need to re-retrieve it
             self.eligibility_diagnosis.job_seeker = self.job_seeker
-            self.eligibility_diagnosis.criteria_display = self.eligibility_diagnosis.get_criteria_display_qs()
+            self.eligibility_diagnosis.criteria_display = iae_criteria_for_display(self.eligibility_diagnosis)
 
         context["can_edit_personal_information"] = can_edit_personal_information(self.request, self.job_seeker)
         context["is_subject_to_eligibility_rules"] = self.company.is_subject_to_eligibility_rules
diff --git a/itou/www/employees_views/views.py b/itou/www/employees_views/views.py
index f0214a5c054..424b85c5e2d 100644
--- a/itou/www/employees_views/views.py
+++ b/itou/www/employees_views/views.py
@@ -10,6 +10,7 @@
     Approval,
     ProlongationRequest,
 )
+from itou.eligibility.utils import iae_criteria_for_display
 from itou.job_applications.enums import JobApplicationState
 from itou.job_applications.models import JobApplication
 from itou.users.enums import UserKind
@@ -106,8 +107,9 @@ def get_context_data(self, **kwargs):
         eligibility_diagnosis = job_application and job_application.get_eligibility_diagnosis()
         if eligibility_diagnosis:
             hiring_start_at = job_application.hiring_start_at if job_application else None
-            eligibility_diagnosis.criteria_display = eligibility_diagnosis.get_criteria_display_qs(
-                hiring_start_at=hiring_start_at
+            eligibility_diagnosis.criteria_display = iae_criteria_for_display(
+                eligibility_diagnosis,
+                hiring_start_at=hiring_start_at,
             )
 
         context["can_view_personal_information"] = True  # SIAE members have access to personal info
diff --git a/itou/www/job_seekers_views/views.py b/itou/www/job_seekers_views/views.py
index a4472ed4982..37310e2c434 100644
--- a/itou/www/job_seekers_views/views.py
+++ b/itou/www/job_seekers_views/views.py
@@ -21,6 +21,7 @@
 from itou.companies.models import Company
 from itou.eligibility.models.geiq import GEIQEligibilityDiagnosis
 from itou.eligibility.models.iae import EligibilityDiagnosis
+from itou.eligibility.utils import geiq_criteria_for_display, iae_criteria_for_display
 from itou.gps.models import FollowUpGroup
 from itou.job_applications.models import JobApplication
 from itou.users.enums import UserKind
@@ -89,10 +90,10 @@ def get_context_data(self, **kwargs):
             can_edit_iae_eligibility = True
 
         if geiq_eligibility_diagnosis:
-            geiq_eligibility_diagnosis.criteria_display = geiq_eligibility_diagnosis.get_criteria_display_qs()
+            geiq_eligibility_diagnosis.criteria_display = geiq_criteria_for_display(geiq_eligibility_diagnosis)
 
         if iae_eligibility_diagnosis:
-            iae_eligibility_diagnosis.criteria_display = iae_eligibility_diagnosis.get_criteria_display_qs()
+            iae_eligibility_diagnosis.criteria_display = iae_criteria_for_display(iae_eligibility_diagnosis)
 
         return super().get_context_data(**kwargs) | {
             "geiq_eligibility_diagnosis": geiq_eligibility_diagnosis,
diff --git a/tests/eligibility/test_iae.py b/tests/eligibility/test_iae.py
index 721bf7f3d0f..225ac066f89 100644
--- a/tests/eligibility/test_iae.py
+++ b/tests/eligibility/test_iae.py
@@ -15,9 +15,6 @@
     AuthorKind,
 )
 from itou.eligibility.models import AdministrativeCriteria, EligibilityDiagnosis
-from itou.eligibility.models.common import (
-    AbstractSelectedAdministrativeCriteria,
-)
 from itou.eligibility.models.geiq import GEIQAdministrativeCriteria
 from itou.gps.models import FollowUpGroup, FollowUpGroupMembership
 from itou.users.enums import IdentityCertificationAuthorities
@@ -704,69 +701,6 @@ def test_selected_administrative_criteria_certified(
     )
 
 
-def test_with_is_considered_certified():
-    def assert_considered_valid_count(hiring_start_at, expected):
-        assert (
-            diagnosis.selected_administrative_criteria.with_is_considered_certified(hiring_start_at)
-            .filter(is_considered_certified=True)
-            .count()
-            == expected
-        )
-
-    diagnosis = IAEEligibilityDiagnosisFactory(
-        certifiable=True,
-        criteria_kinds=CERTIFIABLE_ADMINISTRATIVE_CRITERIA_KINDS,
-    )
-    for selected_criterion in diagnosis.selected_administrative_criteria.all():
-        selected_criterion.certified = True
-        certification_period_start = timezone.now() - datetime.timedelta(days=10)
-        certification_period_end = timezone.now() + datetime.timedelta(days=20)
-        selected_criterion.certification_period = InclusiveDateRange(
-            certification_period_start, certification_period_end
-        )
-        selected_criterion.save()
-    assert (
-        diagnosis.selected_administrative_criteria.with_is_considered_certified().count()
-        == diagnosis.selected_administrative_criteria.count()
-    )
-
-    criterion_to_keep = diagnosis.selected_administrative_criteria.all()[:1]
-    diagnosis.selected_administrative_criteria.exclude(pk__in=criterion_to_keep).delete()
-    assert diagnosis.selected_administrative_criteria.count() == 1
-
-    # no hiring_start_at: none is certified.
-    assert_considered_valid_count(hiring_start_at=None, expected=0)
-
-    # hiring_start_at within certification period.
-    assert_considered_valid_count(hiring_start_at=timezone.now(), expected=1)
-
-    # hiring_start_at before certification period beginning.
-    hiring_start_at = certification_period_start - datetime.timedelta(days=1)
-    assert_considered_valid_count(hiring_start_at=hiring_start_at, expected=0)
-
-    # hiring_start_at on certification period beginning.
-    assert_considered_valid_count(hiring_start_at=certification_period_start, expected=1)
-
-    # hiring_start_at on certification period ending.
-    assert_considered_valid_count(hiring_start_at=certification_period_end, expected=1)
-
-    # hiring start_at after certification period ending.
-    hiring_start_at = certification_period_end + datetime.timedelta(days=1)
-    assert_considered_valid_count(hiring_start_at=hiring_start_at, expected=1)
-
-    # hiring start_at after certification period ending + grace period almost finished.
-    hiring_start_at = certification_period_end + datetime.timedelta(
-        days=AbstractSelectedAdministrativeCriteria.CERTIFICATION_GRACE_PERIOD_DAYS
-    )
-    assert_considered_valid_count(hiring_start_at=hiring_start_at, expected=1)
-
-    # hiring start_at after certification period ending + grace period.
-    hiring_start_at = certification_period_end + datetime.timedelta(
-        days=AbstractSelectedAdministrativeCriteria.CERTIFICATION_GRACE_PERIOD_DAYS + 1
-    )
-    assert_considered_valid_count(hiring_start_at=hiring_start_at, expected=0)
-
-
 def test_is_from_employer():
     diagnosis = GEIQEligibilityDiagnosisFactory(from_employer=True)
     assert diagnosis.is_from_employer
diff --git a/tests/eligibility/test_utils.py b/tests/eligibility/test_utils.py
new file mode 100644
index 00000000000..bbe2c78183c
--- /dev/null
+++ b/tests/eligibility/test_utils.py
@@ -0,0 +1,54 @@
+import datetime
+
+import pytest
+from django.utils import timezone
+
+from itou.eligibility.enums import CERTIFIABLE_ADMINISTRATIVE_CRITERIA_KINDS, AdministrativeCriteriaAnnex
+from itou.eligibility.models import GEIQAdministrativeCriteria, GEIQSelectedAdministrativeCriteria
+from itou.eligibility.utils import geiq_criteria_for_display, iae_criteria_for_display
+from itou.utils.types import InclusiveDateRange
+from tests.eligibility.factories import GEIQEligibilityDiagnosisFactory, IAEEligibilityDiagnosisFactory
+
+
+@pytest.mark.parametrize(
+    "factory,method",
+    [
+        pytest.param(GEIQEligibilityDiagnosisFactory, geiq_criteria_for_display, id="geiq"),
+        pytest.param(IAEEligibilityDiagnosisFactory, iae_criteria_for_display, id="iae"),
+    ],
+)
+def test_criteria_for_display(factory, method):
+    def _assert_considered_certified(diagnosis, expected, hiring_start_at):
+        [criterion] = method(diagnosis, hiring_start_at=hiring_start_at)
+        assert criterion.is_considered_certified is expected
+
+    def assert_considered_certified(diagnosis, hiring_start_at=None):
+        _assert_considered_certified(diagnosis, True, hiring_start_at=hiring_start_at)
+
+    def assert_not_considered_certified(diagnosis, hiring_start_at=None):
+        _assert_considered_certified(diagnosis, False, hiring_start_at=hiring_start_at)
+
+    diagnosis = factory(
+        certifiable=True,
+        criteria_kinds=list(CERTIFIABLE_ADMINISTRATIVE_CRITERIA_KINDS)[:1],
+    )
+    if factory is GEIQEligibilityDiagnosisFactory:
+        # Ignored because they are not displayed.
+        ignored_crit = (
+            GEIQAdministrativeCriteria.objects.filter(annex=AdministrativeCriteriaAnnex.NO_ANNEX).order_by("?").first()
+        )
+        GEIQSelectedAdministrativeCriteria(administrative_criteria=ignored_crit, eligibility_diagnosis=diagnosis)
+
+    [selected_criterion] = diagnosis.selected_administrative_criteria.all()
+    selected_criterion.certified = True
+    today = timezone.localdate()
+    start = today - datetime.timedelta(days=10)
+    end = today + datetime.timedelta(days=10)
+    selected_criterion.certification_period = InclusiveDateRange(start, end)
+    selected_criterion.save(update_fields=["certification_period", "certified"])
+    assert_not_considered_certified(diagnosis)
+    assert_not_considered_certified(diagnosis, hiring_start_at=start - datetime.timedelta(days=1))
+    assert_considered_certified(diagnosis, hiring_start_at=start)
+    assert_considered_certified(diagnosis, hiring_start_at=today)
+    assert_considered_certified(diagnosis, hiring_start_at=end)
+    assert_not_considered_certified(diagnosis, hiring_start_at=end + datetime.timedelta(days=1))
diff --git a/tests/www/apply/__snapshots__/test_process.ambr b/tests/www/apply/__snapshots__/test_process.ambr
index 4e37728cd14..8bdaa6c2310 100644
--- a/tests/www/apply/__snapshots__/test_process.ambr
+++ b/tests/www/apply/__snapshots__/test_process.ambr
@@ -1018,7 +1018,7 @@
 # ---
 # name: TestProcessViews.test_details_for_company_from_approval[job application detail for company]
   dict({
-    'num_queries': 22,
+    'num_queries': 21,
     'queries': list([
       dict({
         'origin': list([
@@ -1918,37 +1918,6 @@
           LIMIT 1
         ''',
       }),
-      dict({
-        'origin': list([
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IncludeNode[apply/process_details_company.html]',
-          'IfNode[apply/process_details_company.html]',
-          'BlockNode[layout/base.html]',
-          'ExtendsNode[apply/process_base.html]',
-          'ExtendsNode[apply/process_details_company.html]',
-          'details_for_company[www/apply/views/process_views.py]',
-          '_check_user_view_wrapper[utils/auth.py]',
-        ]),
-        'sql': '''
-          SELECT "eligibility_selectedadministrativecriteria"."id",
-                 "eligibility_selectedadministrativecriteria"."certified",
-                 "eligibility_selectedadministrativecriteria"."certified_at",
-                 "eligibility_selectedadministrativecriteria"."certification_period",
-                 "eligibility_selectedadministrativecriteria"."data_returned_by_api",
-                 "eligibility_selectedadministrativecriteria"."created_at",
-                 "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id",
-                 "eligibility_selectedadministrativecriteria"."administrative_criteria_id",
-                 ("eligibility_selectedadministrativecriteria"."certification_period" && %s
-                  AND "eligibility_selectedadministrativecriteria"."certified") AS "is_considered_certified"
-          FROM "eligibility_selectedadministrativecriteria"
-          INNER JOIN "eligibility_administrativecriteria" ON ("eligibility_selectedadministrativecriteria"."administrative_criteria_id" = "eligibility_administrativecriteria"."id")
-          WHERE "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id" = %s
-          ORDER BY "eligibility_administrativecriteria"."level" ASC,
-                   "eligibility_administrativecriteria"."ui_rank" ASC
-        ''',
-      }),
       dict({
         'origin': list([
           'User.latest_approval[users/models.py]',
@@ -2208,7 +2177,7 @@
 # ---
 # name: TestProcessViews.test_details_for_company_from_list[job application detail for company]
   dict({
-    'num_queries': 22,
+    'num_queries': 21,
     'queries': list([
       dict({
         'origin': list([
@@ -3108,37 +3077,6 @@
           LIMIT 1
         ''',
       }),
-      dict({
-        'origin': list([
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IncludeNode[apply/process_details_company.html]',
-          'IfNode[apply/process_details_company.html]',
-          'BlockNode[layout/base.html]',
-          'ExtendsNode[apply/process_base.html]',
-          'ExtendsNode[apply/process_details_company.html]',
-          'details_for_company[www/apply/views/process_views.py]',
-          '_check_user_view_wrapper[utils/auth.py]',
-        ]),
-        'sql': '''
-          SELECT "eligibility_selectedadministrativecriteria"."id",
-                 "eligibility_selectedadministrativecriteria"."certified",
-                 "eligibility_selectedadministrativecriteria"."certified_at",
-                 "eligibility_selectedadministrativecriteria"."certification_period",
-                 "eligibility_selectedadministrativecriteria"."data_returned_by_api",
-                 "eligibility_selectedadministrativecriteria"."created_at",
-                 "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id",
-                 "eligibility_selectedadministrativecriteria"."administrative_criteria_id",
-                 ("eligibility_selectedadministrativecriteria"."certification_period" && %s
-                  AND "eligibility_selectedadministrativecriteria"."certified") AS "is_considered_certified"
-          FROM "eligibility_selectedadministrativecriteria"
-          INNER JOIN "eligibility_administrativecriteria" ON ("eligibility_selectedadministrativecriteria"."administrative_criteria_id" = "eligibility_administrativecriteria"."id")
-          WHERE "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id" = %s
-          ORDER BY "eligibility_administrativecriteria"."level" ASC,
-                   "eligibility_administrativecriteria"."ui_rank" ASC
-        ''',
-      }),
       dict({
         'origin': list([
           'User.latest_approval[users/models.py]',
@@ -3454,7 +3392,7 @@
 # ---
 # name: TestProcessViews.test_details_for_job_seeker
   dict({
-    'num_queries': 16,
+    'num_queries': 15,
     'queries': list([
       dict({
         'origin': list([
@@ -4199,36 +4137,6 @@
           ORDER BY "rdv_insertion_appointment"."start_at" DESC
         ''',
       }),
-      dict({
-        'origin': list([
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IncludeNode[apply/process_details.html]',
-          'IfNode[apply/process_details.html]',
-          'BlockNode[layout/base.html]',
-          'ExtendsNode[apply/process_base.html]',
-          'ExtendsNode[apply/process_details.html]',
-          'details_for_jobseeker[www/apply/views/process_views.py]',
-        ]),
-        'sql': '''
-          SELECT "eligibility_selectedadministrativecriteria"."id",
-                 "eligibility_selectedadministrativecriteria"."certified",
-                 "eligibility_selectedadministrativecriteria"."certified_at",
-                 "eligibility_selectedadministrativecriteria"."certification_period",
-                 "eligibility_selectedadministrativecriteria"."data_returned_by_api",
-                 "eligibility_selectedadministrativecriteria"."created_at",
-                 "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id",
-                 "eligibility_selectedadministrativecriteria"."administrative_criteria_id",
-                 ("eligibility_selectedadministrativecriteria"."certification_period" && %s
-                  AND "eligibility_selectedadministrativecriteria"."certified") AS "is_considered_certified"
-          FROM "eligibility_selectedadministrativecriteria"
-          INNER JOIN "eligibility_administrativecriteria" ON ("eligibility_selectedadministrativecriteria"."administrative_criteria_id" = "eligibility_administrativecriteria"."id")
-          WHERE "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id" = %s
-          ORDER BY "eligibility_administrativecriteria"."level" ASC,
-                   "eligibility_administrativecriteria"."ui_rank" ASC
-        ''',
-      }),
       dict({
         'origin': list([
           'User.latest_approval[users/models.py]',
diff --git a/tests/www/apply/__snapshots__/test_submit.ambr b/tests/www/apply/__snapshots__/test_submit.ambr
index 8b694a763c9..e1f0b4f9a9f 100644
--- a/tests/www/apply/__snapshots__/test_submit.ambr
+++ b/tests/www/apply/__snapshots__/test_submit.ambr
@@ -1341,6 +1341,29 @@
                    "companies_jobdescription"."is_active" ASC
         ''',
       }),
+      dict({
+        'origin': list([
+          '_criteria_for_display[eligibility/utils.py]',
+          'iae_criteria_for_display[eligibility/utils.py]',
+          'HireConfirmationView.get_context_data[www/apply/views/submit_views.py]',
+          'HireConfirmationView.dispatch[www/apply/views/submit_views.py]',
+        ]),
+        'sql': '''
+          SELECT "eligibility_selectedadministrativecriteria"."id",
+                 "eligibility_selectedadministrativecriteria"."certified",
+                 "eligibility_selectedadministrativecriteria"."certified_at",
+                 "eligibility_selectedadministrativecriteria"."certification_period",
+                 "eligibility_selectedadministrativecriteria"."data_returned_by_api",
+                 "eligibility_selectedadministrativecriteria"."created_at",
+                 "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id",
+                 "eligibility_selectedadministrativecriteria"."administrative_criteria_id"
+          FROM "eligibility_selectedadministrativecriteria"
+          INNER JOIN "eligibility_administrativecriteria" ON ("eligibility_selectedadministrativecriteria"."administrative_criteria_id" = "eligibility_administrativecriteria"."id")
+          WHERE "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id" = %s
+          ORDER BY "eligibility_administrativecriteria"."level" ASC,
+                   "eligibility_administrativecriteria"."ui_rank" ASC
+        ''',
+      }),
       dict({
         'origin': list([
           'Atomic.__exit__[<site-packages>/django/db/transaction.py]',
@@ -1375,34 +1398,6 @@
           LIMIT 1
         ''',
       }),
-      dict({
-        'origin': list([
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IncludeNode[apply/submit/hire_confirmation.html]',
-          'BlockNode[apply/submit_base.html]',
-          'BlockNode[layout/base.html]',
-          'ExtendsNode[apply/submit_base.html]',
-          'ExtendsNode[apply/submit/hire_confirmation.html]',
-        ]),
-        'sql': '''
-          SELECT "eligibility_selectedadministrativecriteria"."id",
-                 "eligibility_selectedadministrativecriteria"."certified",
-                 "eligibility_selectedadministrativecriteria"."certified_at",
-                 "eligibility_selectedadministrativecriteria"."certification_period",
-                 "eligibility_selectedadministrativecriteria"."data_returned_by_api",
-                 "eligibility_selectedadministrativecriteria"."created_at",
-                 "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id",
-                 "eligibility_selectedadministrativecriteria"."administrative_criteria_id",
-                 %s AS "is_considered_certified"
-          FROM "eligibility_selectedadministrativecriteria"
-          INNER JOIN "eligibility_administrativecriteria" ON ("eligibility_selectedadministrativecriteria"."administrative_criteria_id" = "eligibility_administrativecriteria"."id")
-          WHERE "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id" = %s
-          ORDER BY "eligibility_administrativecriteria"."level" ASC,
-                   "eligibility_administrativecriteria"."ui_rank" ASC
-        ''',
-      }),
       dict({
         'origin': list([
           'RemoteAutocompleteSelect2Widget.optgroups[utils/widgets.py]',
diff --git a/tests/www/apply/test_process.py b/tests/www/apply/test_process.py
index 8d8575bf873..adf1dd4f6a6 100644
--- a/tests/www/apply/test_process.py
+++ b/tests/www/apply/test_process.py
@@ -334,8 +334,13 @@ def test_details_for_company_certified_criteria_after_expiration(self, client):
         response = client.get(url)
         assertNotContains(response, CERTIFIED_BADGE_HTML, html=True)
 
-        eligibility_diagnosis.expires_at = today + datetime.timedelta(days=1)
+        tomorrow = today + datetime.timedelta(days=1)
+        eligibility_diagnosis.expires_at = tomorrow
         eligibility_diagnosis.save()
+        selected_criteria.certification_period = InclusiveDateRange(
+            selected_criteria.certification_period.lower, tomorrow
+        )
+        selected_criteria.save()
         response = client.get(url)
         assertContains(response, CERTIFIED_BADGE_HTML, html=True)
 
diff --git a/tests/www/apply/test_templates.py b/tests/www/apply/test_templates.py
index a8388f312cd..ab3db411f71 100644
--- a/tests/www/apply/test_templates.py
+++ b/tests/www/apply/test_templates.py
@@ -14,6 +14,7 @@
     AdministrativeCriteriaLevel,
 )
 from itou.eligibility.tasks import certify_criteria
+from itou.eligibility.utils import geiq_criteria_for_display, iae_criteria_for_display
 from itou.job_applications.enums import Origin
 from itou.jobs.models import Appellation
 from itou.utils.context_processors import expose_enums
@@ -213,7 +214,9 @@ def default_params(self, diagnosis):
             job_application.to_company = diagnosis.author_siae
             job_application.save()
         # This is the way it's set in views.
-        diagnosis.criteria_display = diagnosis.get_criteria_display_qs(hiring_start_at=job_application.hiring_start_at)
+        diagnosis.criteria_display = iae_criteria_for_display(
+            diagnosis, hiring_start_at=job_application.hiring_start_at
+        )
         return {
             "eligibility_diagnosis": diagnosis,
             "request": RequestFactory(),
@@ -368,7 +371,9 @@ def template(self):
         return load_template("apply/includes/geiq/geiq_diagnosis_details.html")
 
     def default_params_geiq(self, diagnosis, job_application):
-        diagnosis.criteria_display = diagnosis.get_criteria_display_qs(hiring_start_at=job_application.hiring_start_at)
+        diagnosis.criteria_display = geiq_criteria_for_display(
+            diagnosis, hiring_start_at=job_application.hiring_start_at
+        )
         return {
             "request": RequestFactory(),
             "diagnosis": diagnosis,
diff --git a/tests/www/employees_views/__snapshots__/test_detail.ambr b/tests/www/employees_views/__snapshots__/test_detail.ambr
index 9f0e87c26c0..f05dc3494b3 100644
--- a/tests/www/employees_views/__snapshots__/test_detail.ambr
+++ b/tests/www/employees_views/__snapshots__/test_detail.ambr
@@ -1,7 +1,7 @@
 # serializer version: 1
 # name: TestEmployeeDetailView.test_detail_view[employee detail view]
   dict({
-    'num_queries': 25,
+    'num_queries': 24,
     'queries': list([
       dict({
         'origin': list([
@@ -967,34 +967,6 @@
           LIMIT 21
         ''',
       }),
-      dict({
-        'origin': list([
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IfNode[apply/includes/eligibility_diagnosis.html]',
-          'IncludeNode[employees/detail.html]',
-          'IfNode[employees/detail.html]',
-          'BlockNode[layout/base.html]',
-          'ExtendsNode[employees/detail.html]',
-        ]),
-        'sql': '''
-          SELECT "eligibility_selectedadministrativecriteria"."id",
-                 "eligibility_selectedadministrativecriteria"."certified",
-                 "eligibility_selectedadministrativecriteria"."certified_at",
-                 "eligibility_selectedadministrativecriteria"."certification_period",
-                 "eligibility_selectedadministrativecriteria"."data_returned_by_api",
-                 "eligibility_selectedadministrativecriteria"."created_at",
-                 "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id",
-                 "eligibility_selectedadministrativecriteria"."administrative_criteria_id",
-                 ("eligibility_selectedadministrativecriteria"."certification_period" && %s
-                  AND "eligibility_selectedadministrativecriteria"."certified") AS "is_considered_certified"
-          FROM "eligibility_selectedadministrativecriteria"
-          INNER JOIN "eligibility_administrativecriteria" ON ("eligibility_selectedadministrativecriteria"."administrative_criteria_id" = "eligibility_administrativecriteria"."id")
-          WHERE "eligibility_selectedadministrativecriteria"."eligibility_diagnosis_id" = %s
-          ORDER BY "eligibility_administrativecriteria"."level" ASC,
-                   "eligibility_administrativecriteria"."ui_rank" ASC
-        ''',
-      }),
       dict({
         'origin': list([
           'User.latest_approval[users/models.py]',


Reviews:
user: login: celine-m-s
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:44:47Z
user: login: celine-m-s
body: C'est tout bon pour moi mais j'aurais bien attendu le retour de Zohra pour avoir son avis.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:54:38Z
user: login: francoisfreitag
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T14:57:38Z
user: login: rsebille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T10:21:26Z
user: login: francoisfreitag
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T08:01:25Z

Review comments:
diff_hunk: @@ -0,0 +1,58 @@
+import datetime
+
+import pytest
+from django.utils import timezone
+
+from itou.eligibility.enums import CERTIFIABLE_ADMINISTRATIVE_CRITERIA_KINDS, AdministrativeCriteriaAnnex
+from itou.eligibility.models import GEIQAdministrativeCriteria, GEIQSelectedAdministrativeCriteria
+from itou.eligibility.utils import geiq_criteria_for_display, iae_criteria_for_display
+from itou.utils.types import InclusiveDateRange
+from tests.eligibility.factories import GEIQEligibilityDiagnosisFactory, IAEEligibilityDiagnosisFactory
+
+
+@pytest.mark.parametrize(
+    "factory,method",
+    [
+        pytest.param(GEIQEligibilityDiagnosisFactory, geiq_criteria_for_display, id="geiq"),
+        pytest.param(IAEEligibilityDiagnosisFactory, iae_criteria_for_display, id="iae"),
+    ],
+)
+def test_criteria_for_display(factory, method):
+    def _assert_considered_certified(diagnosis, expected, hiring_start_at):
+        [criterion] = method(diagnosis, hiring_start_at=hiring_start_at)
+        assert criterion.is_considered_certified is expected
+
+    def assert_considered_certified(diagnosis, hiring_start_at=None):
+        _assert_considered_certified(diagnosis, True, hiring_start_at=hiring_start_at)
+
+    def assert_not_considered_certified(diagnosis, hiring_start_at=None):
+        _assert_considered_certified(diagnosis, False, hiring_start_at=hiring_start_at)
+
+    diagnosis = factory(
+        certifiable=True,
+        criteria_kinds=list(CERTIFIABLE_ADMINISTRATIVE_CRITERIA_KINDS)[:1],
+    )
+    if factory is GEIQEligibilityDiagnosisFactory:
+        # Ignored because they are not displayed.
+        ignored_crit = (
+            GEIQAdministrativeCriteria.objects.filter(annex=AdministrativeCriteriaAnnex.NO_ANNEX).order_by("?").first()
+        )
+        GEIQSelectedAdministrativeCriteria(administrative_criteria=ignored_crit, eligibility_diagnosis=diagnosis)
+
+    [selected_criterion] = diagnosis.selected_administrative_criteria.all()
+    selected_criterion.certified = True
+    today = timezone.localdate()
+    start = today - datetime.timedelta(days=10)
+    end = today + datetime.timedelta(days=10)
+    selected_criterion.certification_period = InclusiveDateRange(start, end)
+    selected_criterion.save(update_fields=["certification_period", "certified"])
+    assert_not_considered_certified(diagnosis)
+    assert_not_considered_certified(diagnosis, hiring_start_at=start - datetime.timedelta(days=1))
+    assert_considered_certified(diagnosis, hiring_start_at=start)
+    assert_considered_certified(diagnosis, hiring_start_at=today)
+    assert_considered_certified(diagnosis, hiring_start_at=end)
+    assert_considered_certified(diagnosis, hiring_start_at=end + datetime.timedelta(days=1))
+    end_limit = end + datetime.timedelta(days=selected_criterion.CERTIFICATION_GRACE_PERIOD_DAYS)
+    assert_considered_certified(diagnosis, hiring_start_at=end_limit)
+    assert_considered_certified(diagnosis, hiring_start_at=end_limit)
path: tests/eligibility/test_utils.py
user: login: celine-m-s
body: C'est deux fois le même, non ? :thinking: 
created_at: 2025-05-13T14:44:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,58 @@
+import datetime
+
+import pytest
+from django.utils import timezone
+
+from itou.eligibility.enums import CERTIFIABLE_ADMINISTRATIVE_CRITERIA_KINDS, AdministrativeCriteriaAnnex
+from itou.eligibility.models import GEIQAdministrativeCriteria, GEIQSelectedAdministrativeCriteria
+from itou.eligibility.utils import geiq_criteria_for_display, iae_criteria_for_display
+from itou.utils.types import InclusiveDateRange
+from tests.eligibility.factories import GEIQEligibilityDiagnosisFactory, IAEEligibilityDiagnosisFactory
+
+
+@pytest.mark.parametrize(
+    "factory,method",
+    [
+        pytest.param(GEIQEligibilityDiagnosisFactory, geiq_criteria_for_display, id="geiq"),
+        pytest.param(IAEEligibilityDiagnosisFactory, iae_criteria_for_display, id="iae"),
+    ],
+)
+def test_criteria_for_display(factory, method):
+    def _assert_considered_certified(diagnosis, expected, hiring_start_at):
+        [criterion] = method(diagnosis, hiring_start_at=hiring_start_at)
+        assert criterion.is_considered_certified is expected
+
+    def assert_considered_certified(diagnosis, hiring_start_at=None):
+        _assert_considered_certified(diagnosis, True, hiring_start_at=hiring_start_at)
+
+    def assert_not_considered_certified(diagnosis, hiring_start_at=None):
+        _assert_considered_certified(diagnosis, False, hiring_start_at=hiring_start_at)
+
+    diagnosis = factory(
+        certifiable=True,
+        criteria_kinds=list(CERTIFIABLE_ADMINISTRATIVE_CRITERIA_KINDS)[:1],
+    )
+    if factory is GEIQEligibilityDiagnosisFactory:
+        # Ignored because they are not displayed.
+        ignored_crit = (
+            GEIQAdministrativeCriteria.objects.filter(annex=AdministrativeCriteriaAnnex.NO_ANNEX).order_by("?").first()
+        )
+        GEIQSelectedAdministrativeCriteria(administrative_criteria=ignored_crit, eligibility_diagnosis=diagnosis)
+
+    [selected_criterion] = diagnosis.selected_administrative_criteria.all()
+    selected_criterion.certified = True
+    today = timezone.localdate()
+    start = today - datetime.timedelta(days=10)
+    end = today + datetime.timedelta(days=10)
+    selected_criterion.certification_period = InclusiveDateRange(start, end)
+    selected_criterion.save(update_fields=["certification_period", "certified"])
+    assert_not_considered_certified(diagnosis)
+    assert_not_considered_certified(diagnosis, hiring_start_at=start - datetime.timedelta(days=1))
+    assert_considered_certified(diagnosis, hiring_start_at=start)
+    assert_considered_certified(diagnosis, hiring_start_at=today)
+    assert_considered_certified(diagnosis, hiring_start_at=end)
+    assert_considered_certified(diagnosis, hiring_start_at=end + datetime.timedelta(days=1))
+    end_limit = end + datetime.timedelta(days=selected_criterion.CERTIFICATION_GRACE_PERIOD_DAYS)
+    assert_considered_certified(diagnosis, hiring_start_at=end_limit)
+    assert_considered_certified(diagnosis, hiring_start_at=end_limit)
path: tests/eligibility/test_utils.py
user: login: francoisfreitag
body: Ça sent le raté dans mes rebases :see_no_evil:, merci !
created_at: 2025-05-13T14:57:38Z
author_association: MEMBER
diff_hunk: @@ -12,6 +18,35 @@ def iae_has_required_criteria(criteria, company_kind):
     return level_2_count >= ADMINISTRATIVE_CRITERIA_LEVEL_2_REQUIRED_FOR_SIAE_KIND[company_kind]
 
 
+def _inclusive_overlap(date_range1: InclusiveDateRange, date_range2: InclusiveDateRange):
+    return date_range1.lower <= date_range2.upper and date_range2.lower <= date_range1.upper
+
+
+def _criteria_for_display(selected_criteria, hiring_start_at):
+    for criterion in selected_criteria:
+        criterion.is_considered_certified = False
+        if hiring_start_at and criterion.certified:
+            validity_period = InclusiveDateRange(
+                hiring_start_at - datetime.timedelta(days=criterion.CERTIFICATION_GRACE_PERIOD_DAYS),
+                hiring_start_at,
+            )
+            criterion.is_considered_certified = _inclusive_overlap(validity_period, criterion.certification_period)
path: itou/eligibility/utils.py
user: login: rsebille
body: Pourquoi créer une `InclusiveDateRange` et pas juste tester que `hiring_start_at` est dans le range de `certification_period` ? Je crois que ça fait pareil et que c'est plus simple, non ? :thinking: 

Ce qui d'ailleurs me fait me demander si il faudrait pas que ça soit une méthode sur le modèle ? Et éventuellement faire un template filter pour les templates plutôt qu'ajouter un attribut.
created_at: 2025-05-14T10:19:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,6 +18,35 @@ def iae_has_required_criteria(criteria, company_kind):
     return level_2_count >= ADMINISTRATIVE_CRITERIA_LEVEL_2_REQUIRED_FOR_SIAE_KIND[company_kind]
 
 
+def _inclusive_overlap(date_range1: InclusiveDateRange, date_range2: InclusiveDateRange):
+    return date_range1.lower <= date_range2.upper and date_range2.lower <= date_range1.upper
+
+
+def _criteria_for_display(selected_criteria, hiring_start_at):
+    for criterion in selected_criteria:
+        criterion.is_considered_certified = False
+        if hiring_start_at and criterion.certified:
+            validity_period = InclusiveDateRange(
+                hiring_start_at - datetime.timedelta(days=criterion.CERTIFICATION_GRACE_PERIOD_DAYS),
+                hiring_start_at,
+            )
+            criterion.is_considered_certified = _inclusive_overlap(validity_period, criterion.certification_period)
path: itou/eligibility/utils.py
user: login: francoisfreitag
body: > Pourquoi créer une InclusiveDateRange et pas juste tester que hiring_start_at est dans le range de certification_period ? Je crois que ça fait pareil et que c'est plus simple, non ? 🤔

L’idée était de porter à parité dans le premier commit en virant la requête SQL en trop (et donc en gardant le bug de double durée de certification de `is_considered_certified`), puis de dégager le bug en utilisant `contains` dans le second commit.

> Ce qui d'ailleurs me fait me demander si il faudrait pas que ça soit une méthode sur le modèle ? Et éventuellement faire un template filter pour les templates plutôt qu'ajouter un attribut.

Le problème est qu’il faut passer la `hiring_start_at`, et que c’est pénible à faire dans les templates. Je trouve un peu dommage d’injecter les données incomplètes (`hiring_start_at`, `eligibility_diagnosis`) pour finir par appeler du Python via un template filter custom qui ne sert que dans ce cas. Il me semble plus lisible, facile à maintenir, etc de formatter les données en Python dans les vues, pour que le template puisse simplement itérer et présenter.
created_at: 2025-05-15T08:01:25Z
author_association: MEMBER

PR comments:
user: login: francoisfreitag
created_at: 2025-05-13T14:58:14Z
body: On peut attendre lundi prochain, pas d’urgence, le bug est là depuis plusieurs mois et n’affectera pas beaucoup plus d’utilisateurs.
author_association: MEMBER
user: login: sentry[bot]
created_at: 2025-05-21T05:54:40Z
body: ## Suspect Issues
This pull request was deployed and Sentry observed the following issues:

- ‼️ **TypeError: argument of type 'NoneType' is not iterable** `/employees/detail/{public_id}` [View Issue](https://inclusion.sentry.io/issues/42627599/?referrer=github-pr-bot)

<sub>Did you find this useful? React with a 👍 or 👎</sub>
author_association: NONE