diff --git a/streetup/assets/migrations/0012_assetinspection_inspector_role_and_more.py b/streetup/assets/migrations/0012_assetinspection_inspector_role_and_more.py new file mode 100644 index 0000000..49a2d42 --- /dev/null +++ b/streetup/assets/migrations/0012_assetinspection_inspector_role_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.7 on 2026-08-07 10:27 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0011_artworkfountain_funding_program_and_more'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.AddField( + model_name='assetinspection', + name='inspector_role', + field=models.CharField(choices=[('observer', 'Observer / Agent de terrain'), ('provider', 'Prestataire technique externe'), ('manager', 'Gestionnaire / Contrôleur interne')], default='observer', max_length=50, verbose_name="Rôle de l'inspecteur"), + ), + migrations.AddField( + model_name='assetinspection', + name='nature', + field=models.CharField(choices=[('visual', 'Inspection visuelle'), ('technical', 'Inspection technique'), ('post_intervention', 'Suivi / Qualité post-intervention')], default='visual', max_length=50, verbose_name="Nature de l'inspection"), + ), + migrations.AlterField( + model_name='assetinspection', + name='asset_content_type', + field=models.ForeignKey(limit_choices_to={'app_label__in': ['assets', 'interventions']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset / Cible"), + ), + migrations.AlterField( + model_name='assetinspection', + name='asset_object_id', + field=models.PositiveIntegerField(verbose_name='ID de la cible'), + ), + ] diff --git a/streetup/assets/models/core.py b/streetup/assets/models/core.py index 67b33b5..ddbe470 100644 --- a/streetup/assets/models/core.py +++ b/streetup/assets/models/core.py @@ -182,6 +182,10 @@ class AssetCategory(models.Model): lang = get_language() return getattr(self, f"name_{lang}", self.name_fr) # Fallback to name_fr + @property + def name(self): + return self.get_name() + def __str__(self): return f"{self.get_name()}" diff --git a/streetup/assets/models/inspection.py b/streetup/assets/models/inspection.py index 5fbe2d3..58fbdfb 100644 --- a/streetup/assets/models/inspection.py +++ b/streetup/assets/models/inspection.py @@ -13,21 +13,45 @@ INSPECTION_RESULT_CHOICES = ( ('missing', _('Absent / Retiré')), ) +INSPECTION_NATURE_CHOICES = ( + ('visual', _('Inspection visuelle')), + ('technical', _('Inspection technique')), + ('post_intervention', _('Suivi / Qualité post-intervention')), +) + +INSPECTOR_ROLE_CHOICES = ( + ('observer', _('Observer / Agent de terrain')), + ('provider', _('Prestataire technique externe')), + ('manager', _('Gestionnaire / Contrôleur interne')), +) + class AssetInspection(models.Model): """ - Modèle d'enregistrement générique d'une vérification/inspection sur le terrain d'un asset. - Contient uniquement les données communes à tous les assets. + Modèle d'enregistrement générique d'une vérification/inspection sur le terrain d'une cible (Asset, Localisation, Intervention). """ asset_content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, - verbose_name=_("Type d'asset"), - limit_choices_to={'app_label': 'assets'} + verbose_name=_("Type d'asset / Cible"), + limit_choices_to={'app_label__in': ['assets', 'interventions']} ) - asset_object_id = models.PositiveIntegerField(verbose_name=_("ID de l'asset")) + asset_object_id = models.PositiveIntegerField(verbose_name=_("ID de la cible")) asset = GenericForeignKey('asset_content_type', 'asset_object_id') + nature = models.CharField( + max_length=50, + choices=INSPECTION_NATURE_CHOICES, + default='visual', + verbose_name=_("Nature de l'inspection") + ) + inspector_role = models.CharField( + max_length=50, + choices=INSPECTOR_ROLE_CHOICES, + default='observer', + verbose_name=_("Rôle de l'inspecteur") + ) + inspector = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, @@ -103,6 +127,23 @@ class AssetInspection(models.Model): if modified: target_asset.save() + @property + def status_badge_class(self): + mapping = { + 'compliant': 'bg-success', + 'non_compliant': 'bg-warning text-dark', + 'damaged': 'bg-danger', + 'missing': 'bg-dark', + 'updated': 'bg-info text-dark', + } + return mapping.get(self.result_status, 'bg-secondary') + + @property + def target_display(self): + if self.asset: + return str(self.asset) + return f"Cible #{self.asset_object_id}" + def __str__(self): asset_str = str(self.asset) if self.asset else f"Asset #{self.asset_object_id}" date_str = self.inspection_date.strftime('%d/%m/%Y') if self.inspection_date else '' diff --git a/streetup/assets/static/assets/inspection_map.js b/streetup/assets/static/assets/inspection_map.js new file mode 100644 index 0000000..3b9fb1a --- /dev/null +++ b/streetup/assets/static/assets/inspection_map.js @@ -0,0 +1,589 @@ +/** + * Inspection Map JS - Module de carte d'inspection rapide optimisé MapLibre GL, GPS & Mobile + */ +(function() { + let map = null; + let userMarker = null; + let loadedAssets = []; + + const STORAGE_THEMATIC_KEY = 'streetup_insp_thematic'; + const STORAGE_CATEGORY_KEY = 'streetup_insp_category'; + + document.addEventListener('DOMContentLoaded', function() { + initMap(); + initFilterStorage(); + initFormHandler(); + setupGeolocation(); + }); + + function initMap() { + const mapContainer = document.getElementById('inspection-map'); + if (!mapContainer || typeof maplibregl === 'undefined') return; + if (mapContainer.dataset.mapInitialized === 'true') return; + mapContainer.dataset.mapInitialized = 'true'; + + // Centrage et style depuis les settings globaux (window.MAP_STYLE_LIGHT / window.DEFAULT_MAP_CENTER) + const defaultCenter = window.DEFAULT_MAP_CENTER || [4.3517, 50.8503]; + const defaultZoom = window.DEFAULT_MAP_ZOOM || 15; + const styleUrl = window.MAP_STYLE_LIGHT || 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'; + + map = new maplibregl.Map({ + container: 'inspection-map', + style: styleUrl, + center: defaultCenter, + zoom: defaultZoom, + minzoom: 0, + maxzoom: 24 + }); + + map.addControl(new maplibregl.NavigationControl({ showCompass: true }), 'top-right'); + + if (typeof LayerSwitcherControl !== 'undefined') { + const layerSwitcher = new LayerSwitcherControl({ + layers: [{ + id: 'ortho', + name: 'Orthophotos', + icon: 'satellite', + group: 'ortho', + beforeLayer: 'assets-polygons', + layer: { type: 'raster', paint: { 'raster-opacity': 1 } }, + sublayers: [ + { + id: 'urbis', + name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)', + default: true, + source: { + type: 'raster', + tiles: [ + window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}' + ], + tileSize: 256, + attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS', + }, + }, + { + id: 'vlaanderen', + name: 'Vlaanderen 2023', + source: { + type: 'raster', + tiles: ['https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'], + tileSize: 256, + attribution: '© Digitaal Vlaanderen - Orthofoto 2023', + }, + }, + ], + }], + }); + map.addControl(layerSwitcher, 'top-right'); + } + + if (typeof AddressSearchControl !== 'undefined') { + const autocompleteUrl = window.AUTOCOMPLETE_STREET_URL || '/common/autocomplete-street/'; + const searchCtrl = new AddressSearchControl({ + autocompleteUrl: autocompleteUrl, + placeholder: 'Rechercher une adresse...' + }); + map.addControl(searchCtrl, 'top-left'); + } + + map.on('load', function() { + // Source GeoJSON pour les assets + map.addSource('assets-source', { + type: 'geojson', + data: { type: 'FeatureCollection', features: [] } + }); + + // Couche Polygones (ex: ParkingSpot polygon) + map.addLayer({ + id: 'assets-polygons', + type: 'fill', + source: 'assets-source', + filter: ['==', '$type', 'Polygon'], + paint: { + 'fill-color': '#0d6efd', + 'fill-opacity': 0.35, + 'fill-outline-color': '#0d6efd' + } + }); + + // Couche Lignes pour le contour des polygones + map.addLayer({ + id: 'assets-lines', + type: 'line', + source: 'assets-source', + filter: ['==', '$type', 'Polygon'], + paint: { + 'line-color': '#0d6efd', + 'line-width': 2 + } + }); + + // Couche Points (ex: Poteaux, Armoires) + map.addLayer({ + id: 'assets-points', + type: 'circle', + source: 'assets-source', + filter: ['==', '$type', 'Point'], + paint: { + 'circle-radius': 8, + 'circle-color': '#0d6efd', + 'circle-stroke-width': 2, + 'circle-stroke-color': '#ffffff' + } + }); + + // Clic sur un asset + map.on('click', ['assets-polygons', 'assets-points'], function(e) { + if (e.features && e.features.length > 0) { + const feature = e.features[0]; + handleAssetClick(feature); + } + }); + + // Survol curseur + map.on('mouseenter', ['assets-polygons', 'assets-points'], function() { + map.getCanvas().style.cursor = 'pointer'; + }); + map.on('mouseleave', ['assets-polygons', 'assets-points'], function() { + map.getCanvas().style.cursor = ''; + }); + + // Charger les assets au premier affichage + loadAssets(); + setupOverlayResizeObserver(); + }); + + map.on('moveend', onMapMoveEnd); + + setTimeout(function() { + if (map) map.resize(); + adjustTopControlsMargin(); + }, 300); + + window.addEventListener('resize', function() { + if (map) map.resize(); + adjustTopControlsMargin(); + }); + } + + function adjustTopControlsMargin() { + const overlay = document.querySelector('.map-controls-overlay'); + if (!overlay) return; + + const overlayHeight = overlay.offsetHeight || 0; + const overlayTop = overlay.offsetTop || 6; + const marginValue = (overlayTop + overlayHeight + 8) + 'px'; + + const leftCtrl = document.querySelector('#inspection-map .maplibregl-ctrl-top-left'); + const rightCtrl = document.querySelector('#inspection-map .maplibregl-ctrl-top-right'); + + if (leftCtrl) leftCtrl.style.marginTop = marginValue; + if (rightCtrl) rightCtrl.style.marginTop = marginValue; + } + + function setupOverlayResizeObserver() { + const overlay = document.querySelector('.map-controls-overlay'); + if (!overlay) return; + + adjustTopControlsMargin(); + + if (window.ResizeObserver) { + const observer = new ResizeObserver(function() { + adjustTopControlsMargin(); + }); + observer.observe(overlay); + } else { + window.addEventListener('resize', adjustTopControlsMargin); + } + } + + function getMapBbox() { + if (!map || typeof map.getBounds !== 'function') return null; + const b = map.getBounds(); + const sw = b.getSouthWest(), ne = b.getNorthEast(); + return { + type: 'Polygon', + coordinates: [[ + [sw.lng, sw.lat], [ne.lng, sw.lat], + [ne.lng, ne.lat], [sw.lng, ne.lat], + [sw.lng, sw.lat], + ]], + }; + } + + let moveendTimeout = null; + function onMapMoveEnd() { + if (moveendTimeout) clearTimeout(moveendTimeout); + moveendTimeout = setTimeout(function() { + loadAssets(); + }, 400); + } + + function setupGeolocation() { + const gpsBtn = document.getElementById('btn-gps-locate'); + if (gpsBtn) { + gpsBtn.addEventListener('click', locateUser); + } + + locateUser(); + } + + function locateUser() { + if (!navigator.geolocation) return; + + const statusText = document.getElementById('gps-status-text'); + if (statusText) statusText.textContent = 'Géolocalisation...'; + + navigator.geolocation.getCurrentPosition( + function(position) { + const lng = position.coords.longitude; + const lat = position.coords.latitude; + + if (typeof maplibregl !== 'undefined' && maplibregl.Marker && map) { + if (userMarker) { + userMarker.setLngLat([lng, lat]); + } else { + const el = document.createElement('div'); + el.className = 'user-gps-dot'; + + userMarker = new maplibregl.Marker({ element: el }) + .setLngLat([lng, lat]) + .addTo(map); + } + + map.flyTo({ center: [lng, lat], zoom: 17 }); + } + if (statusText) statusText.textContent = 'Position actualisée'; + }, + function(err) { + console.warn('GPS error:', err.message); + if (statusText) statusText.textContent = 'GPS indisponible'; + }, + { enableHighAccuracy: true, timeout: 10000, maximumAge: 5000 } + ); + } + + function updateUIElements(thematicCode, categoryId) { + const thematicSelect = document.getElementById('filter-thematic'); + if (thematicSelect) { + thematicSelect.value = thematicCode || ''; + } + + const thematicTiles = document.querySelectorAll('.thematic-tile'); + let selectedIcon = 'bi-folder2'; + let selectedThematicName = 'Thématique'; + + thematicTiles.forEach(tile => { + const code = tile.getAttribute('data-code'); + const icon = tile.getAttribute('data-icon'); + const name = tile.getAttribute('data-name'); + const check = tile.querySelector('.thematic-check'); + + if (code === thematicCode) { + tile.classList.add('active'); + if (check) check.classList.remove('d-none'); + selectedIcon = icon || selectedIcon; + selectedThematicName = name || selectedThematicName; + } else { + tile.classList.remove('active'); + if (check) check.classList.add('d-none'); + } + }); + + const iconDisp = document.getElementById('thematic-icon-display'); + const nameDisp = document.getElementById('thematic-name-display'); + if (iconDisp) iconDisp.className = `bi ${selectedIcon} text-primary`; + if (nameDisp) nameDisp.textContent = selectedThematicName; + + const categorySelect = document.getElementById('filter-category'); + if (categorySelect) { + categorySelect.value = categoryId || ''; + } + + const categoryItems = document.querySelectorAll('.category-item'); + let selectedCategoryName = 'Toutes les catégories'; + + categoryItems.forEach(item => { + const id = item.getAttribute('data-id'); + const itemThematic = item.getAttribute('data-thematic'); + const name = item.getAttribute('data-name'); + const check = item.querySelector('.category-check'); + + if (!id) { + item.classList.remove('d-none'); + } else if (!thematicCode || itemThematic === thematicCode) { + item.classList.remove('d-none'); + } else { + item.classList.add('d-none'); + } + + if ((id || '') === (categoryId || '')) { + item.classList.add('active'); + if (check) check.classList.remove('d-none'); + selectedCategoryName = name || selectedCategoryName; + } else { + item.classList.remove('active'); + if (check) check.classList.add('d-none'); + } + }); + + const catNameDisp = document.getElementById('category-name-display'); + if (catNameDisp) catNameDisp.textContent = selectedCategoryName; + } + + function initFilterStorage() { + const savedThematic = localStorage.getItem(STORAGE_THEMATIC_KEY) || 'parking'; + const savedCategory = localStorage.getItem(STORAGE_CATEGORY_KEY) || ''; + + updateUIElements(savedThematic, savedCategory); + + const thematicTiles = document.querySelectorAll('.thematic-tile'); + thematicTiles.forEach(tile => { + tile.addEventListener('click', function() { + const code = this.getAttribute('data-code'); + localStorage.setItem(STORAGE_THEMATIC_KEY, code); + localStorage.removeItem(STORAGE_CATEGORY_KEY); + + updateUIElements(code, ''); + loadAssets(); + }); + }); + + const categoryItems = document.querySelectorAll('.category-item'); + categoryItems.forEach(item => { + item.addEventListener('click', function() { + const id = this.getAttribute('data-id'); + const thematicSelect = document.getElementById('filter-thematic'); + const currentThematic = thematicSelect ? thematicSelect.value : ''; + + localStorage.setItem(STORAGE_CATEGORY_KEY, id || ''); + + updateUIElements(currentThematic, id || ''); + loadAssets(); + }); + }); + + const thematicSelect = document.getElementById('filter-thematic'); + if (thematicSelect) { + thematicSelect.addEventListener('change', function() { + localStorage.setItem(STORAGE_THEMATIC_KEY, thematicSelect.value); + updateUIElements(thematicSelect.value, ''); + localStorage.removeItem(STORAGE_CATEGORY_KEY); + loadAssets(); + }); + } + + const categorySelect = document.getElementById('filter-category'); + if (categorySelect) { + categorySelect.addEventListener('change', function() { + const thematic = thematicSelect ? thematicSelect.value : ''; + localStorage.setItem(STORAGE_CATEGORY_KEY, categorySelect.value); + updateUIElements(thematic, categorySelect.value); + loadAssets(); + }); + } + } + + const MIN_ZOOM_FOR_GEOJSON = 16; + + function loadAssets() { + if (!map) return; + + const warningAlert = document.getElementById('map-warning-alert'); + const currentZoom = (typeof map.getZoom === 'function') ? map.getZoom() : 0; + + if (currentZoom < MIN_ZOOM_FOR_GEOJSON) { + if (warningAlert) warningAlert.classList.remove('d-none'); + const source = map.getSource('assets-source'); + if (source) { + source.setData({ type: 'FeatureCollection', features: [] }); + } + loadedAssets = []; + return; + } + + const thematicSelect = document.getElementById('filter-thematic'); + const thematic = thematicSelect ? thematicSelect.value : ''; + + const categorySelect = document.getElementById('filter-category'); + const categoryId = categorySelect ? categorySelect.value : ''; + + let fetchUrl = '/assets/parking/assets/geojson/'; + if (thematic === 'parking') { + fetchUrl = '/assets/parking/assets/geojson/'; + } else if (thematic === 'trafficlights') { + fetchUrl = '/assets/trafficlights/assets/geojson/'; + } else if (thematic === 'publiclighting') { + fetchUrl = '/assets/publiclighting/assets/geojson/'; + } + + const payload = {}; + if (categoryId) { + payload.category = [categoryId]; + } + + const bbox = getMapBbox(); + if (bbox) { + payload.filter_geom = { type: 'Feature', geometry: bbox }; + } + + fetch(fetchUrl, { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken() + } + }) + .then(res => { + if (res.status === 413) { + if (warningAlert) warningAlert.classList.remove('d-none'); + return null; + } + if (!res.ok) { + if (warningAlert) warningAlert.classList.remove('d-none'); + return null; + } + return res.json(); + }) + .then(data => { + if (!data) return; + + if (data.error) { + if (warningAlert) warningAlert.classList.remove('d-none'); + return; + } + + if (warningAlert) warningAlert.classList.add('d-none'); + + if (data.features) { + loadedAssets = data.features; + const source = map.getSource('assets-source'); + if (source) { + source.setData(data); + } + } + }) + .catch(err => { + console.error('Error loading asset GeoJSON:', err); + if (warningAlert) warningAlert.classList.remove('d-none'); + }); + } + + function fitAllAssets() { + if (!loadedAssets || loadedAssets.length === 0 || !map) return; + + const bounds = new maplibregl.LngLatBounds(); + loadedAssets.forEach(f => { + if (f.geometry) { + if (f.geometry.type === 'Point') { + bounds.extend(f.geometry.coordinates); + } else if (f.geometry.type === 'Polygon') { + f.geometry.coordinates[0].forEach(coord => bounds.extend(coord)); + } else if (f.geometry.type === 'MultiPolygon') { + f.geometry.coordinates.forEach(poly => poly[0].forEach(coord => bounds.extend(coord))); + } + } + }); + + if (!bounds.isEmpty()) { + map.fitBounds(bounds, { padding: 40 }); + } + } + + function handleAssetClick(feature) { + const props = feature.properties || {}; + openInspectionModal(props); + } + + window.openInspectionModal = function(props) { + const modalEl = document.getElementById('quickInspectionModal'); + if (!modalEl) return; + + document.getElementById('insp_content_type_id').value = props.content_type_id || props.ct_id || 1; + document.getElementById('insp_object_id').value = props.id || props.pk; + + document.getElementById('insp_target_type').textContent = props.asset_type_display || props.model_name || 'Asset'; + document.getElementById('insp_target_code').textContent = props.code || ('#' + (props.id || props.pk)); + document.getElementById('insp_target_name').textContent = props.name || props.name_fr || '-'; + document.getElementById('insp_target_status').textContent = props.status_display || props.status || '-'; + + const parkingFields = document.getElementById('parkingSpotSpecificFields'); + if (parkingFields) { + if (props.model_name === 'parkingspot' || props.spot_type) { + parkingFields.classList.remove('d-none'); + document.getElementById('insp_std_stands').value = props.standard_stand_count || 0; + document.getElementById('insp_cargo_stands').value = props.cargo_stand_count || 0; + } else { + parkingFields.classList.add('d-none'); + } + } + + const modal = new bootstrap.Modal(modalEl); + modal.show(); + }; + + function initFormHandler() { + const form = document.getElementById('createInspectionForm') || document.getElementById('quickInspectionForm'); + if (!form) return; + + form.addEventListener('submit', function(e) { + e.preventDefault(); + const alertDiv = document.getElementById('quickInspectionAlert'); + const submitBtn = document.getElementById('submitQuickInspectionBtn'); + const spinner = document.getElementById('submitQuickInspectionSpinner'); + const icon = document.getElementById('submitQuickInspectionIcon'); + + alertDiv.classList.add('d-none'); + alertDiv.textContent = ''; + submitBtn.disabled = true; + if (spinner) spinner.classList.remove('d-none'); + if (icon) icon.classList.add('d-none'); + + const formData = new FormData(form); + + fetch(form.action, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + const modalEl = document.getElementById('quickInspectionModal'); + const modal = bootstrap.Modal.getInstance(modalEl); + if (modal) modal.hide(); + + let msg = data.message || 'Inspection enregistrée !'; + if (data.created_intervention_code) { + msg += ' Intervention ' + data.created_intervention_code + ' créée.'; + } + alert(msg); + loadAssets(); + } else { + alertDiv.textContent = data.error || 'Erreur lors de la validation.'; + alertDiv.classList.remove('d-none'); + } + }) + .catch(err => { + alertDiv.textContent = 'Erreur réseau.'; + alertDiv.classList.remove('d-none'); + }) + .finally(() => { + submitBtn.disabled = false; + if (spinner) spinner.classList.add('d-none'); + if (icon) icon.classList.remove('d-none'); + }); + }); + } + + function getCsrfToken() { + const cookieValue = document.cookie + .split('; ') + .find(row => row.startsWith('csrftoken=')) + ?.split('=')[1]; + return cookieValue || ''; + } +})(); diff --git a/streetup/assets/templates/assets/components/inspection_form_modal.html b/streetup/assets/templates/assets/components/inspection_form_modal.html new file mode 100644 index 0000000..29155d5 --- /dev/null +++ b/streetup/assets/templates/assets/components/inspection_form_modal.html @@ -0,0 +1,154 @@ +{% load i18n %} + + +
+ + + diff --git a/streetup/assets/templates/assets/inspections_list.html b/streetup/assets/templates/assets/inspections_list.html new file mode 100644 index 0000000..ac88198 --- /dev/null +++ b/streetup/assets/templates/assets/inspections_list.html @@ -0,0 +1,146 @@ +{% extends base_template|default:"base.html" %} +{% load i18n %} + +{% block title %}{% translate "Inspections d'assets" %}{% endblock %} + +{% block content %} +{% translate "Suivi et historique des contrôles et vérifications sur le terrain" %}
+| {% translate "Date" %} | +{% translate "Cible" %} | +{% translate "Inspecteur" %} | +{% translate "Résultat" %} | +{% translate "Nature" %} | +{% translate "Remarques" %} | +
|---|---|---|---|---|---|
| + {{ insp.inspection_date|date:"d/m/Y H:i" }} + | +
+ {% if insp.asset and insp.asset.get_absolute_url %}
+
+ {{ insp.target_display }}
+
+ {% else %}
+ {{ insp.target_display }}
+ {% endif %}
+ {{ insp.asset_content_type.name }} + |
+ + {% if insp.inspector %} + {{ insp.inspector.get_full_name|default:insp.inspector.username }} + {% else %} + - + {% endif %} + | ++ + {{ insp.get_result_status_display }} + + | ++ + {{ insp.get_nature_display }} + + | ++ {{ insp.notes|default:"-" }} + | +