feat: add HotBoxInspection model, views, templates, and admin configuration
This commit is contained in:
parent
250a2b4f2f
commit
010466e26f
10 changed files with 845 additions and 6 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.gis import admin as gis_admin
|
from django.contrib.gis import admin as gis_admin
|
||||||
from .models import Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, InspectionDocument
|
from .models import Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, HotBoxInspection, InspectionDocument
|
||||||
|
|
||||||
|
|
||||||
@admin.register(InspectionZone)
|
@admin.register(InspectionZone)
|
||||||
|
|
@ -83,6 +83,23 @@ class GiepInspectionAdmin(admin.ModelAdmin):
|
||||||
actions = [make_cancelled, make_compliant]
|
actions = [make_cancelled, make_compliant]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(HotBoxInspection)
|
||||||
|
class HotBoxInspectionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"id", "asset", "inspector", "inspection_date", "result_status",
|
||||||
|
"joint_level", "joint_sealing", "cracking", "water_drainage",
|
||||||
|
"checked_intervention"
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
"result_status", "joint_level", "joint_sealing",
|
||||||
|
"cracking", "water_drainage", "inspection_date"
|
||||||
|
)
|
||||||
|
search_fields = ("notes", "inspector__username", "inspector__first_name", "inspector__last_name", "checked_intervention__code")
|
||||||
|
raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "zone")
|
||||||
|
inlines = [InspectionDocumentInline]
|
||||||
|
actions = [make_cancelled, make_compliant]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(InspectionDocument)
|
@admin.register(InspectionDocument)
|
||||||
class InspectionDocumentAdmin(admin.ModelAdmin):
|
class InspectionDocumentAdmin(admin.ModelAdmin):
|
||||||
list_display = ("id", "inspection", "field_key", "filename", "created_at")
|
list_display = ("id", "inspection", "field_key", "filename", "created_at")
|
||||||
|
|
|
||||||
29
loko/inspections/migrations/0010_hotboxinspection.py
Normal file
29
loko/inspections/migrations/0010_hotboxinspection.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-18 09:24
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('inspections', '0009_inspection_non_compliance_notified_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='HotBoxInspection',
|
||||||
|
fields=[
|
||||||
|
('inspection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='inspections.inspection')),
|
||||||
|
('joint_level', 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='flush', max_length=50, null=True, verbose_name='Planéité / Arasement')),
|
||||||
|
('joint_sealing', 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='good', max_length=50, null=True, verbose_name='Joints de raccordement')),
|
||||||
|
('cracking', 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')),
|
||||||
|
('water_drainage', models.CharField(blank=True, choices=[('good', 'Bon écoulement / Pas de flaque'), ('stagnation', "Eau stagnante / Flaque d'eau")], default='good', max_length=50, null=True, verbose_name='Évacuation des eaux pluviales')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Inspection de réparation Hot Box',
|
||||||
|
'verbose_name_plural': 'Inspections de réparations Hot Box',
|
||||||
|
},
|
||||||
|
bases=('inspections.inspection',),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -567,15 +567,39 @@ class Inspection(models.Model):
|
||||||
return th.icon
|
return th.icon
|
||||||
return 'bi-geo-alt'
|
return 'bi-geo-alt'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_hotbox_related(self):
|
||||||
|
"""Détermine si l'inspection concerne une réparation Hot Box (asphalte à chaud)."""
|
||||||
|
if hasattr(self, 'hotboxinspection'):
|
||||||
|
return True
|
||||||
|
interv = self.related_intervention
|
||||||
|
if interv and getattr(interv, 'sub_type', None):
|
||||||
|
st = interv.sub_type
|
||||||
|
code = (st.code or '').lower()
|
||||||
|
name = (st.name_fr or '').lower()
|
||||||
|
if any(k in code or k in name for k in ('hotbox', 'hot_box', 'hot box')):
|
||||||
|
return True
|
||||||
|
if self.nature == 'post_intervention' and 'hot box' in (self.notes or '').lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def detailed_inspection(self):
|
def detailed_inspection(self):
|
||||||
"""Downcast à la sous-classe spécialisée si disponible (ex: ParkingSpotInspection, GiepInspection, SignPanelInspection)."""
|
"""Downcast à la sous-classe spécialisée si disponible (ex: ParkingSpotInspection, GiepInspection, SignPanelInspection, HotBoxInspection)."""
|
||||||
if hasattr(self, 'parkingspotinspection'):
|
if hasattr(self, 'parkingspotinspection'):
|
||||||
return self.parkingspotinspection
|
return self.parkingspotinspection
|
||||||
if hasattr(self, 'giepinspection'):
|
if hasattr(self, 'giepinspection'):
|
||||||
return self.giepinspection
|
return self.giepinspection
|
||||||
if hasattr(self, 'signpanelinspection'):
|
if hasattr(self, 'signpanelinspection'):
|
||||||
return self.signpanelinspection
|
return self.signpanelinspection
|
||||||
|
if hasattr(self, 'hotboxinspection'):
|
||||||
|
return self.hotboxinspection
|
||||||
|
if self.is_hotbox_related and self.pk:
|
||||||
|
try:
|
||||||
|
hb, _ = HotBoxInspection.objects.get_or_create(inspection_ptr=self)
|
||||||
|
return hb
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -615,6 +639,17 @@ class Inspection(models.Model):
|
||||||
if getattr(leaf, 'date_derniere_pluie', None) and leaf.date_derniere_pluie != 'null':
|
if getattr(leaf, 'date_derniere_pluie', None) and leaf.date_derniere_pluie != 'null':
|
||||||
parts.append(str(_("Pluie : %s") % leaf.get_date_derniere_pluie_display()))
|
parts.append(str(_("Pluie : %s") % leaf.get_date_derniere_pluie_display()))
|
||||||
|
|
||||||
|
# Hot Box summary
|
||||||
|
if hasattr(leaf, 'joint_level') and hasattr(leaf, 'joint_sealing'):
|
||||||
|
if leaf.joint_level:
|
||||||
|
parts.append(f"{_('Planéité')} : {leaf.get_joint_level_display()}")
|
||||||
|
if leaf.joint_sealing:
|
||||||
|
parts.append(f"{_('Joints')} : {leaf.get_joint_sealing_display()}")
|
||||||
|
if leaf.cracking:
|
||||||
|
parts.append(f"{_('Fissuration')} : {leaf.get_cracking_display()}")
|
||||||
|
if leaf.water_drainage:
|
||||||
|
parts.append(f"{_('Évacuation')} : {leaf.get_water_drainage_display()}")
|
||||||
|
|
||||||
if leaf.verified_obsolescence:
|
if leaf.verified_obsolescence:
|
||||||
parts.append(str(_("Obsolescence : %s") % leaf.verified_obsolescence))
|
parts.append(str(_("Obsolescence : %s") % leaf.verified_obsolescence))
|
||||||
|
|
||||||
|
|
@ -1010,6 +1045,91 @@ class SignPanelInspection(Inspection):
|
||||||
target_asset.save()
|
target_asset.save()
|
||||||
|
|
||||||
|
|
||||||
|
class HotBoxInspection(Inspection):
|
||||||
|
"""
|
||||||
|
Spécialisation de l'inspection pour les réparations d'enrobé à chaud (Hot Box).
|
||||||
|
Formulaire visuel simplifié adapté aux observateurs non experts :
|
||||||
|
- Planéité (Arasement)
|
||||||
|
- Joints (Raccordement)
|
||||||
|
- Fissuration
|
||||||
|
- Évacuation des eaux pluviales
|
||||||
|
"""
|
||||||
|
JOINT_LEVEL_CHOICES = (
|
||||||
|
('flush', _("Bien à niveau / Arasé")),
|
||||||
|
('slight_bump_hollow', _("Léger décalage / Creux ou bosse")),
|
||||||
|
('step', _("Ressaut important (> 1-2 cm)")),
|
||||||
|
)
|
||||||
|
joint_level = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=JOINT_LEVEL_CHOICES,
|
||||||
|
default='flush',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Planéité / Arasement")
|
||||||
|
)
|
||||||
|
|
||||||
|
JOINT_SEALING_CHOICES = (
|
||||||
|
('good', _("Joints bien fermés / scellés")),
|
||||||
|
('cracked', _("Joints fissurés ou abîmés")),
|
||||||
|
('open', _("Joints ouverts ou décollés")),
|
||||||
|
)
|
||||||
|
joint_sealing = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=JOINT_SEALING_CHOICES,
|
||||||
|
default='good',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Joints de raccordement")
|
||||||
|
)
|
||||||
|
|
||||||
|
CRACKING_CHOICES = (
|
||||||
|
('none', _("Aucune fissure")),
|
||||||
|
('minor', _("Petites fissures / Faïençage")),
|
||||||
|
('major', _("Grosses fissures / Fissures ouvertes")),
|
||||||
|
)
|
||||||
|
cracking = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=CRACKING_CHOICES,
|
||||||
|
default='none',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Fissuration")
|
||||||
|
)
|
||||||
|
|
||||||
|
WATER_DRAINAGE_CHOICES = (
|
||||||
|
('good', _("Bon écoulement / Pas de flaque")),
|
||||||
|
('stagnation', _("Eau stagnante / Flaque d'eau")),
|
||||||
|
)
|
||||||
|
water_drainage = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=WATER_DRAINAGE_CHOICES,
|
||||||
|
default='good',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Évacuation des eaux pluviales")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Inspection de réparation Hot Box")
|
||||||
|
verbose_name_plural = _("Inspections de réparations Hot Box")
|
||||||
|
|
||||||
|
def compute_status(self):
|
||||||
|
"""
|
||||||
|
Calcule automatiquement le statut de conformité ('compliant', 'non_compliant', 'damaged')
|
||||||
|
selon la sévérité des constats visuels.
|
||||||
|
"""
|
||||||
|
if self.joint_level == 'step' or self.joint_sealing == 'open' or self.cracking == 'major':
|
||||||
|
return 'damaged'
|
||||||
|
if (
|
||||||
|
self.joint_level == 'slight_bump_hollow' or
|
||||||
|
self.joint_sealing == 'cracked' or
|
||||||
|
self.cracking == 'minor' or
|
||||||
|
self.water_drainage == 'stagnation'
|
||||||
|
):
|
||||||
|
return 'non_compliant'
|
||||||
|
return 'compliant'
|
||||||
|
|
||||||
|
|
||||||
def inspection_document_upload_path(instance, filename):
|
def inspection_document_upload_path(instance, filename):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
year = now.strftime("%Y")
|
year = now.strftime("%Y")
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ def get_inspection_detail_partial_template(insp_or_model_or_name):
|
||||||
if not insp_or_model_or_name:
|
if not insp_or_model_or_name:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if hasattr(insp_or_model_or_name, 'asset') and insp_or_model_or_name.asset:
|
if hasattr(insp_or_model_or_name, 'is_hotbox_related') and insp_or_model_or_name.is_hotbox_related:
|
||||||
|
model_name = 'hotbox'
|
||||||
|
elif hasattr(insp_or_model_or_name, 'asset') and insp_or_model_or_name.asset:
|
||||||
model_name = insp_or_model_or_name.asset._meta.model_name.lower()
|
model_name = insp_or_model_or_name.asset._meta.model_name.lower()
|
||||||
elif hasattr(insp_or_model_or_name, '_meta'):
|
elif hasattr(insp_or_model_or_name, '_meta'):
|
||||||
model_name = insp_or_model_or_name._meta.model_name.lower()
|
model_name = insp_or_model_or_name._meta.model_name.lower()
|
||||||
|
|
@ -51,6 +53,8 @@ def get_inspection_detail_partial_template(insp_or_model_or_name):
|
||||||
model_name = 'parkingspot'
|
model_name = 'parkingspot'
|
||||||
elif model_name in ('signpanel', 'signpanelinspection'):
|
elif model_name in ('signpanel', 'signpanelinspection'):
|
||||||
model_name = 'signpanel'
|
model_name = 'signpanel'
|
||||||
|
elif model_name in ('hotbox', 'hotboxinspection'):
|
||||||
|
model_name = 'hotbox'
|
||||||
|
|
||||||
template_path = f"inspections/components/asset_details/_{model_name}.html"
|
template_path = f"inspections/components/asset_details/_{model_name}.html"
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,14 @@ def schedule_post_intervention_inspection(intervention):
|
||||||
if len(zone_inspectors) == 1:
|
if len(zone_inspectors) == 1:
|
||||||
assigned_inspector = zone_inspectors[0]
|
assigned_inspector = zone_inspectors[0]
|
||||||
|
|
||||||
|
from .models import Inspection, InspectionZone, HotBoxInspection
|
||||||
|
|
||||||
|
is_hotbox = any(k in (sub_type.code or '').lower() or k in (sub_type.name_fr or '').lower() for k in ('hotbox', 'hot_box', 'hot box'))
|
||||||
|
inspection_cls = HotBoxInspection if is_hotbox else Inspection
|
||||||
|
|
||||||
# Création de l'inspection planifiée
|
# Création de l'inspection planifiée
|
||||||
notes = f"Inspection de durabilité planifiée pour {sub_type.get_name()} (Intervention {intervention.code})."
|
notes = f"Inspection de durabilité planifiée pour {sub_type.get_name()} (Intervention {intervention.code})."
|
||||||
inspection = Inspection.objects.create(
|
inspection = inspection_cls.objects.create(
|
||||||
asset_content_type=target_ct,
|
asset_content_type=target_ct,
|
||||||
asset_object_id=target_id,
|
asset_object_id=target_id,
|
||||||
checked_intervention=intervention,
|
checked_intervention=intervention,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% with leaf=insp.detailed_inspection %}
|
||||||
|
{% if leaf %}
|
||||||
|
<div class="card border-0 bg-light p-3 mb-3 rounded-3 shadow-sm">
|
||||||
|
<h6 class="fw-bold mb-3 text-primary border-bottom pb-2 small text-uppercase d-flex align-items-center">
|
||||||
|
<i class="bi bi-fire text-danger me-2"></i>{% translate "Contrôles visuels Hot Box (Enrobé à chaud)" %}
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<ul class="list-group list-group-flush bg-transparent">
|
||||||
|
<!-- 1. Planéité -->
|
||||||
|
{% if leaf.joint_level %}
|
||||||
|
<li class="list-group-item bg-transparent d-flex justify-content-between align-items-center px-0 py-2.5 border-bottom-0">
|
||||||
|
<span class="text-secondary fw-medium">
|
||||||
|
<i class="bi bi-distribute-vertical me-2 text-primary fs-6"></i>{% translate "Planéité / Arasement" %}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{% if leaf.joint_level == 'flush' %}
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-check-circle-fill me-1"></i>{% translate "Bien à niveau / Arasé" %}
|
||||||
|
</span>
|
||||||
|
{% elif leaf.joint_level == 'slight_bump_hollow' %}
|
||||||
|
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-exclamation-circle-fill me-1"></i>{% translate "Léger décalage / Creux ou bosse" %}
|
||||||
|
</span>
|
||||||
|
{% elif leaf.joint_level == 'step' %}
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill me-1"></i>{% translate "Ressaut important (> 1-2 cm)" %}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
{{ leaf.get_joint_level_display|default:leaf.joint_level }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- 2. Joints de raccordement -->
|
||||||
|
{% if leaf.joint_sealing %}
|
||||||
|
<li class="list-group-item bg-transparent d-flex justify-content-between align-items-center px-0 py-2.5 border-bottom-0">
|
||||||
|
<span class="text-secondary fw-medium">
|
||||||
|
<i class="bi bi-bounding-box me-2 text-info fs-6"></i>{% translate "Joints de raccordement" %}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{% if leaf.joint_sealing == 'good' %}
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-check-circle-fill me-1"></i>{% translate "Joints bien fermés / scellés" %}
|
||||||
|
</span>
|
||||||
|
{% elif leaf.joint_sealing == 'cracked' %}
|
||||||
|
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-exclamation-circle-fill me-1"></i>{% translate "Joints fissurés ou abîmés" %}
|
||||||
|
</span>
|
||||||
|
{% elif leaf.joint_sealing == 'open' %}
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill me-1"></i>{% translate "Joints ouverts ou décollés" %}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
{{ leaf.get_joint_sealing_display|default:leaf.joint_sealing }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- 3. Fissuration -->
|
||||||
|
{% if leaf.cracking %}
|
||||||
|
<li class="list-group-item bg-transparent d-flex justify-content-between align-items-center px-0 py-2.5 border-bottom-0">
|
||||||
|
<span class="text-secondary fw-medium">
|
||||||
|
<i class="bi bi-activity me-2 text-warning fs-6"></i>{% translate "Fissuration" %}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{% if leaf.cracking == 'none' %}
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-check-circle-fill me-1"></i>{% translate "Aucune fissure" %}
|
||||||
|
</span>
|
||||||
|
{% elif leaf.cracking == 'minor' %}
|
||||||
|
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-exclamation-circle-fill me-1"></i>{% translate "Petites fissures / Faïençage" %}
|
||||||
|
</span>
|
||||||
|
{% elif leaf.cracking == 'major' %}
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill me-1"></i>{% translate "Grosses fissures / Fissures ouvertes" %}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
{{ leaf.get_cracking_display|default:leaf.cracking }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- 4. Évacuation des eaux pluviales -->
|
||||||
|
{% if leaf.water_drainage %}
|
||||||
|
<li class="list-group-item bg-transparent d-flex justify-content-between align-items-center px-0 py-2.5 border-bottom-0">
|
||||||
|
<span class="text-secondary fw-medium">
|
||||||
|
<i class="bi bi-droplet-half me-2 text-primary fs-6"></i>{% translate "Évacuation des eaux pluviales" %}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{% if leaf.water_drainage == 'good' %}
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-check-circle-fill me-1"></i>{% translate "Bon écoulement / Pas de flaque" %}
|
||||||
|
</span>
|
||||||
|
{% elif leaf.water_drainage == 'stagnation' %}
|
||||||
|
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-2.5 py-1.5 rounded-pill">
|
||||||
|
<i class="bi bi-droplet-fill me-1"></i>{% translate "Eau stagnante / Flaque d'eau" %}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
{{ leaf.get_water_drainage_display|default:leaf.water_drainage }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<!-- Contrôles visuels Hot Box (Enrobé à chaud) -->
|
||||||
|
<div id="hotBoxSpecificFields" class="border rounded-3 p-3 mb-3 bg-white shadow-sm">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2">
|
||||||
|
<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>
|
||||||
|
</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 :" %}
|
||||||
|
</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" %}
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check hotbox-input" name="joint_level" id="hb_level_slight" value="slight_bump_hollow">
|
||||||
|
<label class="btn btn-outline-warning btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_level_slight">
|
||||||
|
<i class="bi bi-exclamation-circle"></i>
|
||||||
|
<span>{% translate "Léger creux ou bosse" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check hotbox-input" name="joint_level" id="hb_level_step" value="step">
|
||||||
|
<label class="btn btn-outline-danger btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_level_step">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<span>{% translate "Ressaut (> 1-2 cm)" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Joints de raccordement -->
|
||||||
|
<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" %}
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check hotbox-input" name="joint_sealing" id="hb_sealing_cracked" value="cracked">
|
||||||
|
<label class="btn btn-outline-warning btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_sealing_cracked">
|
||||||
|
<i class="bi bi-exclamation-circle"></i>
|
||||||
|
<span>{% translate "Joints fissurés / abîmés" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check hotbox-input" name="joint_sealing" id="hb_sealing_open" value="open">
|
||||||
|
<label class="btn btn-outline-danger btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_sealing_open">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<span>{% translate "Joints ouverts / décollés" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Fissuration -->
|
||||||
|
<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" %}
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check hotbox-input" name="cracking" id="hb_cracking_minor" value="minor">
|
||||||
|
<label class="btn btn-outline-warning btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_cracking_minor">
|
||||||
|
<i class="bi bi-exclamation-circle"></i>
|
||||||
|
<span>{% translate "Petites fissures / Faïençage" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check hotbox-input" name="cracking" id="hb_cracking_major" value="major">
|
||||||
|
<label class="btn btn-outline-danger btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_cracking_major">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<span>{% translate "Grosses fissures ouvertes" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. Évacuation des eaux pluviales -->
|
||||||
|
<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" %}
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<input type="radio" class="btn-check hotbox-input" name="water_drainage" id="hb_drainage_stagnation" value="stagnation">
|
||||||
|
<label class="btn btn-outline-warning btn-sm w-100 py-2 d-flex align-items-center justify-content-center gap-1 rounded-3" for="hb_drainage_stagnation">
|
||||||
|
<i class="bi bi-droplet-fill"></i>
|
||||||
|
<span>{% translate "Eau stagnante / Flaque d'eau" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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';
|
||||||
|
|
||||||
|
let recommended = 'compliant';
|
||||||
|
let badgeText = '{% translate "Suggéré : Conforme" %}';
|
||||||
|
let badgeClass = 'bg-success-subtle text-success border border-success-subtle';
|
||||||
|
let icon = 'bi-check-circle-fill';
|
||||||
|
|
||||||
|
if (level === 'step' || sealing === 'open' || cracking === 'major') {
|
||||||
|
recommended = 'damaged';
|
||||||
|
badgeText = '{% translate "Suggéré : Dégradé" %}';
|
||||||
|
badgeClass = 'bg-danger-subtle text-danger border border-danger-subtle';
|
||||||
|
icon = 'bi-exclamation-triangle-fill';
|
||||||
|
} else if (level === 'slight_bump_hollow' || sealing === 'cracked' || cracking === 'minor' || drainage === 'stagnation') {
|
||||||
|
recommended = 'non_compliant';
|
||||||
|
badgeText = '{% translate "Suggéré : Non conforme" %}';
|
||||||
|
badgeClass = 'bg-warning-subtle text-warning-emphasis border border-warning-subtle';
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-select the corresponding general inspection result status radio if present
|
||||||
|
const modal = document.getElementById('createInspectionModal') || document.getElementById('quickInspectionModal');
|
||||||
|
if (modal) {
|
||||||
|
const rad = modal.querySelector(`input[name="result_status"][value="${recommended}"]`);
|
||||||
|
if (rad) {
|
||||||
|
rad.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.hotbox-input').forEach(input => {
|
||||||
|
input.addEventListener('change', computeHotBoxStatus);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
@ -143,6 +143,135 @@
|
||||||
{% include "inspections/components/asset_fields/_naturerwiz.html" %}
|
{% include "inspections/components/asset_fields/_naturerwiz.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Champs spécifiques aux réparations Hot Box (HotBoxInspection) -->
|
||||||
|
<div id="editHotBoxSpecificFields" class="border rounded-3 p-3 mb-3 bg-light d-none">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 1. Planéité -->
|
||||||
|
<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" %}
|
||||||
|
</label>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="joint_level" id="edit_hb_level_flush" value="flush">
|
||||||
|
<label class="btn btn-outline-success btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_level_flush">
|
||||||
|
<i class="bi bi-check-circle"></i>
|
||||||
|
<span>{% translate "Bien à niveau / Arasé" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="joint_level" id="edit_hb_level_slight" value="slight_bump_hollow">
|
||||||
|
<label class="btn btn-outline-warning btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_level_slight">
|
||||||
|
<i class="bi bi-exclamation-circle"></i>
|
||||||
|
<span>{% translate "Léger creux / bosse" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="joint_level" id="edit_hb_level_step" value="step">
|
||||||
|
<label class="btn btn-outline-danger btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_level_step">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<span>{% translate "Ressaut (> 1-2 cm)" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Joints de raccordement -->
|
||||||
|
<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" %}
|
||||||
|
</label>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="joint_sealing" id="edit_hb_sealing_good" value="good">
|
||||||
|
<label class="btn btn-outline-success btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_sealing_good">
|
||||||
|
<i class="bi bi-check-circle"></i>
|
||||||
|
<span>{% translate "Bien fermés / scellés" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="joint_sealing" id="edit_hb_sealing_cracked" value="cracked">
|
||||||
|
<label class="btn btn-outline-warning btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_sealing_cracked">
|
||||||
|
<i class="bi bi-exclamation-circle"></i>
|
||||||
|
<span>{% translate "Fissurés / abîmés" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="joint_sealing" id="edit_hb_sealing_open" value="open">
|
||||||
|
<label class="btn btn-outline-danger btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_sealing_open">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<span>{% translate "Ouverts / décollés" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Fissuration -->
|
||||||
|
<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" %}
|
||||||
|
</label>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="cracking" id="edit_hb_cracking_none" value="none">
|
||||||
|
<label class="btn btn-outline-success btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_cracking_none">
|
||||||
|
<i class="bi bi-check-circle"></i>
|
||||||
|
<span>{% translate "Aucune fissure" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="cracking" id="edit_hb_cracking_minor" value="minor">
|
||||||
|
<label class="btn btn-outline-warning btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_cracking_minor">
|
||||||
|
<i class="bi bi-exclamation-circle"></i>
|
||||||
|
<span>{% translate "Petites fissures" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="cracking" id="edit_hb_cracking_major" value="major">
|
||||||
|
<label class="btn btn-outline-danger btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_cracking_major">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<span>{% translate "Grosses fissures" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. Évacuation des eaux pluviales -->
|
||||||
|
<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" %}
|
||||||
|
</label>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="water_drainage" id="edit_hb_drainage_good" value="good">
|
||||||
|
<label class="btn btn-outline-success btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_drainage_good">
|
||||||
|
<i class="bi bi-check-circle"></i>
|
||||||
|
<span>{% translate "Bon écoulement / Pas de flaque" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<input type="radio" class="btn-check edit-hotbox-input" name="water_drainage" id="edit_hb_drainage_stagnation" value="stagnation">
|
||||||
|
<label class="btn btn-outline-warning btn-sm w-100 py-1.5 rounded-3 d-flex align-items-center justify-content-center gap-1" for="edit_hb_drainage_stagnation">
|
||||||
|
<i class="bi bi-droplet-fill"></i>
|
||||||
|
<span>{% translate "Eau stagnante / Flaque d'eau" %}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Remarques -->
|
<!-- Remarques -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold mb-1">{% translate "Remarques / Remarques du constat" %}</label>
|
<label class="form-label fw-semibold mb-1">{% translate "Remarques / Remarques du constat" %}</label>
|
||||||
|
|
@ -630,6 +759,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (giepFieldsContainer) giepFieldsContainer.classList.add('d-none');
|
if (giepFieldsContainer) giepFieldsContainer.classList.add('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Affichage et préremplissage des champs spécifiques Hot Box (HotBoxInspection)
|
||||||
|
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';
|
||||||
|
|
||||||
|
const radLevel = document.getElementById('edit_hb_level_' + (jl === 'slight_bump_hollow' ? 'slight' : jl));
|
||||||
|
if (radLevel) radLevel.checked = true;
|
||||||
|
|
||||||
|
const radSealing = document.getElementById('edit_hb_sealing_' + js);
|
||||||
|
if (radSealing) radSealing.checked = true;
|
||||||
|
|
||||||
|
const radCracking = document.getElementById('edit_hb_cracking_' + cr);
|
||||||
|
if (radCracking) radCracking.checked = true;
|
||||||
|
|
||||||
|
const radDrainage = document.getElementById('edit_hb_drainage_' + wd);
|
||||||
|
if (radDrainage) radDrainage.checked = true;
|
||||||
|
|
||||||
|
if (typeof updateEditHotBoxRecommendation === 'function') {
|
||||||
|
updateEditHotBoxRecommendation(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hotboxFieldsContainer) hotboxFieldsContainer.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Rendu des photos / documents existants
|
// Rendu des photos / documents existants
|
||||||
const existingDocsContainer = document.getElementById('editInspectionExistingDocsContainer');
|
const existingDocsContainer = document.getElementById('editInspectionExistingDocsContainer');
|
||||||
|
|
@ -678,6 +835,47 @@ 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';
|
||||||
|
|
||||||
|
let recommended = 'compliant';
|
||||||
|
let badgeText = '{% translate "Suggéré : Conforme" %}';
|
||||||
|
let badgeClass = 'bg-success-subtle text-success border border-success-subtle';
|
||||||
|
let icon = 'bi-check-circle-fill';
|
||||||
|
|
||||||
|
if (level === 'step' || sealing === 'open' || cracking === 'major') {
|
||||||
|
recommended = 'damaged';
|
||||||
|
badgeText = '{% translate "Suggéré : Dégradé" %}';
|
||||||
|
badgeClass = 'bg-danger-subtle text-danger border border-danger-subtle';
|
||||||
|
icon = 'bi-exclamation-triangle-fill';
|
||||||
|
} else if (level === 'slight_bump_hollow' || sealing === 'cracked' || cracking === 'minor' || drainage === 'stagnation') {
|
||||||
|
recommended = 'non_compliant';
|
||||||
|
badgeText = '{% translate "Suggéré : Non conforme" %}';
|
||||||
|
badgeClass = 'bg-warning-subtle text-warning-emphasis border border-warning-subtle';
|
||||||
|
icon = 'bi-exclamation-circle-fill';
|
||||||
|
}
|
||||||
|
|
||||||
|
const badge = document.getElementById('editHotboxRecommendedBadge');
|
||||||
|
if (badge) {
|
||||||
|
badge.className = `badge ${badgeClass} px-2.5 py-1 rounded-pill small`;
|
||||||
|
badge.innerHTML = `<i class="bi ${icon} me-1"></i>${badgeText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncRadio) {
|
||||||
|
const rad = document.getElementById('edit_res_' + recommended);
|
||||||
|
if (rad) {
|
||||||
|
rad.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.edit-hotbox-input').forEach(input => {
|
||||||
|
input.addEventListener('change', () => updateEditHotBoxRecommendation(true));
|
||||||
|
});
|
||||||
|
|
||||||
window.deleteInspectionDoc = function(docId) {
|
window.deleteInspectionDoc = function(docId) {
|
||||||
if (!docId) return;
|
if (!docId) return;
|
||||||
if (!confirm("{% translate 'Êtes-vous sûr de vouloir supprimer ce document ?' %}")) return;
|
if (!confirm("{% translate 'Êtes-vous sûr de vouloir supprimer ce document ?' %}")) return;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from django.utils import timezone
|
||||||
from assets.models import NatureRWIZ, NatureRWIAsset, ParkingSpot
|
from assets.models import NatureRWIZ, NatureRWIAsset, ParkingSpot
|
||||||
from interventions.models import Intervention
|
from interventions.models import Intervention
|
||||||
from common.models import Thematic
|
from common.models import Thematic
|
||||||
from .models import Inspection, ParkingSpotInspection, GiepInspection, InspectionDocument
|
from .models import Inspection, ParkingSpotInspection, GiepInspection, HotBoxInspection, InspectionDocument
|
||||||
from .registry import get_inspection_partial_template
|
from .registry import get_inspection_partial_template
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
@ -1849,5 +1849,120 @@ class PostInterventionDurabilityInspectionTestCase(TestCase):
|
||||||
self.assertContains(resp, f"inspection_id={insp.id}")
|
self.assertContains(resp, f"inspection_id={insp.id}")
|
||||||
self.assertContains(resp, "Inspections de terrain")
|
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."""
|
||||||
|
intervention = self._create_finished_intervention()
|
||||||
|
interv_ct = ContentType.objects.get_for_model(Intervention)
|
||||||
|
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.assertEqual(hb.compute_status(), 'compliant')
|
||||||
|
self.assertTrue(hb.is_hotbox_related)
|
||||||
|
self.assertEqual(hb.detailed_inspection, hb)
|
||||||
|
|
||||||
|
# Vérification details_summary
|
||||||
|
summary = hb.details_summary
|
||||||
|
self.assertIn("Planéité", summary)
|
||||||
|
self.assertIn("Joints", summary)
|
||||||
|
self.assertIn("Fissuration", summary)
|
||||||
|
self.assertIn("Évacuation", summary)
|
||||||
|
|
||||||
|
# Problème mineur -> non_compliant
|
||||||
|
hb.water_drainage = 'stagnation'
|
||||||
|
self.assertEqual(hb.compute_status(), 'non_compliant')
|
||||||
|
hb.water_drainage = 'good'
|
||||||
|
hb.cracking = 'minor'
|
||||||
|
self.assertEqual(hb.compute_status(), 'non_compliant')
|
||||||
|
|
||||||
|
# Problème majeur -> damaged
|
||||||
|
hb.cracking = 'major'
|
||||||
|
self.assertEqual(hb.compute_status(), 'damaged')
|
||||||
|
hb.cracking = 'none'
|
||||||
|
hb.joint_level = 'step'
|
||||||
|
self.assertEqual(hb.compute_status(), 'damaged')
|
||||||
|
hb.joint_level = 'flush'
|
||||||
|
hb.joint_sealing = 'open'
|
||||||
|
self.assertEqual(hb.compute_status(), 'damaged')
|
||||||
|
|
||||||
|
def test_hotbox_inspection_apis(self):
|
||||||
|
"""Vérifie la création, la lecture des détails et la mise à jour via les endpoints API."""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
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')
|
||||||
|
post_data = {
|
||||||
|
'content_type_id': interv_ct.id,
|
||||||
|
'object_id': intervention.pk,
|
||||||
|
'result_status': 'compliant',
|
||||||
|
'nature': 'post_intervention',
|
||||||
|
'is_hotbox': 'true',
|
||||||
|
'joint_level': 'flush',
|
||||||
|
'joint_sealing': 'good',
|
||||||
|
'cracking': 'minor',
|
||||||
|
'water_drainage': 'good',
|
||||||
|
'notes': 'Contrôle réparation enrobé',
|
||||||
|
}
|
||||||
|
resp = self.client.post(create_url, post_data)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
data = resp.json()
|
||||||
|
self.assertTrue(data['success'])
|
||||||
|
insp_id = data['inspection_id']
|
||||||
|
|
||||||
|
# Vérifier que l'instance créée est bien un HotBoxInspection
|
||||||
|
hb_obj = HotBoxInspection.objects.get(pk=insp_id)
|
||||||
|
self.assertEqual(hb_obj.joint_level, 'flush')
|
||||||
|
self.assertEqual(hb_obj.cracking, 'minor')
|
||||||
|
self.assertEqual(hb_obj.water_drainage, 'good')
|
||||||
|
|
||||||
|
# 2. 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)
|
||||||
|
detail_data = resp_detail.json()
|
||||||
|
self.assertTrue(detail_data['success'])
|
||||||
|
self.assertTrue(detail_data.get('is_hotbox'))
|
||||||
|
self.assertEqual(detail_data['joint_level'], 'flush')
|
||||||
|
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
|
||||||
|
update_url = reverse('inspections:update_inspection_api', kwargs={'inspection_id': insp_id})
|
||||||
|
update_data = {
|
||||||
|
'result_status': 'non_compliant',
|
||||||
|
'joint_level': 'slight_bump_hollow',
|
||||||
|
'joint_sealing': 'cracked',
|
||||||
|
'cracking': 'minor',
|
||||||
|
'water_drainage': 'stagnation',
|
||||||
|
'notes': 'Flaque résiduelle observée après pluie',
|
||||||
|
}
|
||||||
|
resp_update = self.client.post(update_url, update_data)
|
||||||
|
self.assertEqual(resp_update.status_code, 200)
|
||||||
|
self.assertTrue(resp_update.json()['success'])
|
||||||
|
|
||||||
|
hb_obj.refresh_from_db()
|
||||||
|
self.assertEqual(hb_obj.result_status, 'non_compliant')
|
||||||
|
self.assertEqual(hb_obj.joint_level, 'slight_bump_hollow')
|
||||||
|
self.assertEqual(hb_obj.joint_sealing, 'cracked')
|
||||||
|
self.assertEqual(hb_obj.water_drainage, 'stagnation')
|
||||||
|
self.assertEqual(hb_obj.notes, 'Flaque résiduelle observée après pluie')
|
||||||
|
|
||||||
|
def test_schedule_post_intervention_inspection_creates_hotbox_inspection(self):
|
||||||
|
"""Vérifie que schedule_post_intervention_inspection pour un sous-type Hot Box instancie directement un HotBoxInspection."""
|
||||||
|
from inspections.services import schedule_post_intervention_inspection
|
||||||
|
intervention = self._create_finished_intervention(sub_type=self.hotbox_subtype)
|
||||||
|
insp = schedule_post_intervention_inspection(intervention)
|
||||||
|
self.assertIsNotNone(insp)
|
||||||
|
self.assertIsInstance(insp.detailed_inspection, HotBoxInspection)
|
||||||
|
self.assertTrue(insp.is_hotbox_related)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from common.models import Thematic
|
||||||
from common.utils import get_short_name
|
from common.utils import get_short_name
|
||||||
from assets.models import AssetCategory
|
from assets.models import AssetCategory
|
||||||
from .models import (
|
from .models import (
|
||||||
Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, SignPanelInspection, InspectionDocument,
|
Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, SignPanelInspection, HotBoxInspection, InspectionDocument,
|
||||||
INSPECTION_RESULT_CHOICES, INSPECTION_NATURE_CHOICES, INSPECTOR_ROLE_CHOICES
|
INSPECTION_RESULT_CHOICES, INSPECTION_NATURE_CHOICES, INSPECTOR_ROLE_CHOICES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -579,6 +579,18 @@ def create_inspection_api(request):
|
||||||
is_parking_spot = (content_type.model == 'parkingspot')
|
is_parking_spot = (content_type.model == 'parkingspot')
|
||||||
is_giep = (content_type.model == 'naturerwiz')
|
is_giep = (content_type.model == 'naturerwiz')
|
||||||
is_signpanel = (content_type.model == 'signpanel')
|
is_signpanel = (content_type.model == 'signpanel')
|
||||||
|
is_hotbox = (
|
||||||
|
request.POST.get('is_hotbox') == 'true' or
|
||||||
|
request.POST.get('inspection_type') == 'hotbox' or
|
||||||
|
content_type.model in ('hotbox', 'hotboxinspection')
|
||||||
|
)
|
||||||
|
if not is_hotbox and checked_intervention_id:
|
||||||
|
from interventions.models import Intervention
|
||||||
|
interv = Intervention.objects.filter(pk=checked_intervention_id).first()
|
||||||
|
if interv and getattr(interv, 'sub_type', None):
|
||||||
|
st = interv.sub_type
|
||||||
|
if any(k in (st.code or '').lower() or k in (st.name_fr or '').lower() for k in ('hotbox', 'hot_box', 'hot box')):
|
||||||
|
is_hotbox = True
|
||||||
|
|
||||||
if is_giep and result_status == 'missing':
|
if is_giep and result_status == 'missing':
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
|
|
@ -715,6 +727,30 @@ def create_inspection_api(request):
|
||||||
ai_confidence=ai_conf,
|
ai_confidence=ai_conf,
|
||||||
apply_sign_updates_to_asset=apply_changes,
|
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'
|
||||||
|
|
||||||
|
inspection = HotBoxInspection(
|
||||||
|
asset_content_type=content_type,
|
||||||
|
asset_object_id=target_obj.pk,
|
||||||
|
inspector=request.user,
|
||||||
|
inspection_date=timezone.now(),
|
||||||
|
result_status=result_status,
|
||||||
|
nature=nature,
|
||||||
|
inspector_role=inspector_role,
|
||||||
|
mission_intervention_id=mission_intervention_id,
|
||||||
|
checked_intervention_id=checked_intervention_id,
|
||||||
|
verified_obsolescence=verified_obsolescence,
|
||||||
|
notes=notes,
|
||||||
|
apply_changes_to_asset=apply_changes,
|
||||||
|
joint_level=joint_level,
|
||||||
|
joint_sealing=joint_sealing,
|
||||||
|
cracking=cracking,
|
||||||
|
water_drainage=water_drainage,
|
||||||
|
)
|
||||||
inspection.save()
|
inspection.save()
|
||||||
else:
|
else:
|
||||||
inspection = Inspection.objects.create(
|
inspection = Inspection.objects.create(
|
||||||
|
|
@ -916,6 +952,17 @@ def get_inspection_detail_api(request, inspection_id):
|
||||||
data['ai_confidence'] = leaf.ai_confidence
|
data['ai_confidence'] = leaf.ai_confidence
|
||||||
data['apply_sign_updates_to_asset'] = leaf.apply_sign_updates_to_asset
|
data['apply_sign_updates_to_asset'] = leaf.apply_sign_updates_to_asset
|
||||||
|
|
||||||
|
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 ''
|
||||||
|
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1015,6 +1062,15 @@ def update_inspection_api(request, inspection_id):
|
||||||
inspection.verified_cleanliness = request.POST.get('verified_cleanliness')
|
inspection.verified_cleanliness = request.POST.get('verified_cleanliness')
|
||||||
if 'apply_sign_updates_to_asset' in request.POST:
|
if 'apply_sign_updates_to_asset' in request.POST:
|
||||||
inspection.apply_sign_updates_to_asset = (request.POST.get('apply_sign_updates_to_asset') == 'true')
|
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']
|
||||||
|
if 'joint_sealing' in request.POST:
|
||||||
|
inspection.joint_sealing = request.POST['joint_sealing']
|
||||||
|
if 'cracking' in request.POST:
|
||||||
|
inspection.cracking = request.POST['cracking']
|
||||||
|
if 'water_drainage' in request.POST:
|
||||||
|
inspection.water_drainage = request.POST['water_drainage']
|
||||||
|
|
||||||
# Traitement des nouvelles pièces jointes / photos
|
# Traitement des nouvelles pièces jointes / photos
|
||||||
uploaded_files = request.FILES.getlist('photos') + request.FILES.getlist('documents') + request.FILES.getlist('files')
|
uploaded_files = request.FILES.getlist('photos') + request.FILES.getlist('documents') + request.FILES.getlist('files')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue