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 %}
{% translate "Équipement cible" %}
{{ insp.target_display }}
+ {% 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" }}
@@ -119,7 +139,7 @@
{% translate "Inspecteur" %}
- {{ insp.inspector|short_name|default:insp.inspector.username|default:"-" }}
+ {{ insp.inspector|short_name|default:"-" }}
{% translate "Date de modification" %}
diff --git a/loko/assets/tests_inspections_views.py b/loko/assets/tests_inspections_views.py
index 7ff0704..9dea371 100644
--- a/loko/assets/tests_inspections_views.py
+++ b/loko/assets/tests_inspections_views.py
@@ -24,6 +24,8 @@ class InspectionViewsTestCase(TestCase):
from common.models import UserConfig, UserThematics, Thematic
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
+ user_config.is_intern = True
+ user_config.save()
t_parking, _ = Thematic.objects.get_or_create(code='parking', defaults={'name_fr': 'Stationnement'})
UserThematics.objects.update_or_create(user_config=user_config, thematic=t_parking, defaults={'can_inspect': True, 'can_view_assets': True})
self.client.force_login(self.user)
@@ -37,7 +39,7 @@ class InspectionViewsTestCase(TestCase):
'standard_stand_count': 5
}
)
- self.spot.geom = Polygon(((4.35, 50.85), (4.36, 50.85), (4.36, 50.86), (4.35, 50.86), (4.35, 50.85)), srid=4326)
+ self.spot.geom = Polygon(((150000, 170000), (150010, 170000), (150010, 170010), (150000, 170010), (150000, 170000)), srid=3812)
self.spot.save()
self.spot_ct = ContentType.objects.get_for_model(ParkingSpot)
@@ -52,7 +54,10 @@ class InspectionViewsTestCase(TestCase):
response = self.client.get(reverse('inspections:inspections_list'))
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'PK-001')
+
+ response_mobile = self.client.get(reverse('mobile:inspections_list_mobile'))
+ self.assertEqual(response_mobile.status_code, 200)
+ self.assertContains(response_mobile, 'PK-001')
def test_quick_inspection_map_view(self):
response = self.client.get(reverse('inspections:quick_inspection_map'))
@@ -162,7 +167,7 @@ class InspectionViewsTestCase(TestCase):
def test_inspections_geojson_view(self):
from django.contrib.gis.geos import Polygon
- self.spot.geom = Polygon(((4.35, 50.85), (4.36, 50.85), (4.36, 50.86), (4.35, 50.86), (4.35, 50.85)), srid=4326)
+ self.spot.geom = Polygon(((150000, 170000), (150010, 170000), (150010, 170010), (150000, 170010), (150000, 170000)), srid=3812)
self.spot.save()
AssetInspection.objects.filter(asset_content_type=self.spot_ct, asset_object_id=self.spot.id).delete()
@@ -198,7 +203,7 @@ class InspectionViewsTestCase(TestCase):
nature='visual'
)
- response = self.client.get(reverse('inspections:inspections_list') + '?tab=all&q=SEARCH-99')
+ response = self.client.get(reverse('mobile:inspections_list_mobile') + '?tab=all&q=SEARCH-99')
self.assertEqual(response.status_code, 200)
self.assertIn(insp, response.context['inspections'])
@@ -209,11 +214,13 @@ class InspectionViewsTestCase(TestCase):
user_custom = User.objects.create_user(username='user_custom_perm', password='pwd')
user_config, _ = UserConfig.objects.get_or_create(user=user_custom)
+ user_config.is_intern = True
+ user_config.save()
UserThematics.objects.create(user_config=user_config, thematic=t_parking, can_view_assets=True, can_inspect=False)
UserThematics.objects.create(user_config=user_config, thematic=t_lighting, can_view_assets=False, can_inspect=False)
self.client.force_login(user_custom)
- resp = self.client.get(reverse('inspections:inspections_list'))
+ resp = self.client.get(reverse('mobile:inspections_list_mobile'))
self.assertEqual(resp.status_code, 200)
self.assertIn(t_parking, resp.context['thematics'])
self.assertNotIn(t_lighting, resp.context['thematics'])
@@ -246,3 +253,54 @@ class InspectionViewsTestCase(TestCase):
self.assertEqual(data['total_count'], 1)
self.assertIn(insp, data['inspections'])
self.assertEqual(data['asset_pk'], self.spot.pk)
+
+ def test_asset_inspections_templatetag_renders_with_null_inspector(self):
+ """Vérifie que le composant asset_inspections se rend sans erreur quand inspector est None."""
+ from assets.models import AssetInspection
+ from django.template import Template, Context
+
+ AssetInspection.objects.filter(asset_content_type=self.spot_ct, asset_object_id=self.spot.id).delete()
+ insp = AssetInspection.objects.create(
+ asset=self.spot,
+ inspector=None,
+ result_status='pending',
+ nature='post_intervention'
+ )
+
+ request = self.client.get(reverse('inspections:inspections_list')).wsgi_request
+ t = Template('{% load asset_tags %}{% asset_inspections asset %}')
+ rendered = t.render(Context({'request': request, 'asset': self.spot}))
+ self.assertIn('assetInspectionDetailModal', rendered)
+ self.assertIn('-', rendered)
+
+ def test_roadway_detail_renders_with_inspection_without_inspector(self):
+ """Vérifie que la page de détail d'une Roadway avec une inspection planifiée (sans inspecteur) ne plante pas (500)."""
+ from assets.models.roads import RoadStreet, Roadway
+ from common.models import UserThematics, Thematic, UserConfig
+ from inspections.models import Inspection
+ from django.contrib.gis.geos import MultiPolygon, Polygon
+
+ t_roads, _ = Thematic.objects.get_or_create(code='roads', defaults={'name_fr': 'Voiries'})
+ user_config = UserConfig.objects.get(user=self.user)
+ UserThematics.objects.update_or_create(user_config=user_config, thematic=t_roads, defaults={'can_view_assets': True})
+
+ street = RoadStreet.objects.create(code='WAT_R22_TEST_VIEW', name_fr='Chaussée Test')
+ p = Polygon(((150000, 170000), (150010, 170000), (150010, 170010), (150000, 170010), (150000, 170000)))
+ roadway = Roadway.objects.create(
+ street=street,
+ geom=MultiPolygon(p, srid=3812)
+ )
+
+ # Créer une inspection ciblant la Roadway sans inspecteur (comme une inspection de durabilité automatique)
+ insp = Inspection.objects.create(
+ asset_content_type=ContentType.objects.get_for_model(Roadway),
+ asset_object_id=roadway.id,
+ inspector=None,
+ nature='post_intervention',
+ status='scheduled'
+ )
+
+ url = reverse('assets:roads_assets_detail', kwargs={'asset_model': 'roadway', 'asset_id': roadway.id})
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
diff --git a/loko/controls/templates/controls/controls_detail_content.html b/loko/controls/templates/controls/controls_detail_content.html
index f07c6bb..289d923 100644
--- a/loko/controls/templates/controls/controls_detail_content.html
+++ b/loko/controls/templates/controls/controls_detail_content.html
@@ -435,7 +435,7 @@
{{ entry.get_status_display }}
- ({% translate 'par' %} {{ entry.created_by|short_name|default:entry.created_by.username }})
+ ({% translate 'par' %} {{ entry.created_by|short_name|default:"-" }})
{% if entry.description %}
{{ entry.description }}
@@ -486,7 +486,7 @@
{{ remark.get_status_display }}
- ({% translate 'par' %} {{ remark.updated_by|short_name|default:remark.updated_by.username }})
+ ({% translate 'par' %} {{ remark.updated_by|short_name|default:"-" }})
{% if remark.description %}
{{ remark.description }}
diff --git a/loko/controls/templates/controls/controls_detail_mobile.html b/loko/controls/templates/controls/controls_detail_mobile.html
index 2745859..4c601cd 100644
--- a/loko/controls/templates/controls/controls_detail_mobile.html
+++ b/loko/controls/templates/controls/controls_detail_mobile.html
@@ -467,7 +467,7 @@
{% load short_name %}
- {% translate 'Mise à jour par' %}
+ {% translate 'Mise à jour par' %}
{% translate 'le' %}
@@ -483,7 +483,7 @@
{{ entry.get_status_display }}
- ({% translate 'par' %} {{ entry.created_by|short_name|default:entry.created_by.username }})
+ ({% translate 'par' %} {{ entry.created_by|short_name|default:"-" }})
{% if entry.description %}
{{ entry.description }}
@@ -534,7 +534,7 @@
{{ remark.get_status_display }}
- ({% translate 'par' %} {{ remark.updated_by|short_name|default:remark.updated_by.username }})
+ ({% translate 'par' %} {{ remark.updated_by|short_name|default:"-" }})
{% if remark.description %}
{{ remark.description }}
diff --git a/loko/controls/templates/controls/remark_form_content.html b/loko/controls/templates/controls/remark_form_content.html
index 5e76196..98446d8 100644
--- a/loko/controls/templates/controls/remark_form_content.html
+++ b/loko/controls/templates/controls/remark_form_content.html
@@ -36,7 +36,7 @@
{{ entry.get_status_display }}
- ({% translate 'par' %} {{ entry.created_by|short_name|default:entry.created_by.username }})
+ ({% translate 'par' %} {{ entry.created_by|short_name|default:"-" }})
{% if entry.description %}
{{ entry.description }}
diff --git a/loko/inspections/models.py b/loko/inspections/models.py
index 1832b59..efc0b7d 100644
--- a/loko/inspections/models.py
+++ b/loko/inspections/models.py
@@ -244,6 +244,38 @@ class Inspection(models.Model):
def is_cross_check(self):
return self.checked_intervention_id is not None
+ @property
+ def related_intervention(self):
+ """
+ Renvoie l'intervention liée à cette inspection si elle existe :
+ - checked_intervention (intervention de travaux contrôlée, ex: durabilité)
+ - mission_intervention (mission d'inspection)
+ - asset si la cible de l'inspection est une intervention
+ """
+ if self.checked_intervention_id:
+ return self.checked_intervention
+ if self.mission_intervention_id:
+ return self.mission_intervention
+ if self.asset_content_type and self.asset_content_type.model == 'intervention':
+ return self.asset
+ return None
+
+ @property
+ def related_intervention_url(self):
+ interv = self.related_intervention
+ if interv:
+ from django.urls import reverse
+ return reverse('interventions:intervention_detail', kwargs={'intervention_id': interv.id})
+ return ''
+
+ @property
+ def mobile_related_intervention_url(self):
+ interv = self.related_intervention
+ if interv:
+ from django.urls import reverse
+ return reverse('interventions:intervention_detail_mobile', kwargs={'intervention_id': interv.id})
+ return ''
+
@property
def is_cancelled(self):
return self.result_status == 'cancelled' or self.status == 'cancelled'
diff --git a/loko/inspections/static/inspections/inspections_index.js b/loko/inspections/static/inspections/inspections_index.js
index 65988bc..4732bb6 100644
--- a/loko/inspections/static/inspections/inspections_index.js
+++ b/loko/inspections/static/inspections/inspections_index.js
@@ -88,6 +88,11 @@ document.addEventListener('DOMContentLoaded', () => {
inspectionsgeojsonloader.loadGeojson(initialUrl);
_updateExportExcelUrl(query);
+
+ const urlInspectionId = new URLSearchParams(window.location.search).get('inspection_id');
+ if (urlInspectionId) {
+ openInspectionDetailModal(parseInt(urlInspectionId, 10));
+ }
});
inspectionsgeojsonloader.addEventListener('geojsonLoaded', (event) => {
@@ -923,6 +928,37 @@ window.openInspectionDetailModal = function(inspectionId) {
}
}
+ // Intervention link
+ const intervBtnContainer = document.getElementById('detail-intervention-btn-container');
+ const intervLink = document.getElementById('detail-intervention-link');
+ const intervBtnText = document.getElementById('detail-intervention-btn-text');
+ const intervRow = document.getElementById('detail-intervention-row');
+ const intervRowLink = document.getElementById('detail-intervention-row-link');
+ const intervCode = document.getElementById('detail-intervention-code');
+ const intervBadge = document.getElementById('detail-intervention-badge');
+
+ if (data.intervention_url && data.intervention_code) {
+ if (intervLink) intervLink.href = data.intervention_url;
+ if (intervBtnText) intervBtnText.textContent = `${gettext("Intervention")} ${data.intervention_code}`;
+ if (intervBtnContainer) intervBtnContainer.classList.remove('d-none');
+ if (intervRow) {
+ intervRow.classList.remove('d-none');
+ if (intervRowLink) intervRowLink.href = data.intervention_url;
+ if (intervCode) intervCode.textContent = data.intervention_code;
+ if (intervBadge) {
+ if (data.intervention_subtype) {
+ intervBadge.textContent = data.intervention_subtype;
+ intervBadge.classList.remove('d-none');
+ } else {
+ intervBadge.classList.add('d-none');
+ }
+ }
+ }
+ } else {
+ if (intervBtnContainer) intervBtnContainer.classList.add('d-none');
+ if (intervRow) intervRow.classList.add('d-none');
+ }
+
// Key fields
document.getElementById('detail-target-display').textContent = data.target_display || '-';
document.getElementById('detail-asset-type').textContent = data.asset_type_name || '-';
diff --git a/loko/inspections/templates/inspections/inspections_index.html b/loko/inspections/templates/inspections/inspections_index.html
index 35f58f0..0a8321d 100644
--- a/loko/inspections/templates/inspections/inspections_index.html
+++ b/loko/inspections/templates/inspections/inspections_index.html
@@ -186,11 +186,13 @@
{% translate "Inspections" %}
+ {% if can_inspect %}
+ {% 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 %}
+
{% endif %}
-
+ {% if visible_controls %}
+
+ {% translate "Rapports de contrôle" %}
+
+
{% translate "Date" %}
@@ -63,9 +67,188 @@
{% endfor %}
+ {% 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 }}
+
+
+
+
+
+
+ {% translate "Inspection" %}
+ {% translate "Date / Échéance" %}
+ {% translate "Nature" %}
+ {% translate "Cible" %}
+ {% translate "Inspecteur" %}
+ {% translate "Statut / Résultat" %}
+ {% translate "Action" %}
+
+
+
+ {% for insp in visible_inspections %}
+
+
+
+ #{{ 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 %}
+
+
+
+ {% translate "Détail" %}
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+{% for insp in visible_inspections %}
+
+
+
+
+
+
+ {% if insp.is_cancelled %}
+
+
+
+ {% translate "Inspection annulée" %} — {% translate "Cette inspection a été annulée et n'est plus prise en compte dans la conformité active de la cible." %}
+
+
+ {% endif %}
+
+ {% if insp.asset and insp.asset.get_absolute_url %}
+
+ {% endif %}
+
+
+
+ {% translate "Cible" %}
+ {{ insp.target_display }}
+
+
+ {% translate "Type d'asset" %}
+ {{ insp.asset_content_type.name }}
+
+
+ {% translate "Date & Heure" %}
+ {{ insp.inspection_date|date:"d/m/Y H:i"|default:"-" }}
+
+
+ {% translate "Résultat du contrôle" %}
+ {{ insp.get_result_status_display }}
+
+
+ {% translate "Nature" %}
+ {{ insp.get_nature_display }}
+
+
+ {% translate "Inspecteur" %}
+ {{ insp.inspector|short_name|default:"-" }}
+
+
+ {% translate "Date de modification" %}
+ {{ insp.updated_at|date:"d/m/Y H:i"|default:"-" }}
+
+
+
+ {% if insp.notes %}
+
+
{% translate "Remarques" %}
+
{{ insp.notes }}
+
+ {% endif %}
+
+ {% if insp.documents.all %}
+
+
+ {% translate "Photos & Documents joints" %} ({{ insp.documents.count }})
+
+
+ {% for doc in insp.documents.all %}
+ {% if doc.is_image %}
+
+
+
+ {% else %}
+
+
+ {{ doc.filename }}
+
+ {% endif %}
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
+
+
+{% 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()],