diff --git a/loko/assets/templates/assets/components/asset_inspections.html b/loko/assets/templates/assets/components/asset_inspections.html index 4dba7e2..679aef4 100644 --- a/loko/assets/templates/assets/components/asset_inspections.html +++ b/loko/assets/templates/assets/components/asset_inspections.html @@ -153,7 +153,7 @@ {% endif %} - {% if insp.notes %} + {% if insp.notes and not insp.is_giep %}
@@ -162,7 +162,7 @@
{% endif %} - {% if insp.documents.all %} + {% if insp.documents.all and not insp.is_giep %}
diff --git a/loko/inspections/models.py b/loko/inspections/models.py index 0dba912..5e808a1 100644 --- a/loko/inspections/models.py +++ b/loko/inspections/models.py @@ -245,6 +245,14 @@ class Inspection(models.Model): def is_scheduled(self): return self.status == 'scheduled' + @property + def is_giep(self): + return ( + (self.asset_content_type and self.asset_content_type.model in ('naturerwiz', 'giepinspection')) or + hasattr(self, 'giepinspection') or + isinstance(self, GiepInspection) + ) + @property def timeliness(self): """ @@ -791,6 +799,149 @@ class GiepInspection(Inspection): verbose_name = _("Inspection GIEP / Eaux pluviales") verbose_name_plural = _("Inspections GIEP / Eaux pluviales") + @property + def is_giep(self): + return True + + @classmethod + def compute_status_for_single_ouvrage(cls, ouv): + """ + Calcule le résultat ('damaged', 'non_compliant', 'compliant') + sur base des réponses aux questions d'un ouvrage GIEP. + Priorité : damaged > non_compliant > compliant. + """ + if not ouv or not isinstance(ouv, dict): + return 'compliant' + explicit_status = ouv.get('result_status') + if explicit_status in ('damaged', 'non_compliant', 'compliant') and ouv.get('is_result_manual'): + return explicit_status + + device_type = (ouv.get('device_type') or 'evc').lower() + q = ouv.get('questions') or {} + if not isinstance(q, dict): + return explicit_status if explicit_status in ('damaged', 'non_compliant', 'compliant') else 'compliant' + + def is_val(key, *expected): + v = q.get(key) + if isinstance(v, bool): + v_str = 'true' if v else 'false' + elif v is None: + v_str = 'null' + else: + v_str = str(v).strip().lower() + return v_str in [str(e).strip().lower() for e in expected] + + has_damaged = False + has_non_compliant = False + + if device_type == 'evc': + # 1. Point d'entrée + if is_val('evc_pe_ok', 'false', 'non'): + has_non_compliant = True + + # 2. Empierrement + if is_val('evc_empierrement_present', True, 'true'): + if is_val('evc_empierrement_ok', 'false', 'non'): + has_non_compliant = True + if is_val('evc_empierrement_emporte', True, 'true'): + has_damaged = True + + # 3. Végétation + if is_val('evc_ev_veg_etat', 'mauvais'): + has_non_compliant = True + if is_val('evc_ev_plantes_mortes', 'plus_10', '+ de 10%'): + has_damaged = True + + # 4. Chenaux d'érosion + if is_val('evc_ev_chenaux_erosion', 'true', 'oui', True): + has_non_compliant = True + + # 5. Flaques d'eau + if is_val('evc_ev_flaques', 'true', 'oui', True): + has_non_compliant = True + + # 6. Sédiments + if is_val('evc_ev_sediments', 'true', 'oui', True): + has_non_compliant = True + if is_val('evc_ev_croute_sediments', 'true', 'oui', True): + has_damaged = True + + # 7. Espèces envahissantes + if is_val('evc_ev_envahissantes', 'true', 'oui', True): + has_non_compliant = True + + # 8. Surverse + if is_val('evc_surverse_presente', True, 'true'): + if is_val('evc_sv_grille_propre', 'false', 'non'): + has_non_compliant = True + if is_val('evc_sv_decantation_vide', 'false', 'non'): + has_non_compliant = True + if is_val('evc_sv_haute', 'false', 'non'): + has_damaged = True + + elif device_type == 'oed': + # 1. Revêtement général + if is_val('oed_revetement_ok', 'false', 'non'): + has_non_compliant = True + + # 2. Bouche d'injection + if is_val('oed_injection_presente', True, 'true'): + if is_val('oed_bi_niveaux_ok', 'false', 'non'): + has_damaged = True + if is_val('oed_bi_grille_propre', 'false', 'non'): + has_non_compliant = True + if is_val('oed_bi_decantation_vide', 'false', 'non'): + has_damaged = True + if is_val('oed_bi_drain_colmate', 'true', 'oui', True): + has_damaged = True + + # 3. Structure enherbée / minérale + if is_val('oed_rhm_presente', True, 'true'): + if is_val('oed_rhm_stagne', 'true', 'oui', True): + has_non_compliant = True + if is_val('oed_rhm_joints_ok', 'false', 'non'): + has_non_compliant = True + if is_val('oed_rhm_joints_fines', 'true', 'oui', True): + has_damaged = True + if is_val('oed_rhm_joints_remplissage', 'true', 'oui', True): + has_non_compliant = True + + # 4. Modulaire poreux / continu poreux + if is_val('oed_rp_presente', True, 'true'): + if is_val('oed_rp_stagne', 'true', 'oui', True): + has_non_compliant = True + if is_val('oed_rp_depots', 'true', 'oui', True): + has_non_compliant = True + + elif device_type == 'ta': + # 1. Point d'entrée + if is_val('ta_pe_ok', 'false', 'non'): + has_non_compliant = True + if is_val('ta_pe_conduits_ok', 'false', 'non'): + has_damaged = True + if is_val('ta_pe_decantation_vide', 'false', 'non'): + has_non_compliant = True + + # 2. Bassin d'orage + if is_val('ta_bo_vegetation_etat', 'mauvais'): + has_non_compliant = True + if is_val('ta_bo_plantes_mortes', 'plus_10', '+ de 10%'): + has_damaged = True + if is_val('ta_bo_chenaux', 'true', 'oui', True): + has_non_compliant = True + if is_val('ta_sediments', 'true', 'oui', True): + has_non_compliant = True + if is_val('ta_sediments_rempli', 'true', 'oui', True): + has_damaged = True + if is_val('ta_bo_envahissantes', 'true', 'oui', True): + has_non_compliant = True + + if has_damaged: + return 'damaged' + if has_non_compliant: + return 'non_compliant' + return explicit_status if explicit_status in ('damaged', 'non_compliant', 'compliant') else 'compliant' + @classmethod def compute_status_from_data(cls, data): """ @@ -810,122 +961,11 @@ class GiepInspection(Inspection): for ouv in ouvrages: if not isinstance(ouv, dict): continue - device_type = (ouv.get('device_type') or 'evc').lower() - q = ouv.get('questions') or {} - if not isinstance(q, dict): - continue - - def is_val(key, *expected): - v = q.get(key) - if isinstance(v, bool): - v_str = 'true' if v else 'false' - elif v is None: - v_str = 'null' - else: - v_str = str(v).strip().lower() - return v_str in [str(e).strip().lower() for e in expected] - - if device_type == 'evc': - # 1. Point d'entrée - if is_val('evc_pe_ok', 'false', 'non'): - has_non_compliant = True - - # 2. Empierrement - if is_val('evc_empierrement_present', True, 'true'): - if is_val('evc_empierrement_ok', 'false', 'non'): - has_non_compliant = True - if is_val('evc_empierrement_emporte', True, 'true'): - has_damaged = True - - # 3. Végétation - if is_val('evc_ev_veg_etat', 'mauvais'): - has_non_compliant = True - if is_val('evc_ev_plantes_mortes', 'plus_10', '+ de 10%'): - has_damaged = True - - # 4. Chenaux d'érosion - if is_val('evc_ev_chenaux_erosion', 'true', 'oui', True): - has_non_compliant = True - - # 5. Flaques d'eau - if is_val('evc_ev_flaques', 'true', 'oui', True): - has_non_compliant = True - - # 6. Sédiments - if is_val('evc_ev_sediments', 'true', 'oui', True): - has_non_compliant = True - if is_val('evc_ev_croute_sediments', 'true', 'oui', True): - has_damaged = True - - # 7. Espèces envahissantes - if is_val('evc_ev_envahissantes', 'true', 'oui', True): - has_non_compliant = True - - # 8. Surverse - if is_val('evc_surverse_presente', True, 'true'): - if is_val('evc_sv_grille_propre', 'false', 'non'): - has_non_compliant = True - if is_val('evc_sv_decantation_vide', 'false', 'non'): - has_non_compliant = True - if is_val('evc_sv_haute', 'false', 'non'): - has_damaged = True - - elif device_type == 'oed': - # 1. Revêtement général - if is_val('oed_revetement_ok', 'false', 'non'): - has_non_compliant = True - - # 2. Bouche d'injection - if is_val('oed_injection_presente', True, 'true'): - if is_val('oed_bi_niveaux_ok', 'false', 'non'): - has_damaged = True - if is_val('oed_bi_grille_propre', 'false', 'non'): - has_non_compliant = True - if is_val('oed_bi_decantation_vide', 'false', 'non'): - has_damaged = True - if is_val('oed_bi_drain_colmate', 'true', 'oui', True): - has_damaged = True - - # 3. Structure enherbée / minérale - if is_val('oed_rhm_presente', True, 'true'): - if is_val('oed_rhm_stagne', 'true', 'oui', True): - has_non_compliant = True - if is_val('oed_rhm_joints_ok', 'false', 'non'): - has_non_compliant = True - if is_val('oed_rhm_joints_fines', 'true', 'oui', True): - has_damaged = True - if is_val('oed_rhm_joints_remplissage', 'true', 'oui', True): - has_non_compliant = True - - # 4. Modulaire poreux / continu poreux - if is_val('oed_rp_presente', True, 'true'): - if is_val('oed_rp_stagne', 'true', 'oui', True): - has_non_compliant = True - if is_val('oed_rp_depots', 'true', 'oui', True): - has_non_compliant = True - - elif device_type == 'ta': - # 1. Point d'entrée - if is_val('ta_pe_ok', 'false', 'non'): - has_non_compliant = True - if is_val('ta_pe_conduits_ok', 'false', 'non'): - has_damaged = True - if is_val('ta_pe_decantation_vide', 'false', 'non'): - has_non_compliant = True - - # 2. Bassin d'orage - if is_val('ta_bo_vegetation_etat', 'mauvais'): - has_non_compliant = True - if is_val('ta_bo_plantes_mortes', 'plus_10', '+ de 10%'): - has_damaged = True - if is_val('ta_bo_chenaux', 'true', 'oui', True): - has_non_compliant = True - if is_val('ta_sediments', 'true', 'oui', True): - has_non_compliant = True - if is_val('ta_sediments_rempli', 'true', 'oui', True): - has_damaged = True - if is_val('ta_bo_envahissantes', 'true', 'oui', True): - has_non_compliant = True + st = cls.compute_status_for_single_ouvrage(ouv) + if st == 'damaged': + has_damaged = True + elif st == 'non_compliant': + has_non_compliant = True if has_damaged: return 'damaged' @@ -934,6 +974,7 @@ class GiepInspection(Inspection): return 'compliant' + class SignPanelInspection(Inspection): """ Spécialisation de l'inspection pour les panneaux de signalisation routière. diff --git a/loko/inspections/templates/inspections/components/asset_details/_naturerwiz.html b/loko/inspections/templates/inspections/components/asset_details/_naturerwiz.html index f58c8d0..0d4e1ea 100644 --- a/loko/inspections/templates/inspections/components/asset_details/_naturerwiz.html +++ b/loko/inspections/templates/inspections/components/asset_details/_naturerwiz.html @@ -73,11 +73,45 @@ {{ ouv.name }} {% endif %}
+ + +
+ + {{ ouv|ouvrage_result_label }} + +
+ + {% with ouv_docs=insp.documents.all|filter_ouvrage_docs:ouv %} + {% if ouv_docs %} +
+
+
+ {% translate "Photos de l'ouvrage" %} ({{ ouv_docs|length }}) +
+ +
+
+ {% endif %} + {% endwith %} + {% if ouv.device_type == 'evc' %} @@ -357,6 +391,17 @@
{% endif %} + + {% if ouv.notes %} +
+
+
+ {% translate "Remarques sur cet ouvrage" %} +
+

{{ ouv.notes }}

+
+
+ {% endif %}
@@ -366,6 +411,31 @@ {% endif %} + + + {% with gen_docs=insp.documents.all|filter_general_docs %} + {% if gen_docs %} +
+
+ {% translate "Photos générales de la zone" %} ({{ gen_docs|length }}) +
+
+ {% for doc in gen_docs %} + + {% if doc.is_image %} + {{ doc.filename }} + {% else %} +
+ + {{ doc.filename }} +
+ {% endif %} +
+ {% endfor %} +
+
+ {% endif %} + {% endwith %} {% endif %} {% endwith %} diff --git a/loko/inspections/templates/inspections/components/asset_fields/_naturerwiz.html b/loko/inspections/templates/inspections/components/asset_fields/_naturerwiz.html index bd9e85b..855952e 100644 --- a/loko/inspections/templates/inspections/components/asset_fields/_naturerwiz.html +++ b/loko/inspections/templates/inspections/components/asset_fields/_naturerwiz.html @@ -14,6 +14,7 @@ +
@@ -178,12 +179,17 @@ function createEmptyOuvrage(idx, deviceType = "evc", asset = null) { return { - local_id: String(idx), + local_id: 'ouv_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4), device_type: deviceType, name: asset ? (asset.code + (asset.structure_type ? ' (' + asset.structure_type + ')' : '')) : `Ouvrage #${idx + 1}`, asset_id: asset ? asset.id : null, asset_code: asset ? asset.code : null, asset_name: asset ? (asset.structure_type || asset.name || '') : null, + result_status: 'compliant', + is_result_manual: false, + notes: '', + newFiles: [], + existingDocs: [], questions: { // EVC evc_pe_ok: null, @@ -510,20 +516,105 @@ renderOuvrages(); } + function syncHiddenFileInputs() { + const hiddenContainer = rootModal.querySelector('#giep_hidden_file_inputs') || document.getElementById('giep_hidden_file_inputs'); + if (!hiddenContainer) return; + hiddenContainer.innerHTML = ''; + + giepOuvrages.forEach((ouv, idx) => { + const localId = ouv.local_id || String(idx); + (ouv.newFiles || []).forEach(file => { + try { + const dt = new DataTransfer(); + dt.items.add(file); + const fileInp = document.createElement('input'); + fileInp.type = 'file'; + fileInp.name = 'photos'; + fileInp.files = dt.files; + hiddenContainer.appendChild(fileInp); + + const keyInp = document.createElement('input'); + keyInp.type = 'hidden'; + keyInp.name = 'field_keys'; + keyInp.value = 'ouvrage_' + localId; + hiddenContainer.appendChild(keyInp); + } catch (err) { + console.warn('DataTransfer error:', err); + } + }); + }); + } + + function toggleParentGlobalSections(isGiep) { + const sectionsToToggle = [ + rootModal.querySelector('#quickInspResultSection'), + rootModal.querySelector('#editInspResultSection'), + rootModal.querySelector('#quickInspPhotosSection'), + rootModal.querySelector('#editInspPhotosSection'), + rootModal.querySelector('#quickInspNotesSection'), + rootModal.querySelector('#editInspNotesSection') + ]; + sectionsToToggle.forEach(sec => { + if (sec) { + if (isGiep) sec.classList.add('d-none'); + else sec.classList.remove('d-none'); + } + }); + + const editExistingDocs = rootModal.querySelector('#editInspectionExistingDocsContainer'); + if (editExistingDocs && isGiep) { + editExistingDocs.classList.add('d-none'); + } + } + function updateSerializedGiepData() { const dateRainEl = rootModal.querySelector('#giep_date_derniere_pluie, [name="date_derniere_pluie"]'); const stormEl = rootModal.querySelector('#giep_orage_moins_une_heure, [name="orage_moins_une_heure"]'); const pluieVal = dateRainEl?.value || 'null'; const orageVal = stormEl?.value || 'null'; + // Auto-calcul pour chaque ouvrage si non manuel + giepOuvrages.forEach(ouv => { + if (!ouv.is_result_manual) { + ouv.result_status = evaluateSingleOuvrageStatus(ouv); + } + }); + + // Consolidation des remarques par ouvrage dans le champ notes global de la modale + const notesEl = rootModal.querySelector('textarea[name="notes"]'); + if (notesEl) { + const consolidatedNotes = giepOuvrages + .map((o, i) => { + const title = o.asset_code || `Ouvrage #${i + 1}`; + return o.notes && o.notes.trim() ? `[${title}] : ${o.notes.trim()}` : ''; + }) + .filter(Boolean) + .join('\n\n'); + notesEl.value = consolidatedNotes; + } + const payload = { - ouvrages: giepOuvrages, + ouvrages: giepOuvrages.map((o, i) => ({ + local_id: o.local_id || String(i), + device_type: o.device_type, + name: o.name, + asset_id: o.asset_id, + asset_code: o.asset_code, + asset_name: o.asset_name, + result_status: o.result_status || 'compliant', + is_result_manual: Boolean(o.is_result_manual), + notes: o.notes || '', + questions: o.questions || {}, + actions: o.actions || {} + })), date_derniere_pluie: pluieVal, orage_moins_une_heure: orageVal === 'true' ? true : (orageVal === 'false' ? false : null) }; if (serializedInput) { serializedInput.value = JSON.stringify(payload); } + + syncHiddenFileInputs(); } // Liste des clés de questions où "Oui" représente une anomalie / dysfonctionnement @@ -623,6 +714,196 @@ return false; } + function evaluateSingleOuvrageStatus(ouv) { + if (!ouv) return 'compliant'; + if (ouv.is_result_manual && ouv.result_status) { + return ouv.result_status; + } + + const deviceType = (ouv.device_type || 'evc').toLowerCase(); + const q = ouv.questions || {}; + + const isVal = (key, ...expected) => { + const v = q[key]; + if (v === undefined || v === null) return expected.includes('null'); + const vStr = String(v).trim().toLowerCase(); + return expected.map(e => String(e).trim().toLowerCase()).includes(vStr); + }; + + let hasDamaged = false; + let hasNonCompliant = false; + + if (deviceType === 'evc') { + // 1. Point d'entrée + if (isVal('evc_pe_ok', 'false', 'non')) { + hasNonCompliant = true; + } + + // 2. Empierrement + if (isVal('evc_empierrement_present', true, 'true')) { + if (isVal('evc_empierrement_ok', 'false', 'non')) { + hasNonCompliant = true; + if (isVal('evc_empierrement_emporte', true, 'true')) { + hasDamaged = true; + } + } + } + + // 3. Végétation + if (isVal('evc_ev_veg_etat', 'mauvais')) { + hasNonCompliant = true; + if (isVal('evc_ev_plantes_mortes', 'plus_10', '+ de 10%')) { + hasDamaged = true; + } + } + + // 4. Chenaux d'érosion + if (isVal('evc_ev_chenaux_erosion', true, 'true', 'oui')) { + hasNonCompliant = true; + } + + // 5. Flaques + if (isVal('evc_ev_flaques', true, 'true', 'oui')) { + hasNonCompliant = true; + } + + // 6. Sédiments + if (isVal('evc_ev_sediments', true, 'true', 'oui')) { + hasNonCompliant = true; + if (isVal('evc_ev_croute_sediments', true, 'true', 'oui')) { + hasDamaged = true; + } + } + + // 7. Espèces envahissantes + if (isVal('evc_ev_envahissantes', true, 'true', 'oui')) { + hasNonCompliant = true; + } + + // 8. Surverse + if (isVal('evc_surverse_presente', true, 'true')) { + if (isVal('evc_sv_grille_propre', 'false', 'non')) { + hasNonCompliant = true; + } + if (isVal('evc_sv_decantation_vide', 'false', 'non')) { + hasNonCompliant = true; + } + if (isVal('evc_sv_haute', 'false', 'non')) { + hasDamaged = true; + } + } + + } else if (deviceType === 'oed') { + // 1. Revêtement + if (isVal('oed_revetement_ok', 'false', 'non')) { + hasNonCompliant = true; + if (isVal('oed_revetement_ornieres', true, 'true') || isVal('oed_revetement_fissures', true, 'true') || isVal('oed_revetement_trous', true, 'true')) { + hasDamaged = true; + } + } + + // 2. Bouche d'injection + if (isVal('oed_injection_presente', true, 'true')) { + if (isVal('oed_bi_niveaux_ok', 'false', 'non')) { + hasDamaged = true; + } + if (isVal('oed_bi_grille_propre', 'false', 'non')) { + hasNonCompliant = true; + } + if (isVal('oed_bi_decantation_vide', 'false', 'non')) { + hasDamaged = true; + } + if (isVal('oed_bi_drain_colmate', true, 'true', 'oui')) { + hasDamaged = true; + } + } + + // 3. Structure enherbée / minérale + if (isVal('oed_rhm_presente', true, 'true')) { + if (isVal('oed_rhm_stagne', true, 'true', 'oui')) { + hasNonCompliant = true; + if (isVal('oed_rhm_stagne_plus_2h', true, 'true', 'oui')) { + hasDamaged = true; + } + } + if (isVal('oed_rhm_joints_ok', 'false', 'non')) { + hasNonCompliant = true; + if (isVal('oed_rhm_joints_fines', true, 'true', 'oui')) { + hasDamaged = true; + } + if (isVal('oed_rhm_joints_remplissage', true, 'true', 'oui')) { + hasNonCompliant = true; + } + } + } + + // 4. Modulaire poreux / continu poreux + if (isVal('oed_rp_presente', true, 'true')) { + if (isVal('oed_rp_stagne', true, 'true', 'oui')) { + hasNonCompliant = true; + if (isVal('oed_rp_stagne_plus_2h', true, 'true', 'oui')) { + hasDamaged = true; + } + } + if (isVal('oed_rp_depots', true, 'true', 'oui')) { + hasNonCompliant = true; + } + } + + // 5. Matériaux non agrégés + if (isVal('oed_rna_presente', true, 'true')) { + if (isVal('oed_rna_stagne', true, 'true', 'oui')) { + hasNonCompliant = true; + if (isVal('oed_rna_stagne_plus_2h', true, 'true', 'oui')) { + hasDamaged = true; + } + } + if (isVal('oed_rna_matiere_ok', 'false', 'non')) { + hasNonCompliant = true; + } + if (isVal('oed_rna_geotextile_visible', true, 'true', 'oui')) { + hasDamaged = true; + } + } + + } else if (deviceType === 'ta') { + // 1. Point d'entrée + if (isVal('ta_pe_ok', 'false', 'non')) { + hasNonCompliant = true; + } + if (isVal('ta_pe_conduits_ok', 'false', 'non')) { + hasDamaged = true; + } + if (isVal('ta_pe_decantation_vide', 'false', 'non')) { + hasNonCompliant = true; + } + + // 2. Bassin d'orage + if (isVal('ta_bo_vegetation_etat', 'mauvais')) { + hasNonCompliant = true; + if (isVal('ta_bo_plantes_mortes', 'plus_10', '+ de 10%')) { + hasDamaged = true; + } + } + if (isVal('ta_bo_chenaux', true, 'true', 'oui')) { + hasNonCompliant = true; + } + if (isVal('ta_sediments', true, 'true', 'oui')) { + hasNonCompliant = true; + if (isVal('ta_sediments_rempli', true, 'true', 'oui')) { + hasDamaged = true; + } + } + if (isVal('ta_bo_envahissantes', true, 'true', 'oui')) { + hasNonCompliant = true; + } + } + + if (hasDamaged) return 'damaged'; + if (hasNonCompliant) return 'non_compliant'; + return 'compliant'; + } + function evaluateGiepInspectionStatus(ouvrages) { if (!Array.isArray(ouvrages) || ouvrages.length === 0) { return null; @@ -636,156 +917,9 @@ for (const ouv of ouvrages) { if (!ouv) continue; - const deviceType = (ouv.device_type || 'evc').toLowerCase(); - const q = ouv.questions || {}; - - const isVal = (key, ...expected) => { - const v = q[key]; - if (v === undefined || v === null) return expected.includes('null'); - const vStr = String(v).trim().toLowerCase(); - return expected.map(e => String(e).trim().toLowerCase()).includes(vStr); - }; - - if (deviceType === 'evc') { - // 1. Point d'entrée - if (isVal('evc_pe_ok', 'false', 'non')) { - hasNonCompliant = true; - } - - // 2. Empierrement - if (isVal('evc_empierrement_present', true, 'true')) { - if (isVal('evc_empierrement_ok', 'false', 'non')) { - hasNonCompliant = true; - if (isVal('evc_empierrement_emporte', true, 'true')) { - hasDamaged = true; - } - } - } - - // 3. Végétation - if (isVal('evc_ev_veg_etat', 'mauvais')) { - hasNonCompliant = true; - if (isVal('evc_ev_plantes_mortes', 'plus_10', '+ de 10%')) { - hasDamaged = true; - } - } - - // 4. Chenaux d'érosion - if (isVal('evc_ev_chenaux_erosion', true, 'true', 'oui')) { - hasNonCompliant = true; - } - - // 5. Flaques - if (isVal('evc_ev_flaques', true, 'true', 'oui')) { - hasNonCompliant = true; - } - - // 6. Sédiments - if (isVal('evc_ev_sediments', true, 'true', 'oui')) { - hasNonCompliant = true; - if (isVal('evc_ev_croute_sediments', true, 'true', 'oui')) { - hasDamaged = true; - } - } - - // 7. Espèces envahissantes - if (isVal('evc_ev_envahissantes', true, 'true', 'oui')) { - hasNonCompliant = true; - } - - // 8. Surverse - if (isVal('evc_surverse_presente', true, 'true')) { - if (isVal('evc_sv_grille_propre', 'false', 'non')) { - hasNonCompliant = true; - } - if (isVal('evc_sv_decantation_vide', 'false', 'non')) { - hasNonCompliant = true; - } - if (isVal('evc_sv_haute', 'false', 'non')) { - hasDamaged = true; - } - } - - } else if (deviceType === 'oed') { - // 1. Revêtement - if (isVal('oed_revetement_ok', 'false', 'non')) { - hasNonCompliant = true; - } - - // 2. Bouche d'injection - if (isVal('oed_injection_presente', true, 'true')) { - if (isVal('oed_bi_niveaux_ok', 'false', 'non')) { - hasDamaged = true; - } - if (isVal('oed_bi_grille_propre', 'false', 'non')) { - hasNonCompliant = true; - } - if (isVal('oed_bi_decantation_vide', 'false', 'non')) { - hasDamaged = true; - } - if (isVal('oed_bi_drain_colmate', true, 'true', 'oui')) { - hasDamaged = true; - } - } - - // 3. Structure enherbée / minérale - if (isVal('oed_rhm_presente', true, 'true')) { - if (isVal('oed_rhm_stagne', true, 'true', 'oui')) { - hasNonCompliant = true; - } - if (isVal('oed_rhm_joints_ok', 'false', 'non')) { - hasNonCompliant = true; - if (isVal('oed_rhm_joints_fines', true, 'true', 'oui')) { - hasDamaged = true; - } - if (isVal('oed_rhm_joints_remplissage', true, 'true', 'oui')) { - hasNonCompliant = true; - } - } - } - - // 4. Modulaire poreux / continu poreux - if (isVal('oed_rp_presente', true, 'true')) { - if (isVal('oed_rp_stagne', true, 'true', 'oui')) { - hasNonCompliant = true; - } - if (isVal('oed_rp_depots', true, 'true', 'oui')) { - hasNonCompliant = true; - } - } - - } else if (deviceType === 'ta') { - // 1. Point d'entrée - if (isVal('ta_pe_ok', 'false', 'non')) { - hasNonCompliant = true; - } - if (isVal('ta_pe_conduits_ok', 'false', 'non')) { - hasDamaged = true; - } - if (isVal('ta_pe_decantation_vide', 'false', 'non')) { - hasNonCompliant = true; - } - - // 2. Bassin d'orage - if (isVal('ta_bo_vegetation_etat', 'mauvais')) { - hasNonCompliant = true; - if (isVal('ta_bo_plantes_mortes', 'plus_10', '+ de 10%')) { - hasDamaged = true; - } - } - if (isVal('ta_bo_chenaux', true, 'true', 'oui')) { - hasNonCompliant = true; - } - if (isVal('ta_sediments', true, 'true', 'oui')) { - hasNonCompliant = true; - if (isVal('ta_sediments_rempli', true, 'true', 'oui')) { - hasDamaged = true; - } - } - if (isVal('ta_bo_envahissantes', true, 'true', 'oui')) { - hasNonCompliant = true; - } - } + const st = evaluateSingleOuvrageStatus(ouv); + if (st === 'damaged') hasDamaged = true; + else if (st === 'non_compliant') hasNonCompliant = true; } if (hasDamaged) return 'damaged'; @@ -1405,19 +1539,74 @@ `; } - const titleHeader = ouv.asset_code ? `${ouv.asset_code} — ${ouv.asset_name || 'Ouvrage #' + (idx + 1)}` : `Ouvrage #${idx + 1}`; + const titleHeader = ouv.asset_code ? `${ouv.asset_code} — ${ouv.asset_name || 'Ouvrage #' + (idx + 1)}` : (ouv.name || `Ouvrage #${idx + 1}`); + const currentStatus = ouv.is_result_manual ? (ouv.result_status || 'compliant') : evaluateSingleOuvrageStatus(ouv); + ouv.result_status = currentStatus; + + // Photos previews HTML + const existingDocs = ouv.existingDocs || []; + const newFiles = ouv.newFiles || []; + const totalOuvPhotos = existingDocs.length + newFiles.length; + + let photosPreviewsHtml = ''; + existingDocs.forEach(doc => { + let docPreview = ''; + if (doc.is_image) { + docPreview = `${doc.filename || ''}`; + } else { + docPreview = `
${doc.filename || ''}
`; + } + photosPreviewsHtml += ` +
+ ${docPreview} + +
+ `; + }); + + newFiles.forEach((file, fileIdx) => { + const isImg = file.type && file.type.startsWith('image/'); + const fileUrl = isImg ? URL.createObjectURL(file) : ''; + let filePreview = ''; + if (isImg) { + filePreview = `${file.name}`; + } else { + filePreview = `
${file.name}
`; + } + photosPreviewsHtml += ` +
+
${filePreview}
+ +
+ `; + }); + + if (totalOuvPhotos === 0) { + photosPreviewsHtml = '
{% translate "Aucune photo pour cet ouvrage" %}
'; + } card.innerHTML = `
- ${titleHeader} +
+ #${idx + 1} + ${titleHeader} +
-
-
+
+
{% translate "Équipement GIEP associé (Optionnel)" %} @@ -1471,35 +1660,113 @@
- - ${dynamicQuestionsHtml} - - -
- -
-
- - + +
+
+ + ${ouv.is_result_manual ? ` + + ` : ` + + {% translate "Calcul auto" %} + + `} +
+
+
+
-
- - +
+
-
- - -
-
- - -
-
- - +
+
+ + +
+
+ +
+ +
+ + +
+ + + + + +
+ ${photosPreviewsHtml} +
+
+ + +
+ + ${dynamicQuestionsHtml} + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ + +
`; container.appendChild(card); }); @@ -1574,6 +1841,100 @@ }); }); + // Event listeners pour la conformité de l'ouvrage + container.querySelectorAll('.ouv-status-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt(e.currentTarget.getAttribute('data-idx')); + const st = e.currentTarget.getAttribute('data-status'); + if (!giepOuvrages[idx]) return; + giepOuvrages[idx].result_status = st; + giepOuvrages[idx].is_result_manual = true; + updateSerializedGiepData(); + syncGiepCalculatedStatus(); + renderOuvrages(); + }); + }); + + container.querySelectorAll('.reset-ouv-status-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt(e.currentTarget.getAttribute('data-idx')); + if (!giepOuvrages[idx]) return; + giepOuvrages[idx].is_result_manual = false; + giepOuvrages[idx].result_status = evaluateSingleOuvrageStatus(giepOuvrages[idx]); + updateSerializedGiepData(); + syncGiepCalculatedStatus(); + renderOuvrages(); + }); + }); + + // Event listeners pour les photos de l'ouvrage + container.querySelectorAll('.ouv-camera-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt(e.currentTarget.getAttribute('data-idx')); + const inp = container.querySelector(`.ouv-camera-input[data-idx="${idx}"]`); + if (inp) inp.click(); + }); + }); + + container.querySelectorAll('.ouv-gallery-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt(e.currentTarget.getAttribute('data-idx')); + const inp = container.querySelector(`.ouv-gallery-input[data-idx="${idx}"]`); + if (inp) inp.click(); + }); + }); + + container.querySelectorAll('.ouv-camera-input, .ouv-gallery-input').forEach(inp => { + inp.addEventListener('change', (e) => { + const idx = parseInt(e.target.getAttribute('data-idx')); + if (!giepOuvrages[idx]) return; + if (!giepOuvrages[idx].newFiles) giepOuvrages[idx].newFiles = []; + Array.from(e.target.files).forEach(f => { + giepOuvrages[idx].newFiles.push(f); + }); + updateSerializedGiepData(); + renderOuvrages(); + }); + }); + + container.querySelectorAll('.remove-new-file-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt(e.currentTarget.getAttribute('data-idx')); + const fileIdx = parseInt(e.currentTarget.getAttribute('data-file-idx')); + if (giepOuvrages[idx] && giepOuvrages[idx].newFiles) { + giepOuvrages[idx].newFiles.splice(fileIdx, 1); + updateSerializedGiepData(); + renderOuvrages(); + } + }); + }); + + container.querySelectorAll('.delete-existing-doc-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt(e.currentTarget.getAttribute('data-idx')); + const docId = parseInt(e.currentTarget.getAttribute('data-doc-id')); + if (confirm("{% filter escapejs %}{% translate "Supprimer définitivement ce document ?" %}{% endfilter %}")) { + if (typeof window.deleteInspectionDoc === 'function') { + window.deleteInspectionDoc(docId); + } + if (giepOuvrages[idx] && giepOuvrages[idx].existingDocs) { + giepOuvrages[idx].existingDocs = giepOuvrages[idx].existingDocs.filter(d => d.id !== docId); + renderOuvrages(); + } + } + }); + }); + + // Event listeners pour les remarques sur cet ouvrage + container.querySelectorAll('.ouv-notes-input').forEach(textarea => { + textarea.addEventListener('input', (e) => { + const idx = parseInt(e.target.getAttribute('data-idx')); + if (!giepOuvrages[idx]) return; + giepOuvrages[idx].notes = e.target.value; + updateSerializedGiepData(); + }); + }); + // Event listeners pour les boutons segmentés (Tactiles Oui/Non) container.querySelectorAll('.btn-seg').forEach(btn => { btn.addEventListener('click', (e) => { @@ -1614,6 +1975,7 @@ }); }); + toggleParentGlobalSections(true); updateSerializedGiepData(); syncGiepCalculatedStatus(); } @@ -1674,6 +2036,18 @@ giepOuvrages = []; } + // Compléter chaque ouvrage avec local_id, result_status, notes, existingDocs + giepOuvrages.forEach((ouv, idx) => { + if (!ouv.local_id) ouv.local_id = String(idx); + if (!ouv.notes) ouv.notes = ''; + ouv.newFiles = []; + ouv.existingDocs = (data.documents || []).filter(d => (d.field_key === 'ouvrage_' + ouv.local_id || d.field_key === 'ouvrage_' + idx)); + if (ouv.result_status === undefined) { + ouv.result_status = evaluateSingleOuvrageStatus(ouv); + } + ouv.is_result_manual = Boolean(ouv.is_result_manual); + }); + // Si le statut existant diffère du statut calculé automatiquement, marquer comme surchargé const autoStatus = evaluateGiepInspectionStatus(giepOuvrages); if (data.result_status && autoStatus && data.result_status !== autoStatus) { @@ -1684,15 +2058,20 @@ isGiepStatusManuallyOverridden = false; } + toggleParentGlobalSections(true); renderOuvrages(); updateSerializedGiepData(); }; if (rootModal) { rootModal.setGiepFormData = window.setGiepFormData; + rootModal.addEventListener('hidden.bs.modal', () => { + toggleParentGlobalSections(false); + }); } initStatusRadioListeners(); + toggleParentGlobalSections(true); renderOuvrages(); })(); diff --git a/loko/inspections/templates/inspections/components/edit_inspection_modal.html b/loko/inspections/templates/inspections/components/edit_inspection_modal.html index 34bbd83..1062a69 100644 --- a/loko/inspections/templates/inspections/components/edit_inspection_modal.html +++ b/loko/inspections/templates/inspections/components/edit_inspection_modal.html @@ -35,7 +35,7 @@
-
+
{% for choice in INSPECTION_RESULT_CHOICES_CONFIG %} @@ -280,7 +280,7 @@
-
+
@@ -306,7 +306,7 @@
-
+
{% translate "Photos & Fichiers complémentaires" %}
diff --git a/loko/inspections/templates/inspections/components/inspection_form_modal.html b/loko/inspections/templates/inspections/components/inspection_form_modal.html index e27bc6a..f2df967 100644 --- a/loko/inspections/templates/inspections/components/inspection_form_modal.html +++ b/loko/inspections/templates/inspections/components/inspection_form_modal.html @@ -37,7 +37,7 @@
-
+
@@ -57,7 +57,7 @@
-
+
{% translate "Photos & Fichiers" %}
@@ -100,7 +100,7 @@ {% endif %} -
+
diff --git a/loko/inspections/templates/inspections/inspections_list.html b/loko/inspections/templates/inspections/inspections_list.html index 2dfe936..c3d1eb7 100644 --- a/loko/inspections/templates/inspections/inspections_list.html +++ b/loko/inspections/templates/inspections/inspections_list.html @@ -263,7 +263,7 @@ {% endif %} - {% if insp.notes %} + {% if insp.notes and not insp.is_giep %}
{% translate "Remarques" %}
{{ insp.notes }}
@@ -277,7 +277,7 @@
{% endif %} - {% if insp.documents.exists %} + {% if insp.documents.exists and not insp.is_giep %}
{% translate "Photos & Documents joints" %} ({{ insp.documents.count }}) diff --git a/loko/inspections/templates/inspections/inspections_list_mobile.html b/loko/inspections/templates/inspections/inspections_list_mobile.html index 7a7eff5..ccec796 100644 --- a/loko/inspections/templates/inspections/inspections_list_mobile.html +++ b/loko/inspections/templates/inspections/inspections_list_mobile.html @@ -349,7 +349,7 @@
{% endif %} - {% if insp.notes %} + {% if insp.notes and not insp.is_giep %}
{% translate "Remarques" %}
{{ insp.notes }}
@@ -363,7 +363,7 @@
{% endif %} - {% if insp.documents.exists %} + {% if insp.documents.exists and not insp.is_giep %}
{% translate "Photos & Documents joints" %} ({{ insp.documents.count }}) diff --git a/loko/inspections/templatetags/inspections_tags.py b/loko/inspections/templatetags/inspections_tags.py index 3d5d9c6..b3f7252 100644 --- a/loko/inspections/templatetags/inspections_tags.py +++ b/loko/inspections/templatetags/inspections_tags.py @@ -130,3 +130,63 @@ def get_q_val(questions_dict, key): if val is not None and val != '' and val != 'null': return val return 'null' + +@register.filter +def filter_ouvrage_docs(documents, ouv): + """ + Filtre les documents/photos d'une inspection pour un ouvrage donné. + Correspondance sur field_key == 'ouvrage_{local_id}' ou 'ouvrage_{idx}' ou 'asset_{asset_id}'. + """ + if not documents: + return [] + target_keys = set() + if isinstance(ouv, dict): + local_id = ouv.get('local_id') + if local_id is not None: + target_keys.add(f"ouvrage_{local_id}") + target_keys.add(str(local_id)) + ouv_id = ouv.get('id') + if ouv_id is not None: + target_keys.add(f"ouvrage_{ouv_id}") + target_keys.add(str(ouv_id)) + asset_id = ouv.get('asset_id') + if asset_id is not None: + target_keys.add(f"asset_{asset_id}") + elif isinstance(ouv, (int, str)): + target_keys.add(f"ouvrage_{ouv}") + target_keys.add(str(ouv)) + + matching = [] + docs_list = documents.all() if hasattr(documents, 'all') else list(documents) + for doc in docs_list: + f_key = getattr(doc, 'field_key', None) or (doc.get('field_key') if isinstance(doc, dict) else '') + if f_key and str(f_key).strip() in target_keys: + matching.append(doc) + return matching + +@register.filter +def filter_general_docs(documents, ouvrages=None): + """ + Retourne les documents/photos qui ne sont rattachés à aucun ouvrage spécifique (field_key vide ou non lié). + """ + if not documents: + return [] + docs_list = documents.all() if hasattr(documents, 'all') else list(documents) + general = [] + for doc in docs_list: + f_key = (getattr(doc, 'field_key', None) or (doc.get('field_key') if isinstance(doc, dict) else '') or '').strip() + if not f_key or not (f_key.startswith('ouvrage_') or f_key.startswith('asset_')): + general.append(doc) + return general + +@register.filter +def ouvrage_result_badge(status_code): + return INSPECTION_RESULT_CONFIG.get(status_code, {}).get('badge_class', 'bg-secondary') + +@register.filter +def ouvrage_result_label(status_code): + return INSPECTION_RESULT_CONFIG.get(status_code, {}).get('label', status_code or 'Non déterminé') + +@register.filter +def ouvrage_result_icon(status_code): + return INSPECTION_RESULT_CONFIG.get(status_code, {}).get('icon', 'bi-question-circle') diff --git a/loko/inspections/tests.py b/loko/inspections/tests.py index bdd163b..1e255e9 100644 --- a/loko/inspections/tests.py +++ b/loko/inspections/tests.py @@ -2154,4 +2154,234 @@ class PostInterventionDurabilityInspectionTestCase(TestCase): self.assertTrue(base_insp.is_hotbox_related) +class GiepInspectionRestructuringTestCase(TestCase): + def setUp(self): + from django.db import connection + with connection.cursor() as cursor: + cursor.execute("CREATE EXTENSION IF NOT EXISTS unaccent;") + cursor.execute(""" + CREATE OR REPLACE FUNCTION public.unaccent_immutable(text) + RETURNS text + LANGUAGE sql + IMMUTABLE PARALLEL SAFE + AS $function$ + SELECT public.unaccent('public.unaccent', $1) + $function$; + CREATE OR REPLACE FUNCTION public.unaccent_immutable(character varying) + RETURNS text + LANGUAGE sql + IMMUTABLE PARALLEL SAFE + AS $function$ + SELECT public.unaccent('public.unaccent', $1) + $function$; + """) + self.client = Client() + self.user = User.objects.create_user( + username='giep_inspector', + password='password123', + first_name='Giep', + last_name='Tester' + ) + from common.models import UserConfig, Role, UserThematics + admin_role, _ = Role.objects.get_or_create(name='admin') + config = UserConfig.objects.create(user=self.user) + config.roles.add(admin_role) + + self.water_thematic, _ = Thematic.objects.get_or_create( + code='water', + defaults={'name_fr': "Gestion de l'Eau / GIEP"} + ) + UserThematics.objects.create( + user_config=config, + thematic=self.water_thematic, + can_view_assets=True, + can_edit_assets=True + ) + + self.rwiz = NatureRWIZ.objects.create(name_fr="Zone GIEP Parc") + self.rwiz_ct = ContentType.objects.get_for_model(NatureRWIZ) + self.client.login(username='giep_inspector', password='password123') + + def test_compute_status_for_single_ouvrage(self): + # 1. Manual status takes precedence if valid + ouv_manual_damaged = {'result_status': 'damaged', 'is_result_manual': True} + self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_manual_damaged), 'damaged') + + ouv_manual_nc = {'result_status': 'non_compliant'} + self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_manual_nc), 'non_compliant') + + ouv_manual_compliant = {'result_status': 'compliant'} + self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_manual_compliant), 'compliant') + + # 2. Auto status computation for EVC + # Bad vegetation -> non_compliant + ouv_evc_nc = { + 'device_type': 'evc', + 'questions': {'evc_ev_veg_etat': 'mauvais'} + } + self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_evc_nc), 'non_compliant') + + # Crust sediment > 5cm -> damaged + ouv_evc_damaged = { + 'device_type': 'evc', + 'questions': {'evc_ev_sediments': True, 'evc_ev_croute_sediments': True} + } + self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_evc_damaged), 'damaged') + + # Good state -> compliant + ouv_evc_good = { + 'device_type': 'evc', + 'questions': {'evc_ev_veg_etat': 'bon', 'evc_ev_sediments': False} + } + self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_evc_good), 'compliant') + + def test_compute_status_from_data_consolidation(self): + # If any ouvrage is damaged -> overall damaged + data_with_damaged = { + 'ouvrages': [ + {'result_status': 'compliant'}, + {'result_status': 'damaged'}, + ] + } + self.assertEqual(GiepInspection.compute_status_from_data(data_with_damaged), 'damaged') + + # If any is non_compliant and none damaged -> non_compliant + data_with_nc = { + 'ouvrages': [ + {'result_status': 'compliant'}, + {'result_status': 'non_compliant'}, + ] + } + self.assertEqual(GiepInspection.compute_status_from_data(data_with_nc), 'non_compliant') + + # If all compliant -> compliant + data_all_compliant = { + 'ouvrages': [ + {'result_status': 'compliant'}, + {'result_status': 'compliant'}, + ] + } + self.assertEqual(GiepInspection.compute_status_from_data(data_all_compliant), 'compliant') + + def test_templatetags_filters(self): + from inspections.models import INSPECTION_RESULT_CONFIG + from inspections.templatetags.inspections_tags import ( + filter_ouvrage_docs, + filter_general_docs, + ouvrage_result_badge, + ouvrage_result_label, + ouvrage_result_icon, + ) + + insp = Inspection.objects.create( + asset_content_type=self.rwiz_ct, + asset_object_id=self.rwiz.id, + inspector=self.user, + result_status='compliant' + ) + doc1 = InspectionDocument.objects.create( + inspection=insp, + field_key='ouvrage_1', + file='test1.jpg' + ) + doc2 = InspectionDocument.objects.create( + inspection=insp, + field_key='ouvrage_ouv_abc', + file='test2.jpg' + ) + doc_gen = InspectionDocument.objects.create( + inspection=insp, + field_key='', + file='general.jpg' + ) + + all_docs = [doc1, doc2, doc_gen] + + # Test filter_ouvrage_docs with dict ouvrage + ouv1 = {'id': 1, 'local_id': 'local_1'} + res1 = filter_ouvrage_docs(all_docs, ouv1) + self.assertEqual(len(res1), 1) + self.assertEqual(res1[0].id, doc1.id) + + ouv2 = {'id': 99, 'local_id': 'ouv_abc'} + res2 = filter_ouvrage_docs(all_docs, ouv2) + self.assertEqual(len(res2), 1) + self.assertEqual(res2[0].id, doc2.id) + + # Test filter_general_docs + gen_res = filter_general_docs(all_docs) + self.assertEqual(len(gen_res), 1) + self.assertEqual(gen_res[0].id, doc_gen.id) + + # Badges and labels + self.assertIn('bg-success', ouvrage_result_badge('compliant')) + self.assertIn('bg-danger', ouvrage_result_badge('damaged')) + self.assertIn('bg-warning', ouvrage_result_badge('non_compliant')) + self.assertEqual(str(ouvrage_result_label('compliant')), str(INSPECTION_RESULT_CONFIG['compliant']['label'])) + self.assertEqual(str(ouvrage_result_label('damaged')), str(INSPECTION_RESULT_CONFIG['damaged']['label'])) + self.assertEqual(str(ouvrage_result_label('non_compliant')), str(INSPECTION_RESULT_CONFIG['non_compliant']['label'])) + + def test_create_inspection_api_with_giep_data_and_photos(self): + import json + from django.core.files.uploadedfile import SimpleUploadedFile + + giep_dict = { + 'meteo': {'pluie_recente': True, 'sol_sec': False}, + 'ouvrages': [ + { + 'local_id': 'ouv_1', + 'name': 'Ouvrage 1', + 'device_type': 'evc', + 'result_status': 'damaged', + 'is_result_manual': True, + 'notes': 'Bordure affaissée', + 'questions': {} + }, + { + 'local_id': 'ouv_2', + 'name': 'Ouvrage 2', + 'device_type': 'oed', + 'result_status': 'compliant', + 'is_result_manual': True, + 'notes': 'Tout est propre', + 'questions': {} + } + ] + } + + photo_ouv1 = SimpleUploadedFile("ouv1.jpg", b"fake_jpeg_ouv1", content_type="image/jpeg") + photo_gen = SimpleUploadedFile("zone.jpg", b"fake_jpeg_zone", content_type="image/jpeg") + + url = reverse('inspections:create_inspection_api') + post_data = { + 'asset_content_type_id': self.rwiz_ct.id, + 'asset_object_id': self.rwiz.id, + 'nature': 'periodic', + 'status': 'completed', + 'giep_data': json.dumps(giep_dict), + 'field_keys': ['ouvrage_ouv_1', 'general'], + 'photos': [photo_ouv1, photo_gen] + } + + response = self.client.post(url, post_data) + self.assertEqual(response.status_code, 200) + resp_json = response.json() + self.assertTrue(resp_json['success']) + + insp = Inspection.objects.get(id=resp_json['inspection_id']) + self.assertTrue(insp.is_giep) + # Consolidated status should be damaged because ouv_1 is damaged + self.assertEqual(insp.result_status, 'damaged') + # Notes should be auto-consolidated from ouv_1 and ouv_2 + self.assertIn('Ouvrage 1: Bordure affaissée', insp.notes) + self.assertIn('Ouvrage 2: Tout est propre', insp.notes) + + # Check attached documents + self.assertEqual(insp.documents.count(), 2) + doc_keys = list(insp.documents.values_list('field_key', flat=True)) + self.assertIn('ouvrage_ouv_1', doc_keys) + self.assertIn('general', doc_keys) + + + diff --git a/loko/inspections/views.py b/loko/inspections/views.py index 85c25e3..23df5a8 100644 --- a/loko/inspections/views.py +++ b/loko/inspections/views.py @@ -552,11 +552,9 @@ def create_inspection_api(request): """ API endpoint pour l'enregistrement d'une nouvelle inspection (avec prise en compte optionnelle de mission_intervention et checked_intervention). """ - content_type_id = request.POST.get('content_type_id') - object_id = request.POST.get('object_id') + content_type_id = request.POST.get('content_type_id') or request.POST.get('asset_content_type_id') + object_id = request.POST.get('object_id') or request.POST.get('asset_object_id') result_status = request.POST.get('result_status') - if not result_status: - return JsonResponse({'success': False, 'error': _("Veuillez sélectionner le résultat de l'inspection.")}, status=400) nature = request.POST.get('nature', 'visual') inspector_role = request.POST.get('inspector_role', 'observer') notes = request.POST.get('notes', '').strip() @@ -578,6 +576,9 @@ def create_inspection_api(request): is_parking_spot = (content_type.model == 'parkingspot') is_giep = (content_type.model == 'naturerwiz') + + if not is_giep and not result_status: + return JsonResponse({'success': False, 'error': _("Veuillez sélectionner le résultat de l'inspection.")}, status=400) is_signpanel = (content_type.model == 'signpanel') is_hotbox = ( request.POST.get('is_hotbox') == 'true' or @@ -665,6 +666,21 @@ def create_inspection_api(request): except Exception: parsed_giep_data = {} + if not result_status or result_status == 'pending': + result_status = GiepInspection.compute_status_from_data(parsed_giep_data) + if not result_status: + result_status = 'compliant' + + if not notes and parsed_giep_data and isinstance(parsed_giep_data.get('ouvrages'), list): + collected_notes = [] + for idx_o, ouv in enumerate(parsed_giep_data['ouvrages']): + o_notes = (ouv.get('notes') or '').strip() + if o_notes: + o_name = ouv.get('asset_code') or ouv.get('name') or f"Ouvrage #{idx_o + 1}" + collected_notes.append(f"{o_name}: {o_notes}") + if collected_notes: + notes = "\n".join(collected_notes) + inspection = GiepInspection( asset_content_type=content_type, asset_object_id=target_obj.pk, @@ -876,6 +892,7 @@ def get_inspection_detail_api(request, inspection_id): 'filename': doc.filename, 'url': doc.file_url, 'is_image': doc.is_image, + 'field_key': doc.field_key or '', }) from django.template.loader import render_to_string @@ -1099,6 +1116,8 @@ def update_inspection_api(request, inspection_id): inspection.data = json.loads(request.POST['giep_data']) except Exception: pass + if 'result_status' not in request.POST or not request.POST.get('result_status'): + inspection.result_status = GiepInspection.compute_status_from_data(inspection.data) elif isinstance(inspection, SignPanelInspection): from assets.models import SignPanelType if 'verified_signpanel_type_id' in request.POST: