feat: add related inspections to intervention detail views and templates
This commit is contained in:
parent
6c80028906
commit
250a2b4f2f
17 changed files with 586 additions and 42 deletions
|
|
@ -54,7 +54,7 @@
|
|||
<span class="badge bg-light text-dark border">{{ insp.get_nature_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<i class="bi bi-person me-1 text-muted"></i>{{ insp.inspector|short_name|default:insp.inspector.username|default:"-" }}
|
||||
<i class="bi bi-person me-1 text-muted"></i>{{ insp.inspector|short_name|default:"-" }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="small text-muted text-truncate d-inline-block" style="max-width: 220px;" title="{{ insp.details_summary }}">
|
||||
|
|
@ -100,11 +100,31 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if insp.related_intervention %}
|
||||
<div class="mb-3">
|
||||
<a href="{% url 'interventions:intervention_detail' insp.related_intervention.id %}" class="btn btn-outline-primary w-100 fw-semibold" target="_blank">
|
||||
<i class="bi bi-tools me-2"></i>{% translate "Consulter l'intervention" %} {{ insp.related_intervention.code }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul class="list-group list-group-flush mb-3">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-geo-alt me-1"></i>{% translate "Équipement cible" %}</span>
|
||||
<span class="fw-bold text-end ms-2">{{ insp.target_display }}</span>
|
||||
</li>
|
||||
{% if insp.related_intervention %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-tools me-1"></i>{% translate "Intervention liée" %}</span>
|
||||
<span class="fw-semibold text-end ms-2">
|
||||
<a href="{% url 'interventions:intervention_detail' insp.related_intervention.id %}" class="text-decoration-none fw-bold" target="_blank">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>{{ insp.related_intervention.code }}
|
||||
</a>
|
||||
{% if insp.related_intervention.sub_type %}
|
||||
<span class="badge bg-light text-dark border ms-1">{{ insp.related_intervention.sub_type.get_name }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-calendar-event me-1"></i>{% translate "Date & Heure" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
||||
|
|
@ -119,7 +139,7 @@
|
|||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-person me-1"></i>{% translate "Inspecteur" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspector|short_name|default:insp.inspector.username|default:"-" }}</span>
|
||||
<span class="fw-semibold">{{ insp.inspector|short_name|default:"-" }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-clock-history me-1"></i>{% translate "Date de modification" %}</span>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@
|
|||
<i class="bi {{ entry.status|status_icon }}"></i>
|
||||
{{ entry.get_status_display }}
|
||||
</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ entry.created_by|short_name|default:entry.created_by.username }})</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ entry.created_by|short_name|default:"-" }})</span>
|
||||
</div>
|
||||
{% if entry.description %}
|
||||
<div class="mt-1 text-dark" style="white-space: pre-wrap; font-size: 0.95rem;">{{ entry.description }}</div>
|
||||
|
|
@ -486,7 +486,7 @@
|
|||
<i class="bi {{ remark.status|status_icon }}"></i>
|
||||
{{ remark.get_status_display }}
|
||||
</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ remark.updated_by|short_name|default:remark.updated_by.username }})</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ remark.updated_by|short_name|default:"-" }})</span>
|
||||
</div>
|
||||
{% if remark.description %}
|
||||
<div class="mt-1 text-dark" style="white-space: pre-wrap; font-size: 0.95rem;">{{ remark.description }}</div>
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@
|
|||
<!-- ✅ Infos sur l’utilisateur et la date (dernière mise à jour) -->
|
||||
<p class="small text-muted mb-2">
|
||||
{% load short_name %}
|
||||
{% translate 'Mise à jour par' %} <span id="updated-by-{{ remark.id }}">{{ remark.updated_by|short_name|default:remark.updated_by.username }}</span>
|
||||
{% translate 'Mise à jour par' %} <span id="updated-by-{{ remark.id }}">{{ remark.updated_by|short_name|default:"-" }}</span>
|
||||
{% translate 'le' %} <span id="updated-at-{{ remark.id }}">{{ remark.updated_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}</span>
|
||||
</p>
|
||||
|
||||
|
|
@ -483,7 +483,7 @@
|
|||
<i class="bi {{ entry.status|status_icon }}"></i>
|
||||
{{ entry.get_status_display }}
|
||||
</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ entry.created_by|short_name|default:entry.created_by.username }})</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ entry.created_by|short_name|default:"-" }})</span>
|
||||
</div>
|
||||
{% if entry.description %}
|
||||
<div class="mt-1 text-dark" style="white-space: pre-wrap; font-size: 0.95rem;">{{ entry.description }}</div>
|
||||
|
|
@ -534,7 +534,7 @@
|
|||
<i class="bi {{ remark.status|status_icon }}"></i>
|
||||
{{ remark.get_status_display }}
|
||||
</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ remark.updated_by|short_name|default:remark.updated_by.username }})</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ remark.updated_by|short_name|default:"-" }})</span>
|
||||
</div>
|
||||
{% if remark.description %}
|
||||
<div class="mt-1 text-dark" style="white-space: pre-wrap; font-size: 0.95rem;">{{ remark.description }}</div>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
<i class="bi {{ entry.status|status_icon }}"></i>
|
||||
{{ entry.get_status_display }}
|
||||
</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ entry.created_by|short_name|default:entry.created_by.username }})</span>
|
||||
<span class="small text-muted">({% translate 'par' %} {{ entry.created_by|short_name|default:"-" }})</span>
|
||||
</div>
|
||||
{% if entry.description %}
|
||||
<div class="small text-dark" style="white-space: pre-wrap;">{{ entry.description }}</div>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 || '-';
|
||||
|
|
|
|||
|
|
@ -186,11 +186,13 @@
|
|||
<button class="nav-link active" id="inspections-table-tab" data-bs-toggle="tab" data-bs-target="#inspections-table-content" type="button" role="tab" aria-controls="inspections-table" aria-selected="true">{% translate "Inspections" %}</button>
|
||||
</li>
|
||||
|
||||
{% if can_inspect %}
|
||||
<div class="my-1 ms-2">
|
||||
<a href="{% url 'inspections:quick_inspection_map' %}" class="btn btn-light" title="{% translate 'Nouvelle inspection sur carte' %}">
|
||||
<i class="bi bi-plus-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content my-2" id="tablesTabContent">
|
||||
|
|
@ -270,10 +272,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="detail-asset-btn-container" class="mb-3 d-none">
|
||||
<a href="#" id="detail-asset-link" class="btn btn-primary w-100 fw-semibold">
|
||||
<i class="bi bi-box-arrow-up-right me-2"></i>{% translate "Consulter la fiche de l'asset" %}
|
||||
</a>
|
||||
<div class="row g-2 mb-3">
|
||||
<div id="detail-asset-btn-container" class="col-12 col-sm d-none">
|
||||
<a href="#" id="detail-asset-link" class="btn btn-primary w-100 fw-semibold">
|
||||
<i class="bi bi-box-arrow-up-right me-2"></i>{% translate "Consulter la fiche de l'asset" %}
|
||||
</a>
|
||||
</div>
|
||||
<div id="detail-intervention-btn-container" class="col-12 col-sm d-none">
|
||||
<a href="#" id="detail-intervention-link" class="btn btn-outline-primary w-100 fw-semibold">
|
||||
<i class="bi bi-tools me-2"></i><span id="detail-intervention-btn-text">{% translate "Consulter l'intervention" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-group list-group-flush mb-3">
|
||||
|
|
@ -285,6 +294,15 @@
|
|||
<span class="text-muted"><i class="bi bi-layers me-1"></i>{% translate "Type d'asset" %}</span>
|
||||
<span class="badge bg-secondary" id="detail-asset-type">-</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0 d-none" id="detail-intervention-row">
|
||||
<span class="text-muted"><i class="bi bi-tools me-1"></i>{% translate "Intervention liée" %}</span>
|
||||
<span class="fw-semibold text-end ms-2">
|
||||
<a href="#" id="detail-intervention-row-link" class="text-decoration-none fw-bold">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i><span id="detail-intervention-code"></span>
|
||||
</a>
|
||||
<span id="detail-intervention-badge" class="badge bg-light text-dark border ms-1 d-none"></span>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-calendar-event me-1"></i>{% translate "Date & Heure" %}</span>
|
||||
<span class="fw-semibold" id="detail-date">-</span>
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@
|
|||
</span>
|
||||
{% endif %}
|
||||
{% if insp.inspector %}
|
||||
<span><i class="bi bi-person me-1"></i>{{ insp.inspector|short_name|default:insp.inspector.username }}</span>
|
||||
<span><i class="bi bi-person me-1"></i>{{ insp.inspector|short_name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -190,12 +190,23 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Lien vers la fiche de l'asset -->
|
||||
{% if insp.asset %}
|
||||
<div class="mb-3">
|
||||
<a href="{% if is_mobile %}{{ insp.mobile_asset_url }}{% else %}{{ insp.asset.get_absolute_url }}{% endif %}" class="btn btn-primary w-100 fw-semibold">
|
||||
<i class="bi bi-box-arrow-up-right me-2"></i>{% translate "Consulter la fiche de l'asset" %}
|
||||
</a>
|
||||
<!-- Boutons d'action : asset et/ou intervention -->
|
||||
{% if insp.asset or insp.related_intervention %}
|
||||
<div class="row g-2 mb-3">
|
||||
{% if insp.asset %}
|
||||
<div class="col-12 {% if insp.related_intervention %}col-sm-6{% endif %}">
|
||||
<a href="{% if is_mobile %}{{ insp.mobile_asset_url }}{% else %}{{ insp.asset.get_absolute_url }}{% endif %}" class="btn btn-primary w-100 fw-semibold">
|
||||
<i class="bi bi-box-arrow-up-right me-2"></i>{% translate "Consulter la fiche de l'asset" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if insp.related_intervention %}
|
||||
<div class="col-12 {% if insp.asset %}col-sm-6{% endif %}">
|
||||
<a href="{% if is_mobile %}{{ insp.mobile_related_intervention_url }}{% else %}{{ insp.related_intervention_url }}{% endif %}" class="btn btn-outline-primary w-100 fw-semibold">
|
||||
<i class="bi bi-tools me-2"></i>{% translate "Intervention" %} {{ insp.related_intervention.code }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -208,6 +219,19 @@
|
|||
<span class="text-muted"><i class="bi bi-layers me-1"></i>{% translate "Type d'asset" %}</span>
|
||||
<span class="badge bg-secondary">{{ insp.asset_content_type.name }}</span>
|
||||
</li>
|
||||
{% if insp.related_intervention %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-tools me-1"></i>{% translate "Intervention liée" %}</span>
|
||||
<span class="fw-semibold text-end ms-2">
|
||||
<a href="{% if is_mobile %}{{ insp.mobile_related_intervention_url }}{% else %}{{ insp.related_intervention_url }}{% endif %}" class="text-decoration-none fw-bold">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>{{ insp.related_intervention.code }}
|
||||
</a>
|
||||
{% if insp.related_intervention.sub_type %}
|
||||
<span class="badge bg-light text-dark border ms-1">{{ insp.related_intervention.sub_type.get_name }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-calendar-event me-1"></i>{% translate "Date & Heure" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
||||
|
|
@ -222,7 +246,7 @@
|
|||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-person me-1"></i>{% translate "Inspecteur" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspector|short_name|default:insp.inspector.username|default:"-" }}</span>
|
||||
<span class="fw-semibold">{{ insp.inspector|short_name|default:"-" }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-clock-history me-1"></i>{% translate "Date de modification" %}</span>
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@
|
|||
</span>
|
||||
{% endif %}
|
||||
{% if insp.inspector %}
|
||||
<span><i class="bi bi-person me-1"></i>{{ insp.inspector|short_name|default:insp.inspector.username }}</span>
|
||||
<span><i class="bi bi-person me-1"></i>{{ insp.inspector|short_name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -253,12 +253,23 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Lien vers la fiche de l'asset -->
|
||||
{% if insp.asset %}
|
||||
<div class="mb-3">
|
||||
<a href="{% if is_mobile %}{{ insp.mobile_asset_url }}{% else %}{{ insp.asset.get_absolute_url }}{% endif %}" class="btn btn-primary w-100 fw-semibold">
|
||||
<i class="bi bi-box-arrow-up-right me-2"></i>{% translate "Consulter la fiche de l'asset" %}
|
||||
</a>
|
||||
<!-- Boutons d'action : asset et/ou intervention -->
|
||||
{% if insp.asset or insp.related_intervention %}
|
||||
<div class="row g-2 mb-3">
|
||||
{% if insp.asset %}
|
||||
<div class="col-12 {% if insp.related_intervention %}col-6{% endif %}">
|
||||
<a href="{% if is_mobile %}{{ insp.mobile_asset_url }}{% else %}{{ insp.asset.get_absolute_url }}{% endif %}" class="btn btn-primary w-100 fw-semibold">
|
||||
<i class="bi bi-box-arrow-up-right me-2"></i>{% translate "Fiche de l'asset" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if insp.related_intervention %}
|
||||
<div class="col-12 {% if insp.asset %}col-6{% endif %}">
|
||||
<a href="{% if is_mobile %}{{ insp.mobile_related_intervention_url }}{% else %}{{ insp.related_intervention_url }}{% endif %}" class="btn btn-outline-primary w-100 fw-semibold">
|
||||
<i class="bi bi-tools me-2"></i>{% translate "Intervention" %} {{ insp.related_intervention.code }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -271,6 +282,19 @@
|
|||
<span class="text-muted"><i class="bi bi-layers me-1"></i>{% translate "Type d'asset" %}</span>
|
||||
<span class="badge bg-secondary">{{ insp.asset_content_type.name }}</span>
|
||||
</li>
|
||||
{% if insp.related_intervention %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-tools me-1"></i>{% translate "Intervention liée" %}</span>
|
||||
<span class="fw-semibold text-end ms-2">
|
||||
<a href="{% if is_mobile %}{{ insp.mobile_related_intervention_url }}{% else %}{{ insp.related_intervention_url }}{% endif %}" class="text-decoration-none fw-bold">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>{{ insp.related_intervention.code }}
|
||||
</a>
|
||||
{% if insp.related_intervention.sub_type %}
|
||||
<span class="badge bg-light text-dark border ms-1">{{ insp.related_intervention.sub_type.get_name }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if insp.is_scheduled %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-hourglass-split me-1"></i>{% translate "Statut" %}</span>
|
||||
|
|
@ -310,7 +334,7 @@
|
|||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-person me-1"></i>{% translate "Inspecteur" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspector|short_name|default:insp.inspector.username|default:"-" }}</span>
|
||||
<span class="fw-semibold">{{ insp.inspector|short_name|default:"-" }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-clock-history me-1"></i>{% translate "Date de modification" %}</span>
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}
|
||||
<div id="section-controles" class="card mb-4 {% if intervention.status == 'in_preparation' %} d-none {% endif %}">
|
||||
{% load i18n short_name %}
|
||||
<div id="section-controles" class="card mb-4 {% if intervention.status == 'in_preparation' and not visible_controls and not visible_inspections %} d-none {% endif %}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4>{% translate "Contrôles / Rapports de suivi" %}</h4>
|
||||
{% if 'follow_up_reports' in allowed_update_fields %}
|
||||
|
|
@ -43,7 +43,11 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class="table">
|
||||
{% if visible_controls %}
|
||||
<h6 class="fw-bold text-secondary mb-2">
|
||||
<i class="bi bi-file-earmark-text text-primary me-1"></i>{% translate "Rapports de contrôle" %}
|
||||
</h6>
|
||||
<table class="table mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Date" %}</th>
|
||||
|
|
@ -63,9 +67,188 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% elif not visible_inspections %}
|
||||
<p class="text-muted small mb-0">{% translate "Aucun contrôle ou rapport pour le moment." %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if visible_inspections %}
|
||||
<div class="{% if visible_controls %}mt-4 pt-3 border-top{% endif %}">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="fw-bold text-secondary mb-0">
|
||||
<i class="bi bi-patch-check text-primary me-1"></i>{% translate "Inspections de terrain / conformité" %}
|
||||
<span class="badge bg-secondary-subtle text-secondary rounded-pill ms-1">{{ visible_inspections|length }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{% translate "Inspection" %}</th>
|
||||
<th>{% translate "Date / Échéance" %}</th>
|
||||
<th>{% translate "Nature" %}</th>
|
||||
<th>{% translate "Cible" %}</th>
|
||||
<th>{% translate "Inspecteur" %}</th>
|
||||
<th>{% translate "Statut / Résultat" %}</th>
|
||||
<th class="text-end">{% translate "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for insp in visible_inspections %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'inspections:inspections_list' %}?inspection_id={{ insp.id }}" class="fw-bold text-decoration-none">
|
||||
<i class="bi bi-patch-check me-1 text-primary"></i>#{{ insp.id }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-semibold">{{ insp.inspection_date|date:"d/m/Y H:i"|default:"-" }}</span>
|
||||
{% if insp.is_scheduled and insp.scheduled_start_date %}
|
||||
<div class="small text-muted">
|
||||
{% translate "Prévue du" %} {{ insp.scheduled_start_date|date:"d/m/Y" }} {% translate "au" %} {{ insp.scheduled_due_date|date:"d/m/Y" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark border">{{ insp.get_nature_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if insp.asset and insp.asset.get_absolute_url %}
|
||||
<a href="{{ insp.asset.get_absolute_url }}" class="text-decoration-none" title="{% translate "Consulter l'asset" %}">
|
||||
<i class="bi bi-geo-alt me-1"></i>{{ insp.target_display }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span><i class="bi bi-geo-alt me-1"></i>{{ insp.target_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<i class="bi bi-person me-1 text-muted"></i>{{ insp.inspector|short_name|default:"-" }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ insp.status_badge_class }}">
|
||||
<i class="bi {{ insp.status_icon }} me-1"></i>{{ insp.get_result_status_display }}
|
||||
</span>
|
||||
{% if insp.is_scheduled %}
|
||||
<span class="badge bg-info text-dark ms-1">{% translate "Planifiée" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-xs btn-outline-primary py-1 px-2 me-1" data-bs-toggle="modal" data-bs-target="#interventionInspectionDetailModal{{ insp.id }}" title="{% translate "Détail" %}">
|
||||
<i class="bi bi-eye me-1"></i>{% translate "Détail" %}
|
||||
</button>
|
||||
<a href="{% url 'inspections:inspections_list' %}?inspection_id={{ insp.id }}" class="btn btn-xs btn-outline-secondary py-1 px-2" title="{% translate "Consulter dans la carte des inspections" %}">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for insp in visible_inspections %}
|
||||
<!-- Modale de détail de l'inspection liée -->
|
||||
<div class="modal fade" id="interventionInspectionDetailModal{{ insp.id }}" tabindex="-1" aria-labelledby="interventionInspectionDetailModalLabel{{ insp.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-light py-2.5">
|
||||
<h5 class="modal-title fs-6 fw-bold" id="interventionInspectionDetailModalLabel{{ insp.id }}">
|
||||
<i class="bi bi-patch-check me-2 text-primary"></i>{% translate "Détail de l'inspection" %} #{{ insp.id }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-3">
|
||||
{% if insp.is_cancelled %}
|
||||
<div class="alert alert-secondary py-2 px-3 small d-flex align-items-center mb-3">
|
||||
<i class="bi bi-slash-circle me-2 fs-6"></i>
|
||||
<div>
|
||||
<strong>{% translate "Inspection annulée" %}</strong> — {% translate "Cette inspection a été annulée et n'est plus prise en compte dans la conformité active de la cible." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if insp.asset and insp.asset.get_absolute_url %}
|
||||
<div class="mb-3">
|
||||
<a href="{{ insp.asset.get_absolute_url }}" class="btn btn-primary w-100 fw-semibold">
|
||||
<i class="bi bi-box-arrow-up-right me-2"></i>{% translate "Consulter la fiche de l'asset" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<ul class="list-group list-group-flush mb-3">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-geo-alt me-1"></i>{% translate "Cible" %}</span>
|
||||
<span class="fw-bold text-end ms-2">{{ insp.target_display }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-layers me-1"></i>{% translate "Type d'asset" %}</span>
|
||||
<span class="badge bg-secondary">{{ insp.asset_content_type.name }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-calendar-event me-1"></i>{% translate "Date & Heure" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspection_date|date:"d/m/Y H:i"|default:"-" }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-shield-check me-1"></i>{% translate "Résultat du contrôle" %}</span>
|
||||
<span class="badge {{ insp.status_badge_class }}"><i class="bi {{ insp.status_icon }} me-1"></i>{{ insp.get_result_status_display }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-eye me-1"></i>{% translate "Nature" %}</span>
|
||||
<span class="badge bg-light text-dark border">{{ insp.get_nature_display }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-person me-1"></i>{% translate "Inspecteur" %}</span>
|
||||
<span class="fw-semibold">{{ insp.inspector|short_name|default:"-" }}</span>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span class="text-muted"><i class="bi bi-clock-history me-1"></i>{% translate "Date de modification" %}</span>
|
||||
<span class="fw-semibold">{{ insp.updated_at|date:"d/m/Y H:i"|default:"-" }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if insp.notes %}
|
||||
<div class="card bg-light border-0 p-3 mb-3">
|
||||
<h6 class="fw-bold text-secondary mb-2"><i class="bi bi-journal-text me-2"></i>{% translate "Remarques" %}</h6>
|
||||
<div class="text-dark small">{{ insp.notes }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if insp.documents.all %}
|
||||
<div class="card bg-light border-0 p-3 mb-3">
|
||||
<h6 class="fw-bold text-secondary mb-2">
|
||||
<i class="bi bi-images me-2 text-primary"></i>{% translate "Photos & Documents joints" %} ({{ insp.documents.count }})
|
||||
</h6>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
{% for doc in insp.documents.all %}
|
||||
{% if doc.is_image %}
|
||||
<a href="{{ doc.file_url }}" target="_blank" class="d-inline-block border rounded p-1 bg-white" title="{{ doc.filename }}">
|
||||
<img src="{{ doc.file_url }}" alt="{{ doc.filename }}" class="rounded" style="width: 75px; height: 75px; object-fit: cover;">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ doc.file_url }}" target="_blank" class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center gap-1" title="{{ doc.filename }}">
|
||||
<i class="bi bi-file-earmark-arrow-down text-primary"></i>
|
||||
<span class="text-truncate" style="max-width: 160px;">{{ doc.filename }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer bg-light py-2">
|
||||
<a href="{% url 'inspections:inspections_list' %}?inspection_id={{ insp.id }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>{% translate "Ouvrir dans Inspections" %}
|
||||
</a>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1398,9 +1398,42 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{% elif not visible_inspections %}
|
||||
<p class="text-muted mb-0 small">{% translate "Aucun contrôle pour le moment." %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if visible_inspections %}
|
||||
<div class="{% if visible_controls %}mt-3 pt-3 border-top{% endif %}">
|
||||
<div class="fw-semibold text-secondary mb-2 small text-uppercase">
|
||||
<i class="bi bi-patch-check text-primary me-1"></i>{% translate "Inspections de terrain" %} ({{ visible_inspections|length }})
|
||||
</div>
|
||||
{% for insp in visible_inspections %}
|
||||
<div class="card mb-2 bg-light border p-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<a href="{% url 'inspections:inspections_list' %}?inspection_id={{ insp.id }}" class="fw-bold small text-decoration-none">
|
||||
<i class="bi bi-patch-check me-1 text-primary"></i>{% translate "Inspection" %} #{{ insp.id }}
|
||||
</a>
|
||||
<span class="badge {{ insp.status_badge_class }}">
|
||||
{{ insp.get_result_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-muted mb-1">
|
||||
<span class="badge bg-light text-dark border me-1">{{ insp.get_nature_display }}</span>
|
||||
{% if insp.is_scheduled %}
|
||||
<span class="badge bg-info text-dark">{% translate "Planifiée" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="small text-muted mb-1">
|
||||
<i class="bi bi-geo-alt me-1"></i>{{ insp.target_display }}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center small text-muted">
|
||||
<span><i class="bi bi-calendar-event me-1"></i>{{ insp.inspection_date|date:"d/m/Y H:i"|default:"-" }}</span>
|
||||
<span><i class="bi bi-person me-1"></i>{{ insp.inspector|short_name|default:"-" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()],
|
||||
|
|
|
|||
Loading…
Reference in a new issue