feat: require all four hotbox inspection questions to be answered and update validation logic

This commit is contained in:
kdeterme 2026-09-18 13:46:53 +02:00
parent 010466e26f
commit ecb3bbddd1
6 changed files with 232 additions and 78 deletions

View file

@ -0,0 +1,33 @@
# Generated by Django 6.0.7 on 2026-09-18 10:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inspections', '0010_hotboxinspection'),
]
operations = [
migrations.AlterField(
model_name='hotboxinspection',
name='cracking',
field=models.CharField(blank=True, choices=[('none', 'Aucune fissure'), ('minor', 'Petites fissures / Faïençage'), ('major', 'Grosses fissures / Fissures ouvertes')], default=None, max_length=50, null=True, verbose_name='Fissuration'),
),
migrations.AlterField(
model_name='hotboxinspection',
name='joint_level',
field=models.CharField(blank=True, choices=[('flush', 'Bien à niveau / Arasé'), ('slight_bump_hollow', 'Léger décalage / Creux ou bosse'), ('step', 'Ressaut important (> 1-2 cm)')], default=None, max_length=50, null=True, verbose_name='Planéité / Arasement'),
),
migrations.AlterField(
model_name='hotboxinspection',
name='joint_sealing',
field=models.CharField(blank=True, choices=[('good', 'Joints bien fermés / scellés'), ('cracked', 'Joints fissurés ou abîmés'), ('open', 'Joints ouverts ou décollés')], default=None, max_length=50, null=True, verbose_name='Joints de raccordement'),
),
migrations.AlterField(
model_name='hotboxinspection',
name='water_drainage',
field=models.CharField(blank=True, choices=[('good', 'Bon écoulement / Pas de flaque'), ('stagnation', "Eau stagnante / Flaque d'eau")], default=None, max_length=50, null=True, verbose_name='Évacuation des eaux pluviales'),
),
]

View file

@ -586,6 +586,8 @@ class Inspection(models.Model):
@property
def detailed_inspection(self):
"""Downcast à la sous-classe spécialisée si disponible (ex: ParkingSpotInspection, GiepInspection, SignPanelInspection, HotBoxInspection)."""
if self.__class__ is not Inspection:
return self
if hasattr(self, 'parkingspotinspection'):
return self.parkingspotinspection
if hasattr(self, 'giepinspection'):
@ -1062,7 +1064,7 @@ class HotBoxInspection(Inspection):
joint_level = models.CharField(
max_length=50,
choices=JOINT_LEVEL_CHOICES,
default='flush',
default=None,
null=True,
blank=True,
verbose_name=_("Planéité / Arasement")
@ -1076,7 +1078,7 @@ class HotBoxInspection(Inspection):
joint_sealing = models.CharField(
max_length=50,
choices=JOINT_SEALING_CHOICES,
default='good',
default=None,
null=True,
blank=True,
verbose_name=_("Joints de raccordement")
@ -1090,7 +1092,7 @@ class HotBoxInspection(Inspection):
cracking = models.CharField(
max_length=50,
choices=CRACKING_CHOICES,
default='none',
default=None,
null=True,
blank=True,
verbose_name=_("Fissuration")
@ -1103,7 +1105,7 @@ class HotBoxInspection(Inspection):
water_drainage = models.CharField(
max_length=50,
choices=WATER_DRAINAGE_CHOICES,
default='good',
default=None,
null=True,
blank=True,
verbose_name=_("Évacuation des eaux pluviales")
@ -1116,7 +1118,7 @@ class HotBoxInspection(Inspection):
def compute_status(self):
"""
Calcule automatiquement le statut de conformité ('compliant', 'non_compliant', 'damaged')
selon la sévérité des constats visuels.
selon la sévérité des constats visuels, ou None si l'évaluation n'est pas complète.
"""
if self.joint_level == 'step' or self.joint_sealing == 'open' or self.cracking == 'major':
return 'damaged'
@ -1127,7 +1129,14 @@ class HotBoxInspection(Inspection):
self.water_drainage == 'stagnation'
):
return 'non_compliant'
if self.joint_level and self.joint_sealing and self.cracking and self.water_drainage:
return 'compliant'
return None
@property
def is_fully_evaluated(self):
"""Indique si les 4 questions de l'inspection ont été renseignées."""
return bool(self.joint_level and self.joint_sealing and self.cracking and self.water_drainage)
def inspection_document_upload_path(instance, filename):

View file

@ -6,25 +6,25 @@
<span class="small fw-bold text-primary text-uppercase d-flex align-items-center">
<i class="bi bi-fire text-danger me-2 fs-6"></i>{% translate "Contrôle visuel de réparation Hot Box" %}
</span>
<span id="hotboxRecommendedBadge" class="badge bg-success-subtle text-success border border-success-subtle px-2.5 py-1 rounded-pill small">
<i class="bi bi-check-circle-fill me-1"></i>{% translate "Suggéré : Conforme" %}
<span id="hotboxRecommendedBadge" class="badge bg-secondary-subtle text-secondary border border-secondary-subtle px-2.5 py-1 rounded-pill small">
<i class="bi bi-hourglass-split me-1"></i>{% translate "4 questions à évaluer" %}
</span>
</div>
<p class="text-muted small mb-3">
<i class="bi bi-info-circle me-1"></i>
{% translate "Évaluez visuellement la zone réparée sur les 4 critères simples ci-dessous :" %}
{% translate "Évaluez visuellement la zone réparée sur les 4 critères ci-dessous (sans valeur par défaut) :" %}
</p>
<!-- 1. Planéité / Arasement -->
<div class="mb-3">
<label class="form-label small fw-bold text-secondary mb-1 d-flex align-items-center">
<i class="bi bi-distribute-vertical me-1 text-primary"></i>
1. {% translate "Planéité / Arasement" %}
1. {% translate "Planéité / Arasement" %} <span class="text-danger ms-1">*</span>
</label>
<div class="row g-2">
<div class="col-12 col-md-4">
<input type="radio" class="btn-check hotbox-input" name="joint_level" id="hb_level_flush" value="flush" checked>
<input type="radio" class="btn-check hotbox-input" name="joint_level" id="hb_level_flush" value="flush">
<label class="btn btn-outline-success btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_level_flush">
<i class="bi bi-check-circle"></i>
<span>{% translate "Bien à niveau / Arasé" %}</span>
@ -51,11 +51,11 @@
<div class="mb-3">
<label class="form-label small fw-bold text-secondary mb-1 d-flex align-items-center">
<i class="bi bi-bounding-box me-1 text-info"></i>
2. {% translate "Joints de raccordement" %}
2. {% translate "Joints de raccordement" %} <span class="text-danger ms-1">*</span>
</label>
<div class="row g-2">
<div class="col-12 col-md-4">
<input type="radio" class="btn-check hotbox-input" name="joint_sealing" id="hb_sealing_good" value="good" checked>
<input type="radio" class="btn-check hotbox-input" name="joint_sealing" id="hb_sealing_good" value="good">
<label class="btn btn-outline-success btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_sealing_good">
<i class="bi bi-check-circle"></i>
<span>{% translate "Bien fermés / scellés" %}</span>
@ -82,11 +82,11 @@
<div class="mb-3">
<label class="form-label small fw-bold text-secondary mb-1 d-flex align-items-center">
<i class="bi bi-activity me-1 text-warning"></i>
3. {% translate "Fissuration" %}
3. {% translate "Fissuration" %} <span class="text-danger ms-1">*</span>
</label>
<div class="row g-2">
<div class="col-12 col-md-4">
<input type="radio" class="btn-check hotbox-input" name="cracking" id="hb_cracking_none" value="none" checked>
<input type="radio" class="btn-check hotbox-input" name="cracking" id="hb_cracking_none" value="none">
<label class="btn btn-outline-success btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_cracking_none">
<i class="bi bi-check-circle"></i>
<span>{% translate "Aucune fissure" %}</span>
@ -113,11 +113,11 @@
<div class="mb-2">
<label class="form-label small fw-bold text-secondary mb-1 d-flex align-items-center">
<i class="bi bi-droplet-half me-1 text-primary"></i>
4. {% translate "Évacuation des eaux pluviales" %}
4. {% translate "Évacuation des eaux pluviales" %} <span class="text-danger ms-1">*</span>
</label>
<div class="row g-2">
<div class="col-12 col-md-6">
<input type="radio" class="btn-check hotbox-input" name="water_drainage" id="hb_drainage_good" value="good" checked>
<input type="radio" class="btn-check hotbox-input" name="water_drainage" id="hb_drainage_good" value="good">
<label class="btn btn-outline-success btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_drainage_good">
<i class="bi bi-check-circle"></i>
<span>{% translate "Bon écoulement / Pas de flaque" %}</span>
@ -137,10 +137,21 @@
<script>
(function() {
function computeHotBoxStatus() {
const level = document.querySelector('input[name="joint_level"]:checked')?.value || 'flush';
const sealing = document.querySelector('input[name="joint_sealing"]:checked')?.value || 'good';
const cracking = document.querySelector('input[name="cracking"]:checked')?.value || 'none';
const drainage = document.querySelector('input[name="water_drainage"]:checked')?.value || 'good';
const level = document.querySelector('input[name="joint_level"]:checked')?.value;
const sealing = document.querySelector('input[name="joint_sealing"]:checked')?.value;
const cracking = document.querySelector('input[name="cracking"]:checked')?.value;
const drainage = document.querySelector('input[name="water_drainage"]:checked')?.value;
const count = [level, sealing, cracking, drainage].filter(Boolean).length;
const badge = document.getElementById('hotboxRecommendedBadge');
if (count < 4) {
if (badge) {
badge.className = 'badge bg-secondary-subtle text-secondary border border-secondary-subtle px-2.5 py-1 rounded-pill small';
badge.innerHTML = `<i class="bi bi-hourglass-split me-1"></i>${count}/4 {% translate "questions renseignées" %}`;
}
return;
}
let recommended = 'compliant';
let badgeText = '{% translate "Suggéré : Conforme" %}';
@ -159,7 +170,6 @@
icon = 'bi-exclamation-circle-fill';
}
const badge = document.getElementById('hotboxRecommendedBadge');
if (badge) {
badge.className = `badge ${badgeClass} px-2.5 py-1 rounded-pill small`;
badge.innerHTML = `<i class="bi ${icon} me-1"></i>${badgeText}`;

View file

@ -149,8 +149,8 @@
<h6 class="fw-bold small text-primary mb-0">
<i class="bi bi-fire text-danger me-1"></i>{% translate "Contrôles visuels Hot Box" %}
</h6>
<span id="editHotboxRecommendedBadge" class="badge bg-success-subtle text-success border border-success-subtle px-2.5 py-1 rounded-pill small">
<i class="bi bi-check-circle-fill me-1"></i>{% translate "Suggéré : Conforme" %}
<span id="editHotboxRecommendedBadge" class="badge bg-secondary-subtle text-secondary border border-secondary-subtle px-2.5 py-1 rounded-pill small">
<i class="bi bi-hourglass-split me-1"></i>{% translate "4 questions à évaluer" %}
</span>
</div>
@ -158,7 +158,7 @@
<div class="mb-3">
<label class="form-label small fw-semibold text-secondary mb-1 d-flex align-items-center">
<i class="bi bi-distribute-vertical me-1 text-primary"></i>
1. {% translate "Planéité / Arasement" %}
1. {% translate "Planéité / Arasement" %} <span class="text-danger ms-1">*</span>
</label>
<div class="row g-2">
<div class="col-12 col-md-4">
@ -189,7 +189,7 @@
<div class="mb-3">
<label class="form-label small fw-semibold text-secondary mb-1 d-flex align-items-center">
<i class="bi bi-bounding-box me-1 text-info"></i>
2. {% translate "Joints de raccordement" %}
2. {% translate "Joints de raccordement" %} <span class="text-danger ms-1">*</span>
</label>
<div class="row g-2">
<div class="col-12 col-md-4">
@ -220,7 +220,7 @@
<div class="mb-3">
<label class="form-label small fw-semibold text-secondary mb-1 d-flex align-items-center">
<i class="bi bi-activity me-1 text-warning"></i>
3. {% translate "Fissuration" %}
3. {% translate "Fissuration" %} <span class="text-danger ms-1">*</span>
</label>
<div class="row g-2">
<div class="col-12 col-md-4">
@ -251,7 +251,7 @@
<div class="mb-2">
<label class="form-label small fw-semibold text-secondary mb-1 d-flex align-items-center">
<i class="bi bi-droplet-half me-1 text-primary"></i>
4. {% translate "Évacuation des eaux pluviales" %}
4. {% translate "Évacuation des eaux pluviales" %} <span class="text-danger ms-1">*</span>
</label>
<div class="row g-2">
<div class="col-12 col-md-6">
@ -692,11 +692,20 @@ document.addEventListener('DOMContentLoaded', function() {
window.toggleEditMissingResultChoice(Boolean(data.is_zone_rwiz));
}
// Reset all result_status radios first
document.querySelectorAll('input[name="result_status"]').forEach(r => { r.checked = false; });
// Pre-check result_status radio
let targetRadioCode = data.result_status;
if (data.is_scheduled && targetRadioCode === 'pending') {
// Pour les réparations Hot Box : ne pas présélectionner de résultat par défaut pour obliger l'évaluation
if (data.is_hotbox) {
targetRadioCode = null;
} else {
targetRadioCode = 'compliant';
}
}
if (targetRadioCode) {
const rad = document.getElementById('edit_res_' + targetRadioCode);
if (rad) {
if (data.is_zone_rwiz && targetRadioCode === 'missing') {
@ -706,6 +715,7 @@ document.addEventListener('DOMContentLoaded', function() {
rad.checked = true;
}
}
}
if (document.getElementById('edit_insp_nature')) {
document.getElementById('edit_insp_nature').value = data.nature || 'visual';
@ -763,22 +773,30 @@ document.addEventListener('DOMContentLoaded', function() {
const hotboxFieldsContainer = document.getElementById('editHotBoxSpecificFields');
if (data.is_hotbox) {
if (hotboxFieldsContainer) hotboxFieldsContainer.classList.remove('d-none');
const jl = data.joint_level || 'flush';
const js = data.joint_sealing || 'good';
const cr = data.cracking || 'none';
const wd = data.water_drainage || 'good';
// Réinitialiser les boutons radio Hot Box (pas de valeur par défaut)
document.querySelectorAll('.edit-hotbox-input').forEach(r => { r.checked = false; });
const jl = data.joint_level;
const js = data.joint_sealing;
const cr = data.cracking;
const wd = data.water_drainage;
if (jl) {
const radLevel = document.getElementById('edit_hb_level_' + (jl === 'slight_bump_hollow' ? 'slight' : jl));
if (radLevel) radLevel.checked = true;
}
if (js) {
const radSealing = document.getElementById('edit_hb_sealing_' + js);
if (radSealing) radSealing.checked = true;
}
if (cr) {
const radCracking = document.getElementById('edit_hb_cracking_' + cr);
if (radCracking) radCracking.checked = true;
}
if (wd) {
const radDrainage = document.getElementById('edit_hb_drainage_' + wd);
if (radDrainage) radDrainage.checked = true;
}
if (typeof updateEditHotBoxRecommendation === 'function') {
updateEditHotBoxRecommendation(false);
@ -836,10 +854,21 @@ document.addEventListener('DOMContentLoaded', function() {
};
function updateEditHotBoxRecommendation(syncRadio = true) {
const level = document.querySelector('input[name="joint_level"]:checked')?.value || 'flush';
const sealing = document.querySelector('input[name="joint_sealing"]:checked')?.value || 'good';
const cracking = document.querySelector('input[name="cracking"]:checked')?.value || 'none';
const drainage = document.querySelector('input[name="water_drainage"]:checked')?.value || 'good';
const level = document.querySelector('#editHotBoxSpecificFields input[name="joint_level"]:checked')?.value;
const sealing = document.querySelector('#editHotBoxSpecificFields input[name="joint_sealing"]:checked')?.value;
const cracking = document.querySelector('#editHotBoxSpecificFields input[name="cracking"]:checked')?.value;
const drainage = document.querySelector('#editHotBoxSpecificFields input[name="water_drainage"]:checked')?.value;
const count = [level, sealing, cracking, drainage].filter(Boolean).length;
const badge = document.getElementById('editHotboxRecommendedBadge');
if (count < 4) {
if (badge) {
badge.className = 'badge bg-secondary-subtle text-secondary border border-secondary-subtle px-2.5 py-1 rounded-pill small';
badge.innerHTML = `<i class="bi bi-hourglass-split me-1"></i>${count}/4 {% translate "questions renseignées" %}`;
}
return;
}
let recommended = 'compliant';
let badgeText = '{% translate "Suggéré : Conforme" %}';
@ -916,6 +945,34 @@ document.addEventListener('DOMContentLoaded', function() {
alertDiv.classList.add('d-none');
successDiv.classList.add('d-none');
// Vérification pour Hot Box : obliger à répondre consciemment aux 4 questions
const hotboxContainer = document.getElementById('editHotBoxSpecificFields');
if (hotboxContainer && !hotboxContainer.classList.contains('d-none')) {
const resSelected = editForm.querySelector('input[name="result_status"]:checked')?.value;
if (resSelected !== 'cancelled') {
const level = editForm.querySelector('input[name="joint_level"]:checked')?.value;
const sealing = editForm.querySelector('input[name="joint_sealing"]:checked')?.value;
const cracking = editForm.querySelector('input[name="cracking"]:checked')?.value;
const drainage = editForm.querySelector('input[name="water_drainage"]:checked')?.value;
if (!level || !sealing || !cracking || !drainage) {
alertDiv.textContent = "{% translate 'Veuillez répondre à chacun des 4 contrôles de la réparation Hot Box (Planéité, Joints, Fissuration, Évacuation des eaux).' %}";
alertDiv.classList.remove('d-none');
alertDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
}
}
const resultStatus = editForm.querySelector('input[name="result_status"]:checked')?.value;
if (!resultStatus || resultStatus === 'pending') {
alertDiv.textContent = "{% translate 'Veuillez sélectionner le résultat de l’inspection.' %}";
alertDiv.classList.remove('d-none');
alertDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
submitBtn.disabled = true;
spinner.classList.remove('d-none');
icon.classList.add('d-none');

View file

@ -1850,19 +1850,30 @@ class PostInterventionDurabilityInspectionTestCase(TestCase):
self.assertContains(resp, "Inspections de terrain")
def test_hotbox_inspection_model_and_compute_status(self):
"""Vérifie le modèle HotBoxInspection, le calcul de statut automatique et details_summary."""
"""Vérifie le modèle HotBoxInspection sans valeur par défaut, le calcul de statut et details_summary."""
intervention = self._create_finished_intervention()
interv_ct = ContentType.objects.get_for_model(Intervention)
# Création sans valeur par défaut
hb = HotBoxInspection.objects.create(
asset_content_type=interv_ct,
asset_object_id=intervention.pk,
inspector=self.user,
inspection_date=timezone.now(),
joint_level='flush',
joint_sealing='good',
cracking='none',
water_drainage='good',
)
self.assertIsNone(hb.joint_level)
self.assertIsNone(hb.joint_sealing)
self.assertIsNone(hb.cracking)
self.assertIsNone(hb.water_drainage)
self.assertFalse(hb.is_fully_evaluated)
self.assertIsNone(hb.compute_status())
# Renseignement complet conforme
hb.joint_level = 'flush'
hb.joint_sealing = 'good'
hb.cracking = 'none'
hb.water_drainage = 'good'
self.assertTrue(hb.is_fully_evaluated)
self.assertEqual(hb.compute_status(), 'compliant')
self.assertTrue(hb.is_hotbox_related)
self.assertEqual(hb.detailed_inspection, hb)
@ -1897,8 +1908,22 @@ class PostInterventionDurabilityInspectionTestCase(TestCase):
intervention = self._create_finished_intervention()
interv_ct = ContentType.objects.get_for_model(Intervention)
# 1. Création via create_inspection_api
create_url = reverse('inspections:create_inspection_api')
# 1. Tentative de finalisation sans renseigner les 4 questions -> erreur 400
invalid_post = {
'content_type_id': interv_ct.id,
'object_id': intervention.pk,
'result_status': 'compliant',
'is_hotbox': 'true',
'joint_level': 'flush',
# joint_sealing, cracking, water_drainage manquants
}
resp_invalid = self.client.post(create_url, invalid_post)
self.assertEqual(resp_invalid.status_code, 400)
self.assertIn("Veuillez répondre à chacun des 4 contrôles", resp_invalid.json()['error'])
# 2. Création réussie avec les 4 réponses
post_data = {
'content_type_id': interv_ct.id,
'object_id': intervention.pk,
@ -1923,7 +1948,7 @@ class PostInterventionDurabilityInspectionTestCase(TestCase):
self.assertEqual(hb_obj.cracking, 'minor')
self.assertEqual(hb_obj.water_drainage, 'good')
# 2. Détail via get_inspection_detail_api
# 3. Détail via get_inspection_detail_api
detail_url = reverse('inspections:get_inspection_detail_api', kwargs={'inspection_id': insp_id})
resp_detail = self.client.get(detail_url)
self.assertEqual(resp_detail.status_code, 200)
@ -1934,9 +1959,10 @@ class PostInterventionDurabilityInspectionTestCase(TestCase):
self.assertEqual(detail_data['cracking'], 'minor')
self.assertIn("Contrôles visuels Hot Box", detail_data.get('details_html', ''))
# 3. Mise à jour via update_inspection_api
# 4. Mise à jour via update_inspection_api
update_url = reverse('inspections:update_inspection_api', kwargs={'inspection_id': insp_id})
update_data = {
'status': 'completed',
'result_status': 'non_compliant',
'joint_level': 'slight_bump_hollow',
'joint_sealing': 'cracked',

View file

@ -728,10 +728,16 @@ def create_inspection_api(request):
apply_sign_updates_to_asset=apply_changes,
)
elif is_hotbox:
joint_level = request.POST.get('joint_level') or 'flush'
joint_sealing = request.POST.get('joint_sealing') or 'good'
cracking = request.POST.get('cracking') or 'none'
water_drainage = request.POST.get('water_drainage') or 'good'
joint_level = request.POST.get('joint_level') or None
joint_sealing = request.POST.get('joint_sealing') or None
cracking = request.POST.get('cracking') or None
water_drainage = request.POST.get('water_drainage') or None
if result_status not in ('cancelled', 'pending') and not all([joint_level, joint_sealing, cracking, water_drainage]):
return JsonResponse({
'success': False,
'error': _("Veuillez répondre à chacun des 4 contrôles visuels de la réparation Hot Box (Planéité, Joints, Fissuration, Évacuation des eaux).")
}, status=400)
inspection = HotBoxInspection(
asset_content_type=content_type,
@ -954,14 +960,19 @@ def get_inspection_detail_api(request, inspection_id):
elif isinstance(leaf, HotBoxInspection) or getattr(leaf, 'is_hotbox_related', False):
data['is_hotbox'] = True
data['joint_level'] = getattr(leaf, 'joint_level', 'flush')
data['joint_level_display'] = str(leaf.get_joint_level_display()) if hasattr(leaf, 'get_joint_level_display') else ''
data['joint_sealing'] = getattr(leaf, 'joint_sealing', 'good')
data['joint_sealing_display'] = str(leaf.get_joint_sealing_display()) if hasattr(leaf, 'get_joint_sealing_display') else ''
data['cracking'] = getattr(leaf, 'cracking', 'none')
data['cracking_display'] = str(leaf.get_cracking_display()) if hasattr(leaf, 'get_cracking_display') else ''
data['water_drainage'] = getattr(leaf, 'water_drainage', 'good')
data['water_drainage_display'] = str(leaf.get_water_drainage_display()) if hasattr(leaf, 'get_water_drainage_display') else ''
jl = getattr(leaf, 'joint_level', None)
js = getattr(leaf, 'joint_sealing', None)
cr = getattr(leaf, 'cracking', None)
wd = getattr(leaf, 'water_drainage', None)
data['joint_level'] = jl or ''
data['joint_level_display'] = str(leaf.get_joint_level_display()) if jl and hasattr(leaf, 'get_joint_level_display') else ''
data['joint_sealing'] = js or ''
data['joint_sealing_display'] = str(leaf.get_joint_sealing_display()) if js and hasattr(leaf, 'get_joint_sealing_display') else ''
data['cracking'] = cr or ''
data['cracking_display'] = str(leaf.get_cracking_display()) if cr and hasattr(leaf, 'get_cracking_display') else ''
data['water_drainage'] = wd or ''
data['water_drainage_display'] = str(leaf.get_water_drainage_display()) if wd and hasattr(leaf, 'get_water_drainage_display') else ''
data['is_fully_evaluated'] = bool(jl and js and cr and wd)
return JsonResponse(data)
@ -1064,13 +1075,21 @@ def update_inspection_api(request, inspection_id):
inspection.apply_sign_updates_to_asset = (request.POST.get('apply_sign_updates_to_asset') == 'true')
elif isinstance(inspection, HotBoxInspection):
if 'joint_level' in request.POST:
inspection.joint_level = request.POST['joint_level']
inspection.joint_level = request.POST.get('joint_level') or None
if 'joint_sealing' in request.POST:
inspection.joint_sealing = request.POST['joint_sealing']
inspection.joint_sealing = request.POST.get('joint_sealing') or None
if 'cracking' in request.POST:
inspection.cracking = request.POST['cracking']
inspection.cracking = request.POST.get('cracking') or None
if 'water_drainage' in request.POST:
inspection.water_drainage = request.POST['water_drainage']
inspection.water_drainage = request.POST.get('water_drainage') or None
# Si l'inspection est marquée réalisée / complétée, exiger la réponse aux 4 questions
if inspection.status == 'completed' and inspection.result_status not in ('cancelled', 'pending'):
if not all([inspection.joint_level, inspection.joint_sealing, inspection.cracking, inspection.water_drainage]):
return JsonResponse({
'success': False,
'error': _("Veuillez répondre à chacun des 4 contrôles visuels de la réparation Hot Box (Planéité, Joints, Fissuration, Évacuation des eaux).")
}, status=400)
# Traitement des nouvelles pièces jointes / photos
uploaded_files = request.FILES.getlist('photos') + request.FILES.getlist('documents') + request.FILES.getlist('files')