From 250a2b4f2ff9fa83dfc8f5c84679045b3e510584 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 18 Sep 2026 10:44:58 +0200 Subject: [PATCH] feat: add related inspections to intervention detail views and templates --- .../assets/components/asset_inspections.html | 24 ++- loko/assets/tests_inspections_views.py | 68 ++++++- .../controls/controls_detail_content.html | 4 +- .../controls/controls_detail_mobile.html | 6 +- .../controls/remark_form_content.html | 2 +- loko/inspections/models.py | 32 +++ .../static/inspections/inspections_index.js | 36 ++++ .../inspections/inspections_index.html | 26 ++- .../inspections/inspections_list.html | 42 +++- .../inspections/inspections_list_mobile.html | 42 +++- loko/inspections/tests.py | 78 +++++++- loko/inspections/views.py | 9 +- loko/interventions/models.py | 31 +++ .../interventions/detail/card_controles.html | 189 +++++++++++++++++- .../intervention_detail_mobile.html | 35 +++- loko/interventions/views/detail.py | 2 + loko/interventions/views/mobile.py | 2 + 17 files changed, 586 insertions(+), 42 deletions(-) diff --git a/loko/assets/templates/assets/components/asset_inspections.html b/loko/assets/templates/assets/components/asset_inspections.html index 9ec3245..4dba7e2 100644 --- a/loko/assets/templates/assets/components/asset_inspections.html +++ b/loko/assets/templates/assets/components/asset_inspections.html @@ -54,7 +54,7 @@ {{ insp.get_nature_display }} - {{ insp.inspector|short_name|default:insp.inspector.username|default:"-" }} + {{ insp.inspector|short_name|default:"-" }} @@ -100,11 +100,31 @@ {% endif %} + {% if insp.related_intervention %} + + {% endif %}
@@ -270,10 +272,17 @@
-
- - {% translate "Consulter la fiche de l'asset" %} - +
    @@ -285,6 +294,15 @@ {% translate "Type d'asset" %} - +
  • + {% translate "Intervention liée" %} + + + + + + +
  • {% translate "Date & Heure" %} - diff --git a/loko/inspections/templates/inspections/inspections_list.html b/loko/inspections/templates/inspections/inspections_list.html index e4aad71..8208a06 100644 --- a/loko/inspections/templates/inspections/inspections_list.html +++ b/loko/inspections/templates/inspections/inspections_list.html @@ -163,7 +163,7 @@ {% endif %} {% if insp.inspector %} - {{ insp.inspector|short_name|default:insp.inspector.username }} + {{ insp.inspector|short_name }} {% endif %}
@@ -190,12 +190,23 @@ {% endif %} - - {% if insp.asset %} -
- - {% translate "Consulter la fiche de l'asset" %} - + + {% if insp.asset or insp.related_intervention %} +
+ {% if insp.asset %} + + {% endif %} + {% if insp.related_intervention %} + + {% endif %}
{% endif %} @@ -208,6 +219,19 @@ {% translate "Type d'asset" %} {{ insp.asset_content_type.name }} + {% if insp.related_intervention %} +
  • + {% translate "Intervention liée" %} + + + {{ insp.related_intervention.code }} + + {% if insp.related_intervention.sub_type %} + {{ insp.related_intervention.sub_type.get_name }} + {% endif %} + +
  • + {% endif %}
  • {% translate "Date & Heure" %} {{ insp.inspection_date|date:"d/m/Y H:i" }} @@ -222,7 +246,7 @@
  • {% translate "Inspecteur" %} - {{ insp.inspector|short_name|default:insp.inspector.username|default:"-" }} + {{ insp.inspector|short_name|default:"-" }}
  • {% translate "Date de modification" %} @@ -477,7 +501,7 @@ document.addEventListener('DOMContentLoaded', function() { result: "{{ insp.get_result_status_display|escapejs }}", result_code: "{{ insp.result_status|escapejs }}", badge_class: "{{ insp.status_badge_class|escapejs }}", - inspector: "{{ insp.inspector|short_name|default:insp.inspector.username|escapejs }}", + inspector: "{{ insp.inspector|short_name|escapejs }}", nature: "{{ insp.get_nature_display|escapejs }}", summary: "{{ insp.details_summary|escapejs }}" }{% if not forloop.last %},{% endif %} diff --git a/loko/inspections/templates/inspections/inspections_list_mobile.html b/loko/inspections/templates/inspections/inspections_list_mobile.html index efdea91..2f9fe10 100644 --- a/loko/inspections/templates/inspections/inspections_list_mobile.html +++ b/loko/inspections/templates/inspections/inspections_list_mobile.html @@ -225,7 +225,7 @@ {% endif %} {% if insp.inspector %} - {{ insp.inspector|short_name|default:insp.inspector.username }} + {{ insp.inspector|short_name }} {% endif %}
  • {% endif %} @@ -253,12 +253,23 @@ {% endif %} - - {% if insp.asset %} -
    - - {% translate "Consulter la fiche de l'asset" %} - + + {% if insp.asset or insp.related_intervention %} +
    + {% if insp.asset %} + + {% endif %} + {% if insp.related_intervention %} + + {% endif %}
    {% endif %} @@ -271,6 +282,19 @@ {% translate "Type d'asset" %} {{ insp.asset_content_type.name }} + {% if insp.related_intervention %} +
  • + {% translate "Intervention liée" %} + + + {{ insp.related_intervention.code }} + + {% if insp.related_intervention.sub_type %} + {{ insp.related_intervention.sub_type.get_name }} + {% endif %} + +
  • + {% endif %} {% if insp.is_scheduled %}
  • {% translate "Statut" %} @@ -310,7 +334,7 @@
  • {% translate "Inspecteur" %} - {{ insp.inspector|short_name|default:insp.inspector.username|default:"-" }} + {{ insp.inspector|short_name|default:"-" }}
  • {% translate "Date de modification" %} @@ -570,7 +594,7 @@ document.addEventListener('DOMContentLoaded', function() { result: "{{ insp.get_result_status_display|escapejs }}", result_code: "{{ insp.result_status|escapejs }}", badge_class: "{{ insp.status_badge_class|escapejs }}", - inspector: "{{ insp.inspector|short_name|default:insp.inspector.username|escapejs }}", + inspector: "{{ insp.inspector|short_name|escapejs }}", nature: "{{ insp.get_nature_display|escapejs }}", summary: "{{ insp.details_summary|escapejs }}" }{% if not forloop.last %},{% endif %} diff --git a/loko/inspections/tests.py b/loko/inspections/tests.py index 64583a7..4d9866f 100644 --- a/loko/inspections/tests.py +++ b/loko/inspections/tests.py @@ -1492,7 +1492,7 @@ class PostInterventionDurabilityInspectionTestCase(TestCase): ) from common.models import UserConfig, Role, UserThematics inspector_role, _ = Role.objects.get_or_create(name='inspector') - config = UserConfig.objects.create(user=self.user) + config = UserConfig.objects.create(user=self.user, is_intern=True) config.roles.add(inspector_role) self.roads_thematic, _ = Thematic.objects.get_or_create( @@ -1535,6 +1535,18 @@ class PostInterventionDurabilityInspectionTestCase(TestCase): requires_post_inspection=False ) + def _create_finished_intervention(self, sub_type=None): + sub_type = sub_type or self.hotbox_subtype + return Intervention.objects.create( + thematic=self.roads_thematic, + type='repair', + sub_type=sub_type, + status='finished', + created_by=self.user, + assigned_team=self.team, + end_time=timezone.now() + ) + def test_hotbox_intervention_finished_triggers_inspection(self): from datetime import timedelta now = timezone.now() @@ -1773,5 +1785,69 @@ class PostInterventionDurabilityInspectionTestCase(TestCase): self.assertNotIn(cat_mobility, form.fields['concerned_categories'].queryset) self.assertIn('data-cat-models', form.fields['target_asset_model'].widget.attrs) + def test_inspection_related_intervention_properties_and_api(self): + """Vérifie les propriétés related_intervention de l'inspection et les données renvoyées par get_inspection_detail_api.""" + from inspections.services import schedule_post_intervention_inspection + intervention = self._create_finished_intervention(sub_type=self.hotbox_subtype) + insp = schedule_post_intervention_inspection(intervention) + self.assertIsNotNone(insp) + + # Propriétés du modèle + self.assertEqual(insp.related_intervention, intervention) + expected_url = reverse('interventions:intervention_detail', kwargs={'intervention_id': intervention.id}) + expected_mobile_url = reverse('interventions:intervention_detail_mobile', kwargs={'intervention_id': intervention.id}) + self.assertEqual(insp.related_intervention_url, expected_url) + self.assertEqual(insp.mobile_related_intervention_url, expected_mobile_url) + + # API get_inspection_detail_api + self.client.force_login(self.user) + api_url = reverse('inspections:get_inspection_detail_api', kwargs={'inspection_id': insp.id}) + resp = self.client.get(api_url) + self.assertEqual(resp.status_code, 200) + data = resp.json() + self.assertTrue(data['success']) + self.assertEqual(data['intervention_id'], intervention.id) + self.assertEqual(data['intervention_code'], intervention.code) + self.assertEqual(data['intervention_url'], expected_url) + self.assertEqual(data['mobile_intervention_url'], expected_mobile_url) + + def test_intervention_related_inspections_and_detail_view(self): + """Vérifie que l'intervention expose ses inspections liées et que la vue détail les affiche dans le bloc Contrôles.""" + from inspections.services import schedule_post_intervention_inspection + intervention = self._create_finished_intervention(sub_type=self.hotbox_subtype) + insp = schedule_post_intervention_inspection(intervention) + self.assertIsNotNone(insp) + + # Méthode du modèle + related = intervention.get_related_inspections(user=self.user) + self.assertIn(insp, related) + + # Page détail de l'intervention + self.client.force_login(self.user) + detail_url = reverse('interventions:intervention_detail', kwargs={'intervention_id': intervention.id}) + resp = self.client.get(detail_url) + self.assertEqual(resp.status_code, 200) + self.assertIn('visible_inspections', resp.context) + self.assertIn(insp, resp.context['visible_inspections']) + # Vérifie la présence du lien vers l'inspection dans le HTML + self.assertContains(resp, f"inspection_id={insp.id}") + self.assertContains(resp, "Inspections de terrain / conformité") + + def test_intervention_detail_mobile_renders_linked_inspections(self): + """Vérifie que la vue mobile de l'intervention affiche aussi les inspections dans le bloc Contrôles.""" + from inspections.services import schedule_post_intervention_inspection + intervention = self._create_finished_intervention(sub_type=self.hotbox_subtype) + insp = schedule_post_intervention_inspection(intervention) + self.assertIsNotNone(insp) + + self.client.force_login(self.user) + mobile_url = reverse('interventions:intervention_detail_mobile', kwargs={'intervention_id': intervention.id}) + resp = self.client.get(mobile_url) + self.assertEqual(resp.status_code, 200) + self.assertIn('visible_inspections', resp.context) + self.assertIn(insp, resp.context['visible_inspections']) + self.assertContains(resp, f"inspection_id={insp.id}") + self.assertContains(resp, "Inspections de terrain") + diff --git a/loko/inspections/views.py b/loko/inspections/views.py index 80d0a30..2055bc1 100644 --- a/loko/inspections/views.py +++ b/loko/inspections/views.py @@ -864,6 +864,11 @@ def get_inspection_detail_api(request, inspection_id): 'checked_intervention_code': leaf.checked_intervention.code if leaf.checked_intervention else '', 'checked_intervention_id': leaf.checked_intervention_id, 'checked_intervention_subtype': leaf.checked_intervention.sub_type.get_name() if leaf.checked_intervention and leaf.checked_intervention.sub_type else '', + 'intervention_id': leaf.related_intervention.id if leaf.related_intervention else None, + 'intervention_code': leaf.related_intervention.code if leaf.related_intervention else '', + 'intervention_url': leaf.related_intervention_url, + 'mobile_intervention_url': leaf.mobile_related_intervention_url, + 'intervention_subtype': leaf.related_intervention.sub_type.get_name() if leaf.related_intervention and getattr(leaf.related_intervention, 'sub_type', None) else '', 'inspector_name': get_short_name(leaf.inspector) if leaf.inspector else '-', 'inspector_role': leaf.inspector_role, 'inspector_role_display': str(leaf.get_inspector_role_display()) if leaf.inspector_role else '', @@ -1173,8 +1178,8 @@ def inspections_geojson(request): ct_ids.add(ContentType.objects.get_for_model(SignPole).id) elif thematic_code == 'parking': from assets.models import ParkingSpot, ParkingLocation - ct_ids.add(ContentType.objects.get_for_model(ParkingSpot)) - ct_ids.add(ContentType.objects.get_for_model(ParkingLocation)) + ct_ids.add(ContentType.objects.get_for_model(ParkingSpot).id) + ct_ids.add(ContentType.objects.get_for_model(ParkingLocation).id) except Thematic.DoesNotExist: pass diff --git a/loko/interventions/models.py b/loko/interventions/models.py index 29cf51b..7e21850 100644 --- a/loko/interventions/models.py +++ b/loko/interventions/models.py @@ -849,6 +849,37 @@ class Intervention(models.Model): ), ] + def get_absolute_url(self): + from django.urls import reverse + return reverse('interventions:intervention_detail', kwargs={'intervention_id': self.pk}) + + def get_related_inspections(self, user=None): + """ + Retourne l'ensemble des inspections liées à cette intervention : + - quality_inspections (checked_intervention, ex: durabilité post-travaux) + - generated_inspections (mission_intervention, ex: inspection de contrôle) + - inspections ciblant directement cette intervention + """ + from inspections.models import Inspection + from django.contrib.contenttypes.models import ContentType + ct = ContentType.objects.get_for_model(self.__class__) + qs = Inspection.objects.filter( + models.Q(checked_intervention=self) | + models.Q(mission_intervention=self) | + (models.Q(asset_content_type=ct) & models.Q(asset_object_id=self.id)) + ).distinct().select_related( + 'asset_content_type', 'inspector', 'checked_intervention', 'mission_intervention' + ).prefetch_related('documents').order_by('-inspection_date', '-created_at') + + if user: + from inspections.permissions import can_view_inspection + return [insp for insp in qs if can_view_inspection(user, insp)] + return list(qs) + + @property + def related_inspections(self): + return self.get_related_inspections() + @property def intervention_sheet_filename(self): if self.intervention_sheet: diff --git a/loko/interventions/templates/interventions/detail/card_controles.html b/loko/interventions/templates/interventions/detail/card_controles.html index f8de10d..c8d5af0 100644 --- a/loko/interventions/templates/interventions/detail/card_controles.html +++ b/loko/interventions/templates/interventions/detail/card_controles.html @@ -1,5 +1,5 @@ -{% load i18n %} -
    +{% load i18n short_name %} +

    {% translate "Contrôles / Rapports de suivi" %}

    {% if 'follow_up_reports' in allowed_update_fields %} @@ -43,7 +43,11 @@
    {% endif %} - + {% if visible_controls %} +
    + {% translate "Rapports de contrôle" %} +
    +
    @@ -63,9 +67,188 @@ {% endfor %}
    {% translate "Date" %}
    + {% elif not visible_inspections %} +

    {% translate "Aucun contrôle ou rapport pour le moment." %}

    + {% endif %} + + {% if visible_inspections %} +
    +
    +
    + {% translate "Inspections de terrain / conformité" %} + {{ visible_inspections|length }} +
    +
    +
    + + + + + + + + + + + + + + {% for insp in visible_inspections %} + + + + + + + + + + {% endfor %} + +
    {% translate "Inspection" %}{% translate "Date / Échéance" %}{% translate "Nature" %}{% translate "Cible" %}{% translate "Inspecteur" %}{% translate "Statut / Résultat" %}{% translate "Action" %}
    + + #{{ insp.id }} + + + {{ insp.inspection_date|date:"d/m/Y H:i"|default:"-" }} + {% if insp.is_scheduled and insp.scheduled_start_date %} +
    + {% translate "Prévue du" %} {{ insp.scheduled_start_date|date:"d/m/Y" }} {% translate "au" %} {{ insp.scheduled_due_date|date:"d/m/Y" }} +
    + {% endif %} +
    + {{ insp.get_nature_display }} + + {% if insp.asset and insp.asset.get_absolute_url %} + + {{ insp.target_display }} + + {% else %} + {{ insp.target_display }} + {% endif %} + + {{ insp.inspector|short_name|default:"-" }} + + + {{ insp.get_result_status_display }} + + {% if insp.is_scheduled %} + {% translate "Planifiée" %} + {% endif %} + + + + + +
    +
    +
    + {% endif %}
    +{% for insp in visible_inspections %} + + +{% endfor %} + diff --git a/loko/interventions/templates/interventions/intervention_detail_mobile.html b/loko/interventions/templates/interventions/intervention_detail_mobile.html index 78d2940..23320bb 100644 --- a/loko/interventions/templates/interventions/intervention_detail_mobile.html +++ b/loko/interventions/templates/interventions/intervention_detail_mobile.html @@ -1398,9 +1398,42 @@ {% endfor %} - {% else %} + {% elif not visible_inspections %}

    {% translate "Aucun contrôle pour le moment." %}

    {% endif %} + + {% if visible_inspections %} +
    +
    + {% translate "Inspections de terrain" %} ({{ visible_inspections|length }}) +
    + {% for insp in visible_inspections %} +
    +
    + + {% translate "Inspection" %} #{{ insp.id }} + + + {{ insp.get_result_status_display }} + +
    +
    + {{ insp.get_nature_display }} + {% if insp.is_scheduled %} + {% translate "Planifiée" %} + {% endif %} +
    +
    + {{ insp.target_display }} +
    +
    + {{ insp.inspection_date|date:"d/m/Y H:i"|default:"-" }} + {{ insp.inspector|short_name|default:"-" }} +
    +
    + {% endfor %} +
    + {% endif %}
  • {% endif %} diff --git a/loko/interventions/views/detail.py b/loko/interventions/views/detail.py index f10c319..6af23cb 100644 --- a/loko/interventions/views/detail.py +++ b/loko/interventions/views/detail.py @@ -502,6 +502,7 @@ def intervention_detail(request, intervention_id): control for control in intervention.related_controls.all() if user_can_view_control(request.user, control) ] + visible_inspections = intervention.get_related_inspections(user=request.user) # Bons de préparation liés à l'intervention: # - admin @@ -741,6 +742,7 @@ def intervention_detail(request, intervention_id): 'can_take_over': bool(contract and user_contract_access), # Contrôles visibles par l'utilisateur 'visible_controls': visible_controls, + 'visible_inspections': visible_inspections, # Équipes filtrées par thématique 'available_teams': available_teams, # Permission de voir les messages de correction diff --git a/loko/interventions/views/mobile.py b/loko/interventions/views/mobile.py index 96e5a19..e5b8a22 100644 --- a/loko/interventions/views/mobile.py +++ b/loko/interventions/views/mobile.py @@ -912,6 +912,7 @@ def intervention_detail_mobile(request, intervention_id): control for control in intervention.related_controls.all() if user_can_view_control(request.user, control) ] + visible_inspections = intervention.get_related_inspections(user=request.user) # Historique des interventions liées aux mêmes localisations (3 derniers mois) location_history_interventions = [] @@ -1151,6 +1152,7 @@ def intervention_detail_mobile(request, intervention_id): 'is_restricted_team': is_restricted_team, # Contrôles visibles par l'utilisateur 'visible_controls': visible_controls, + 'visible_inspections': visible_inspections, 'tl_intersection_ids': tl_intersection_ids, # --- Timeline mobile --- 'status_choices': [{"key": key, "label": label} for key, label in STATUS_CHOICES.items()],