feat: add inspections application and update interventions module with new models, views, and migrations
This commit is contained in:
parent
836b5c8edb
commit
676e781943
15 changed files with 1008 additions and 46 deletions
|
|
@ -1,5 +1,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Inspection, ParkingSpotInspection, GiepInspection, InspectionDocument
|
from django.contrib.gis import admin as gis_admin
|
||||||
|
from .models import Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, InspectionDocument
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(InspectionZone)
|
||||||
|
class InspectionZoneAdmin(gis_admin.GISModelAdmin):
|
||||||
|
list_display = ("code", "name_fr", "thematic", "is_active", "created_at")
|
||||||
|
list_filter = ("thematic", "is_active")
|
||||||
|
search_fields = ("code", "name_fr", "name_nl")
|
||||||
|
filter_horizontal = ("municipalities", "inspectors")
|
||||||
|
|
||||||
|
|
||||||
class InspectionDocumentInline(admin.TabularInline):
|
class InspectionDocumentInline(admin.TabularInline):
|
||||||
|
|
@ -11,6 +20,7 @@ class InspectionDocumentInline(admin.TabularInline):
|
||||||
def make_cancelled(modeladmin, request, queryset):
|
def make_cancelled(modeladmin, request, queryset):
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
obj.result_status = 'cancelled'
|
obj.result_status = 'cancelled'
|
||||||
|
obj.status = 'cancelled'
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,26 +28,28 @@ def make_cancelled(modeladmin, request, queryset):
|
||||||
def make_compliant(modeladmin, request, queryset):
|
def make_compliant(modeladmin, request, queryset):
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
obj.result_status = 'compliant'
|
obj.result_status = 'compliant'
|
||||||
|
obj.status = 'completed'
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Inspection)
|
@admin.register(Inspection)
|
||||||
class InspectionAdmin(admin.ModelAdmin):
|
class InspectionAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"id", "asset", "inspector", "inspection_date",
|
"id", "asset", "status", "zone", "scheduled_start_date", "scheduled_due_date",
|
||||||
|
"inspector", "inspection_date",
|
||||||
"result_status", "mission_intervention", "checked_intervention",
|
"result_status", "mission_intervention", "checked_intervention",
|
||||||
"apply_changes_to_asset"
|
"apply_changes_to_asset"
|
||||||
)
|
)
|
||||||
list_filter = (
|
list_filter = (
|
||||||
"result_status", "nature", "inspector_role",
|
"status", "result_status", "nature", "zone", "inspector_role",
|
||||||
"apply_changes_to_asset", "inspection_date"
|
"apply_changes_to_asset", "inspection_date"
|
||||||
)
|
)
|
||||||
search_fields = (
|
search_fields = (
|
||||||
"notes", "inspector__username", "inspector__first_name",
|
"notes", "inspector__username", "inspector__first_name",
|
||||||
"inspector__last_name", "mission_intervention__name",
|
"inspector__last_name", "mission_intervention__code",
|
||||||
"checked_intervention__name"
|
"checked_intervention__code"
|
||||||
)
|
)
|
||||||
raw_id_fields = ("inspector", "mission_intervention", "checked_intervention")
|
raw_id_fields = ("inspector", "mission_intervention", "checked_intervention", "zone")
|
||||||
inlines = [InspectionDocumentInline]
|
inlines = [InspectionDocumentInline]
|
||||||
actions = [make_cancelled, make_compliant]
|
actions = [make_cancelled, make_compliant]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-16 14:19
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('common', '0014_userconfig_inspection_list_filters_and_more'),
|
||||||
|
('inspections', '0008_signpanelinspection'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='non_compliance_notified',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Notification de non-conformité envoyée'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='scheduled_due_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name="Échéance limite d'inspection"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='scheduled_start_date',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name="Début de fenêtre d'inspection"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('scheduled', 'Planifiée / À réaliser'), ('in_progress', 'En cours'), ('completed', 'Réalisée'), ('cancelled', 'Annulée')], default='completed', max_length=50, verbose_name="Statut d'avancement"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='result_status',
|
||||||
|
field=models.CharField(choices=[('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('damaged', 'Endommagé'), ('missing', 'Absent / Retiré'), ('cancelled', 'Annulé'), ('pending', 'En attente')], default='compliant', max_length=50, verbose_name='Résultat du contrôle'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InspectionZone',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(max_length=50, unique=True, verbose_name='Code du secteur / zone')),
|
||||||
|
('name_fr', models.CharField(max_length=200, verbose_name='Nom (FR)')),
|
||||||
|
('name_nl', models.CharField(blank=True, max_length=200, verbose_name='Nom (NL)')),
|
||||||
|
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise géographique')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Actif')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('inspectors', models.ManyToManyField(blank=True, related_name='assigned_inspection_zones', to=settings.AUTH_USER_MODEL, verbose_name='Inspecteurs assignés')),
|
||||||
|
('municipalities', models.ManyToManyField(blank=True, related_name='inspection_zones', to='common.municipality', verbose_name='Communes associées')),
|
||||||
|
('thematic', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inspection_zones', to='common.thematic', verbose_name='Thématique')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': "Zone / District d'inspection",
|
||||||
|
'verbose_name_plural': "Zones / Districts d'inspection",
|
||||||
|
'ordering': ['thematic', 'name_fr'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='inspection',
|
||||||
|
name='zone',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inspections', to='inspections.inspectionzone', verbose_name="Zone / District d'inspection"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from django.db import models
|
from django.contrib.gis.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
|
@ -44,12 +44,26 @@ INSPECTION_RESULT_CONFIG = {
|
||||||
'icon': 'bi-slash-circle',
|
'icon': 'bi-slash-circle',
|
||||||
'color_hex': '#6c757d',
|
'color_hex': '#6c757d',
|
||||||
},
|
},
|
||||||
|
'pending': {
|
||||||
|
'label': _('En attente'),
|
||||||
|
'badge_class': 'bg-light text-dark border',
|
||||||
|
'btn_class': 'btn-outline-secondary',
|
||||||
|
'icon': 'bi-clock',
|
||||||
|
'color_hex': '#6c757d',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
INSPECTION_RESULT_CHOICES = tuple(
|
INSPECTION_RESULT_CHOICES = tuple(
|
||||||
(code, cfg['label']) for code, cfg in INSPECTION_RESULT_CONFIG.items()
|
(code, cfg['label']) for code, cfg in INSPECTION_RESULT_CONFIG.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
INSPECTION_STATUS_CHOICES = (
|
||||||
|
('scheduled', _('Planifiée / À réaliser')),
|
||||||
|
('in_progress', _('En cours')),
|
||||||
|
('completed', _('Réalisée')),
|
||||||
|
('cancelled', _('Annulée')),
|
||||||
|
)
|
||||||
|
|
||||||
INSPECTION_NATURE_CHOICES = (
|
INSPECTION_NATURE_CHOICES = (
|
||||||
('visual', _('Inspection visuelle')),
|
('visual', _('Inspection visuelle')),
|
||||||
('technical', _('Inspection technique')),
|
('technical', _('Inspection technique')),
|
||||||
|
|
@ -63,6 +77,53 @@ INSPECTOR_ROLE_CHOICES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InspectionZone(models.Model):
|
||||||
|
"""
|
||||||
|
Zone / District géographique d'inspection pour une thématique donnée.
|
||||||
|
Permet d'affecter automatiquement les inspections de durabilité et de filtrer les tournées des inspecteurs.
|
||||||
|
"""
|
||||||
|
code = models.CharField(max_length=50, unique=True, verbose_name=_("Code du secteur / zone"))
|
||||||
|
name_fr = models.CharField(max_length=200, verbose_name=_("Nom (FR)"))
|
||||||
|
name_nl = models.CharField(max_length=200, blank=True, verbose_name=_("Nom (NL)"))
|
||||||
|
thematic = models.ForeignKey(
|
||||||
|
'common.Thematic',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='inspection_zones',
|
||||||
|
verbose_name=_("Thématique")
|
||||||
|
)
|
||||||
|
geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise géographique"))
|
||||||
|
municipalities = models.ManyToManyField(
|
||||||
|
'common.Municipality',
|
||||||
|
blank=True,
|
||||||
|
related_name='inspection_zones',
|
||||||
|
verbose_name=_("Communes associées")
|
||||||
|
)
|
||||||
|
inspectors = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
blank=True,
|
||||||
|
related_name='assigned_inspection_zones',
|
||||||
|
verbose_name=_("Inspecteurs assignés")
|
||||||
|
)
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name=_("Actif"))
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Zone / District d'inspection")
|
||||||
|
verbose_name_plural = _("Zones / Districts d'inspection")
|
||||||
|
ordering = ['thematic', 'name_fr']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name_fr} ({self.thematic.name})"
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
from django.utils.translation import get_language
|
||||||
|
lang = get_language()
|
||||||
|
if lang == 'nl' and self.name_nl:
|
||||||
|
return self.name_nl
|
||||||
|
return self.name_fr
|
||||||
|
|
||||||
|
|
||||||
class Inspection(models.Model):
|
class Inspection(models.Model):
|
||||||
"""
|
"""
|
||||||
Modèle d'enregistrement générique d'une vérification/inspection sur le terrain d'une cible (Asset, Localisation, Intervention).
|
Modèle d'enregistrement générique d'une vérification/inspection sur le terrain d'une cible (Asset, Localisation, Intervention).
|
||||||
|
|
@ -124,6 +185,35 @@ class Inspection(models.Model):
|
||||||
verbose_name=_("Résultat du contrôle")
|
verbose_name=_("Résultat du contrôle")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=INSPECTION_STATUS_CHOICES,
|
||||||
|
default='completed',
|
||||||
|
verbose_name=_("Statut d'avancement")
|
||||||
|
)
|
||||||
|
scheduled_start_date = models.DateField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Début de fenêtre d'inspection")
|
||||||
|
)
|
||||||
|
scheduled_due_date = models.DateField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Échéance limite d'inspection")
|
||||||
|
)
|
||||||
|
zone = models.ForeignKey(
|
||||||
|
InspectionZone,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='inspections',
|
||||||
|
verbose_name=_("Zone / District d'inspection")
|
||||||
|
)
|
||||||
|
non_compliance_notified = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("Notification de non-conformité envoyée")
|
||||||
|
)
|
||||||
|
|
||||||
verified_obsolescence = models.CharField(
|
verified_obsolescence = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
null=True,
|
null=True,
|
||||||
|
|
@ -156,7 +246,104 @@ class Inspection(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cancelled(self):
|
def is_cancelled(self):
|
||||||
return self.result_status == 'cancelled'
|
return self.result_status == 'cancelled' or self.status == 'cancelled'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_scheduled(self):
|
||||||
|
return self.status == 'scheduled'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeliness(self):
|
||||||
|
"""
|
||||||
|
Niveau d'urgence temporelle pour une inspection planifiée:
|
||||||
|
'future', 'current', 'urgent', 'overdue', ou None.
|
||||||
|
"""
|
||||||
|
if self.status != 'scheduled' or not self.scheduled_due_date:
|
||||||
|
return None
|
||||||
|
today = timezone.now().date()
|
||||||
|
if self.scheduled_start_date and today < self.scheduled_start_date:
|
||||||
|
return 'future'
|
||||||
|
if today > self.scheduled_due_date:
|
||||||
|
return 'overdue'
|
||||||
|
days_left = (self.scheduled_due_date - today).days
|
||||||
|
if days_left <= 7:
|
||||||
|
return 'urgent'
|
||||||
|
return 'current'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeliness_badge_class(self):
|
||||||
|
mapping = {
|
||||||
|
'future': 'bg-secondary text-white',
|
||||||
|
'current': 'bg-success text-white',
|
||||||
|
'urgent': 'bg-warning text-dark',
|
||||||
|
'overdue': 'bg-danger text-white',
|
||||||
|
}
|
||||||
|
return mapping.get(self.timeliness, 'bg-light text-dark')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeliness_label(self):
|
||||||
|
mapping = {
|
||||||
|
'future': _('À venir'),
|
||||||
|
'current': _('À réaliser'),
|
||||||
|
'urgent': _('Échéance proche'),
|
||||||
|
'overdue': _('En retard'),
|
||||||
|
}
|
||||||
|
return mapping.get(self.timeliness, '')
|
||||||
|
|
||||||
|
def notify_sub_type_teams_if_needed(self):
|
||||||
|
"""
|
||||||
|
Notifie les équipes configurées sur le sous-type d'intervention
|
||||||
|
si le résultat du contrôle de durabilité est non-conforme.
|
||||||
|
"""
|
||||||
|
if not self.checked_intervention or not getattr(self.checked_intervention, 'sub_type_id', None):
|
||||||
|
return
|
||||||
|
sub_type = self.checked_intervention.sub_type
|
||||||
|
if not sub_type or not sub_type.requires_post_inspection:
|
||||||
|
return
|
||||||
|
teams = list(sub_type.notification_teams.all())
|
||||||
|
if not teams:
|
||||||
|
return
|
||||||
|
|
||||||
|
from notifications.services import notify_team
|
||||||
|
status_label = self.get_result_status_display()
|
||||||
|
title = _("Non-conformité durabilité (%(status)s) : %(code)s") % {
|
||||||
|
'status': status_label,
|
||||||
|
'code': self.checked_intervention.code,
|
||||||
|
}
|
||||||
|
inspector_name = self.inspector.get_full_name() if self.inspector else str(_("Non assigné"))
|
||||||
|
content = _(
|
||||||
|
"L'inspection de durabilité sur l'intervention %(code)s (%(subtype)s) "
|
||||||
|
"a révélé un constat non conforme : %(status)s.\n"
|
||||||
|
"Inspecteur : %(inspector)s\n"
|
||||||
|
"Remarques : %(notes)s"
|
||||||
|
) % {
|
||||||
|
'code': self.checked_intervention.code,
|
||||||
|
'subtype': sub_type.get_name(),
|
||||||
|
'status': status_label,
|
||||||
|
'inspector': inspector_name,
|
||||||
|
'notes': self.notes or '-',
|
||||||
|
}
|
||||||
|
action_url = f"/interventions/{self.checked_intervention.id}/"
|
||||||
|
|
||||||
|
for team in teams:
|
||||||
|
try:
|
||||||
|
notify_team(
|
||||||
|
team=team,
|
||||||
|
title=str(title),
|
||||||
|
content=str(content),
|
||||||
|
notification_type='alert',
|
||||||
|
action_url=action_url,
|
||||||
|
action_label=str(_("Consulter l'intervention")),
|
||||||
|
source_app='inspections',
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).exception(
|
||||||
|
f"Error sending non-compliance notification for inspection {self.pk} to team {team.pk}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.non_compliance_notified = True
|
||||||
|
Inspection.objects.filter(pk=self.pk).update(non_compliance_notified=True)
|
||||||
|
|
||||||
def can_edit(self, user):
|
def can_edit(self, user):
|
||||||
from .permissions import can_edit_inspection
|
from .permissions import can_edit_inspection
|
||||||
|
|
@ -165,7 +352,11 @@ class Inspection(models.Model):
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if self.apply_changes_to_asset and self.asset:
|
# Notification automatique en cas de non-conformité post-travaux
|
||||||
|
if self.status == 'completed' and self.result_status in ('non_compliant', 'damaged', 'missing') and not self.non_compliance_notified:
|
||||||
|
self.notify_sub_type_teams_if_needed()
|
||||||
|
|
||||||
|
if self.status == 'completed' and self.apply_changes_to_asset and self.asset:
|
||||||
target_asset = self.asset
|
target_asset = self.asset
|
||||||
modified = False
|
modified = False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,23 @@ def can_edit_inspection(user, inspection):
|
||||||
if user_config.has_role('admin') or user_config.has_role('top_manager'):
|
if user_config.has_role('admin') or user_config.has_role('top_manager'):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Auteur de l'inspection (l'inspecteur créateur)
|
# Auteur de l'inspection (l'inspecteur créateur ou assigné)
|
||||||
if inspection and inspection.inspector_id == user.id:
|
if inspection and inspection.inspector_id == user.id:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Pour une inspection planifiée / à réaliser :
|
||||||
|
if inspection and getattr(inspection, 'status', None) == 'scheduled':
|
||||||
|
# Inspecteur faisant partie de la zone d'inspection
|
||||||
|
if inspection.zone_id and inspection.zone.inspectors.filter(id=user.id).exists():
|
||||||
|
return True
|
||||||
|
# Inspecteur ayant la permission can_inspect sur la thématique
|
||||||
|
insp_thematic = get_inspection_thematic(inspection)
|
||||||
|
if user_config and insp_thematic:
|
||||||
|
from common.models import UserThematics
|
||||||
|
ut = UserThematics.objects.filter(user_config=user_config, thematic=insp_thematic).first()
|
||||||
|
if ut and ut.can_inspect:
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -316,8 +329,9 @@ def filter_viewable_inspections_for_user(user, qs=None):
|
||||||
|
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
# 1. Auteur / Inspecteur de l'inspection
|
# 1. Auteur / Inspecteur de l'inspection ou inspecteur de la zone
|
||||||
conditions.append(Q(inspector=user))
|
conditions.append(Q(inspector=user))
|
||||||
|
conditions.append(Q(zone__inspectors=user))
|
||||||
|
|
||||||
# 2. Interventions liées visibles (missions d'intervention, contrôles post-travaux, ou cible intervention)
|
# 2. Interventions liées visibles (missions d'intervention, contrôles post-travaux, ou cible intervention)
|
||||||
from interventions.permissions import filter_viewable_interventions_for_user
|
from interventions.permissions import filter_viewable_interventions_for_user
|
||||||
|
|
|
||||||
103
loko/inspections/services.py
Normal file
103
loko/inspections/services.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_post_intervention_inspection(intervention):
|
||||||
|
"""
|
||||||
|
Planifie automatiquement une inspection de durabilité pour une intervention
|
||||||
|
lorsque son sous-type requiert une inspection post-travaux (ex: Hot Box).
|
||||||
|
"""
|
||||||
|
from .models import Inspection, InspectionZone
|
||||||
|
|
||||||
|
sub_type = getattr(intervention, 'sub_type', None)
|
||||||
|
if not sub_type or not getattr(sub_type, 'requires_post_inspection', False):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Éviter les doublons : vérifier si une inspection post-intervention non annulée existe déjà
|
||||||
|
existing = Inspection.objects.filter(
|
||||||
|
checked_intervention=intervention,
|
||||||
|
nature=sub_type.post_inspection_nature
|
||||||
|
).exclude(status='cancelled').first()
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
# Calcul des dates de la fenêtre d'inspection [fin + x, fin + x + y]
|
||||||
|
ref_date = intervention.end_time or timezone.now()
|
||||||
|
min_days = sub_type.post_inspection_delay_min_days or 30
|
||||||
|
max_days = sub_type.post_inspection_delay_max_days or 60
|
||||||
|
scheduled_start_date = ref_date.date() + timedelta(days=min_days)
|
||||||
|
scheduled_due_date = ref_date.date() + timedelta(days=max_days)
|
||||||
|
|
||||||
|
# Détermination de la zone d'inspection
|
||||||
|
thematic = intervention.thematic
|
||||||
|
zones = InspectionZone.objects.filter(thematic=thematic, is_active=True) if thematic else InspectionZone.objects.none()
|
||||||
|
zone = None
|
||||||
|
|
||||||
|
# 1. Résolution spatiale par intersection / inclusion
|
||||||
|
if zones.exists():
|
||||||
|
if intervention.geom:
|
||||||
|
zone = zones.filter(geom__intersects=intervention.geom).first()
|
||||||
|
|
||||||
|
if not zone and intervention.lon and intervention.lat:
|
||||||
|
try:
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
p = Point(float(intervention.lon), float(intervention.lat), srid=4326)
|
||||||
|
p.transform(3812)
|
||||||
|
zone = zones.filter(geom__contains=p).first()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error resolving inspection zone via coordinates: {e}")
|
||||||
|
|
||||||
|
# 2. Résolution administrative par commune associée
|
||||||
|
if not zone:
|
||||||
|
for loc in intervention.locations.all():
|
||||||
|
loc_obj = loc.location
|
||||||
|
if loc_obj and hasattr(loc_obj, 'municipality') and loc_obj.municipality:
|
||||||
|
zone = zones.filter(municipalities=loc_obj.municipality).first()
|
||||||
|
if zone:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Détermination de la cible (Asset, Localisation ou Intervention elle-même)
|
||||||
|
target_ct = None
|
||||||
|
target_id = None
|
||||||
|
|
||||||
|
first_asset = intervention.interventionasset_set.first() if hasattr(intervention, 'interventionasset_set') else None
|
||||||
|
if first_asset and first_asset.content_type_id and first_asset.object_id:
|
||||||
|
target_ct = first_asset.content_type
|
||||||
|
target_id = first_asset.object_id
|
||||||
|
else:
|
||||||
|
first_loc = intervention.locations.first() if hasattr(intervention, 'locations') else None
|
||||||
|
if first_loc and first_loc.content_type_id and first_loc.object_id:
|
||||||
|
target_ct = first_loc.content_type
|
||||||
|
target_id = first_loc.object_id
|
||||||
|
else:
|
||||||
|
target_ct = ContentType.objects.get_for_model(intervention)
|
||||||
|
target_id = intervention.pk
|
||||||
|
|
||||||
|
# Pré-assignation si un seul inspecteur est assigné à la zone
|
||||||
|
assigned_inspector = None
|
||||||
|
if zone:
|
||||||
|
zone_inspectors = list(zone.inspectors.all()[:2])
|
||||||
|
if len(zone_inspectors) == 1:
|
||||||
|
assigned_inspector = zone_inspectors[0]
|
||||||
|
|
||||||
|
# Création de l'inspection planifiée
|
||||||
|
notes = f"Inspection de durabilité planifiée pour {sub_type.get_name()} (Intervention {intervention.code})."
|
||||||
|
inspection = Inspection.objects.create(
|
||||||
|
asset_content_type=target_ct,
|
||||||
|
asset_object_id=target_id,
|
||||||
|
checked_intervention=intervention,
|
||||||
|
nature=sub_type.post_inspection_nature,
|
||||||
|
status='scheduled',
|
||||||
|
result_status='pending',
|
||||||
|
scheduled_start_date=scheduled_start_date,
|
||||||
|
scheduled_due_date=scheduled_due_date,
|
||||||
|
zone=zone,
|
||||||
|
inspector=assigned_inspector,
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
logger.info(f"Scheduled post-intervention inspection {inspection.pk} created for intervention {intervention.code}")
|
||||||
|
return inspection
|
||||||
|
|
@ -530,21 +530,47 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
document.getElementById('edit_insp_object_id').value = data.object_id;
|
document.getElementById('edit_insp_object_id').value = data.object_id;
|
||||||
document.getElementById('edit_insp_target_display').textContent = data.target_display || '';
|
document.getElementById('edit_insp_target_display').textContent = data.target_display || '';
|
||||||
|
|
||||||
let datesText = "{% translate 'Créée le' %} " + (data.created_at || data.inspection_date);
|
if (data.is_scheduled) {
|
||||||
if (data.updated_at) {
|
const titleModal = document.getElementById('editInspectionModalLabel');
|
||||||
datesText += " | {% translate 'Dernière modification :' %} " + data.updated_at;
|
if (titleModal) {
|
||||||
|
titleModal.innerHTML = `<i class="bi bi-clipboard-check text-success me-2"></i>{% translate "Réaliser l'inspection" %}`;
|
||||||
|
}
|
||||||
|
let schedInfo = `{% translate "Fenêtre" %} : ${data.scheduled_start_date} → ${data.scheduled_due_date}`;
|
||||||
|
if (data.zone_name) {
|
||||||
|
schedInfo += ` | {% translate "Zone" %} : ${data.zone_name}`;
|
||||||
|
}
|
||||||
|
if (data.checked_intervention_code) {
|
||||||
|
schedInfo += ` | ${data.checked_intervention_code}`;
|
||||||
|
}
|
||||||
|
document.getElementById('edit_insp_dates_info').textContent = schedInfo;
|
||||||
|
document.getElementById('edit_insp_current_status').textContent = data.timeliness_label ? `${data.status_display} (${data.timeliness_label})` : data.status_display;
|
||||||
|
document.getElementById('edit_insp_current_status').className = `badge ${data.timeliness_badge_class || 'bg-primary'}`;
|
||||||
|
} else {
|
||||||
|
const titleModal = document.getElementById('editInspectionModalLabel');
|
||||||
|
if (titleModal) {
|
||||||
|
titleModal.innerHTML = `<i class="bi bi-pencil-square text-primary me-2"></i>{% translate "Modifier l'inspection" %}`;
|
||||||
|
}
|
||||||
|
let datesText = "{% translate 'Créée le' %} " + (data.created_at || data.inspection_date);
|
||||||
|
if (data.updated_at) {
|
||||||
|
datesText += " | {% translate 'Dernière modification :' %} " + data.updated_at;
|
||||||
|
}
|
||||||
|
document.getElementById('edit_insp_dates_info').textContent = datesText;
|
||||||
|
document.getElementById('edit_insp_current_status').textContent = data.result_status_display || data.result_status || '';
|
||||||
|
document.getElementById('edit_insp_current_status').className = 'badge bg-secondary';
|
||||||
}
|
}
|
||||||
document.getElementById('edit_insp_dates_info').textContent = datesText;
|
|
||||||
document.getElementById('edit_insp_current_status').textContent = data.result_status || '';
|
|
||||||
|
|
||||||
if (typeof window.toggleEditMissingResultChoice === 'function') {
|
if (typeof window.toggleEditMissingResultChoice === 'function') {
|
||||||
window.toggleEditMissingResultChoice(Boolean(data.is_zone_rwiz));
|
window.toggleEditMissingResultChoice(Boolean(data.is_zone_rwiz));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-check result_status radio
|
// Pre-check result_status radio
|
||||||
const rad = document.getElementById('edit_res_' + data.result_status);
|
let targetRadioCode = data.result_status;
|
||||||
|
if (data.is_scheduled && targetRadioCode === 'pending') {
|
||||||
|
targetRadioCode = 'compliant';
|
||||||
|
}
|
||||||
|
const rad = document.getElementById('edit_res_' + targetRadioCode);
|
||||||
if (rad) {
|
if (rad) {
|
||||||
if (data.is_zone_rwiz && data.result_status === 'missing') {
|
if (data.is_zone_rwiz && targetRadioCode === 'missing') {
|
||||||
const compRad = document.getElementById('edit_res_compliant');
|
const compRad = document.getElementById('edit_res_compliant');
|
||||||
if (compRad) compRad.checked = true;
|
if (compRad) compRad.checked = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,22 @@
|
||||||
|
|
||||||
<!-- Navigation par Onglets -->
|
<!-- Navigation par Onglets -->
|
||||||
<ul class="nav nav-tabs mb-3">
|
<ul class="nav nav-tabs mb-3">
|
||||||
|
{% if can_inspect %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if filter_type == 'scheduled' %}active fw-bold text-primary{% endif %} position-relative" href="?tab=scheduled">
|
||||||
|
<i class="bi bi-calendar2-check me-1"></i>{% translate "À réaliser" %}
|
||||||
|
{% if scheduled_count > 0 %}
|
||||||
|
<span class="badge rounded-pill bg-danger ms-1">{{ scheduled_count }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if filter_type == 'my' %}active fw-bold{% endif %}" href="?tab=my">
|
<a class="nav-link {% if filter_type == 'my' %}active fw-bold{% endif %}" href="?tab=my">
|
||||||
<i class="bi bi-person-check me-1"></i>{% translate "Mes inspections" %}
|
<i class="bi bi-person-check me-1"></i>{% translate "Mes inspections" %}
|
||||||
|
{% if my_count > 0 and filter_type != 'my' %}
|
||||||
|
<span class="badge rounded-pill bg-secondary ms-1">{{ my_count }}</span>
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
|
@ -72,6 +85,17 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if zones %}
|
||||||
|
<div class="col-12 col-sm-6 col-md-2">
|
||||||
|
<select name="zone" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||||
|
<option value="">{% translate "-- Districts / Zones --" %}</option>
|
||||||
|
{% for z in zones %}
|
||||||
|
<option value="{{ z.id }}" {% if selected_zone == z.id|stringformat:"s" %}selected{% endif %}>{{ z.name_fr }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="col-12 col-sm-6 col-md-2">
|
<div class="col-12 col-sm-6 col-md-2">
|
||||||
<select name="nature" class="form-select form-select-sm" onchange="this.form.submit()">
|
<select name="nature" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||||
<option value="">{% translate "-- Natures --" %}</option>
|
<option value="">{% translate "-- Natures --" %}</option>
|
||||||
|
|
@ -81,7 +105,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-sm-6 col-md-3">
|
<div class="col-12 col-sm-6 col-md-2">
|
||||||
<select name="status" class="form-select form-select-sm" onchange="this.form.submit()">
|
<select name="status" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||||
<option value="">{% translate "-- Tous les résultats --" %}</option>
|
<option value="">{% translate "-- Tous les résultats --" %}</option>
|
||||||
{% for val, label in result_choices %}
|
{% for val, label in result_choices %}
|
||||||
|
|
@ -90,7 +114,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-sm-6 col-md-2 d-flex gap-2">
|
<div class="col-12 col-sm-6 col-md-1 d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-sm btn-secondary flex-grow-1">{% translate "Filtrer" %}</button>
|
<button type="submit" class="btn btn-sm btn-secondary flex-grow-1">{% translate "Filtrer" %}</button>
|
||||||
<a href="?tab={{ filter_type }}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Réinitialiser' %}"><i class="bi bi-x-circle"></i></a>
|
<a href="?tab={{ filter_type }}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Réinitialiser' %}"><i class="bi bi-x-circle"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -128,14 +152,29 @@
|
||||||
<div class="card-body p-3 d-flex flex-column justify-content-between">
|
<div class="card-body p-3 d-flex flex-column justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<!-- Entête card : Thématique/Type + Badge résultat -->
|
<!-- Entête card : Thématique/Type + Badge résultat -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2 flex-wrap gap-1">
|
||||||
<span class="badge bg-light text-dark border d-inline-flex align-items-center me-2">
|
<div class="d-flex align-items-center gap-1">
|
||||||
<i class="bi {{ insp.thematic_icon }} me-2 text-primary"></i>
|
<span class="badge bg-light text-dark border d-inline-flex align-items-center">
|
||||||
{{ insp.asset_content_type.name }}
|
<i class="bi {{ insp.thematic_icon }} me-1 text-primary"></i>
|
||||||
</span>
|
{{ insp.asset_content_type.name }}
|
||||||
<span class="badge {{ insp.status_badge_class }}">
|
</span>
|
||||||
<i class="bi {{ insp.status_icon }} me-1"></i>{{ insp.get_result_status_display }}
|
{% if insp.zone %}
|
||||||
</span>
|
<span class="badge bg-light text-secondary border">
|
||||||
|
<i class="bi bi-geo me-1"></i>{{ insp.zone.name_fr }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if insp.is_scheduled %}
|
||||||
|
<span class="badge {{ insp.timeliness_badge_class }}">
|
||||||
|
<i class="bi bi-hourglass-split me-1"></i>{{ insp.timeliness_label }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge {{ insp.status_badge_class }}">
|
||||||
|
<i class="bi {{ insp.status_icon }} me-1"></i>{{ insp.get_result_status_display }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nom de la cible tronqué à 100 caractères -->
|
<!-- Nom de la cible tronqué à 100 caractères -->
|
||||||
|
|
@ -143,6 +182,20 @@
|
||||||
{{ insp.target_display|truncatechars:100 }}
|
{{ insp.target_display|truncatechars:100 }}
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
|
{% if insp.is_scheduled %}
|
||||||
|
<div class="alert alert-light border py-1 px-2 my-2 small text-muted d-flex align-items-center justify-content-between flex-wrap gap-1">
|
||||||
|
<span>
|
||||||
|
<i class="bi bi-calendar-range text-primary me-1"></i>
|
||||||
|
{% translate "Fenêtre" %} : {{ insp.scheduled_start_date|date:"d/m/Y" }} → <strong>{{ insp.scheduled_due_date|date:"d/m/Y" }}</strong>
|
||||||
|
</span>
|
||||||
|
{% if insp.checked_intervention %}
|
||||||
|
<span class="text-primary fw-semibold">
|
||||||
|
<i class="bi bi-tools me-1"></i>{{ insp.checked_intervention.code }}{% if insp.checked_intervention.sub_type %} ({{ insp.checked_intervention.sub_type.get_name }}){% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Résumé des constats si présent -->
|
<!-- Résumé des constats si présent -->
|
||||||
{% if insp.details_summary and insp.details_summary != "-" %}
|
{% if insp.details_summary and insp.details_summary != "-" %}
|
||||||
<p class="text-muted small mb-2 text-truncate mt-1">
|
<p class="text-muted small mb-2 text-truncate mt-1">
|
||||||
|
|
@ -151,8 +204,19 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ligne de pied card : Date & Inspecteur -->
|
<!-- Ligne de pied card : Date & Inspecteur / Action -->
|
||||||
<div class="d-flex justify-content-between align-items-center pt-2 border-top mt-2 text-muted extra-small">
|
<div class="d-flex justify-content-between align-items-center pt-2 border-top mt-2 text-muted extra-small">
|
||||||
|
{% if insp.is_scheduled %}
|
||||||
|
<span><i class="bi bi-clock-history me-1"></i>{% translate "Planifiée" %}</span>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
{% can_edit_inspection insp request.user as can_perform %}
|
||||||
|
{% if can_perform %}
|
||||||
|
<button type="button" class="btn btn-sm btn-success fw-bold py-1 px-2.5 shadow-sm" onclick="event.stopPropagation(); openEditInspectionModal({{ insp.id }});">
|
||||||
|
<i class="bi bi-clipboard-check me-1"></i>{% translate "Réaliser" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
<span><i class="bi bi-clock me-1"></i>{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
<span><i class="bi bi-clock me-1"></i>{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
{% if insp.documents.all %}
|
{% if insp.documents.all %}
|
||||||
|
|
@ -164,6 +228,7 @@
|
||||||
<span><i class="bi bi-person me-1"></i>{{ insp.inspector|short_name|default:insp.inspector.username }}</span>
|
<span><i class="bi bi-person me-1"></i>{{ insp.inspector|short_name|default:insp.inspector.username }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -206,6 +271,30 @@
|
||||||
<span class="text-muted"><i class="bi bi-layers me-1"></i>{% translate "Type d'asset" %}</span>
|
<span class="text-muted"><i class="bi bi-layers me-1"></i>{% translate "Type d'asset" %}</span>
|
||||||
<span class="badge bg-secondary">{{ insp.asset_content_type.name }}</span>
|
<span class="badge bg-secondary">{{ insp.asset_content_type.name }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
{% if insp.is_scheduled %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
|
<span class="text-muted"><i class="bi bi-hourglass-split me-1"></i>{% translate "Statut" %}</span>
|
||||||
|
<span class="badge {{ insp.timeliness_badge_class }}">{{ insp.get_status_display }} ({{ insp.timeliness_label }})</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
|
<span class="text-muted"><i class="bi bi-calendar-range me-1"></i>{% translate "Fenêtre d'inspection" %}</span>
|
||||||
|
<span class="fw-semibold">{{ insp.scheduled_start_date|date:"d/m/Y" }} → {{ insp.scheduled_due_date|date:"d/m/Y" }}</span>
|
||||||
|
</li>
|
||||||
|
{% if insp.zone %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
|
<span class="text-muted"><i class="bi bi-geo me-1"></i>{% translate "District / Zone" %}</span>
|
||||||
|
<span class="fw-semibold">{{ insp.zone.name_fr }}</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if insp.checked_intervention %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
|
<span class="text-muted"><i class="bi bi-tools me-1"></i>{% translate "Intervention liée" %}</span>
|
||||||
|
<a href="/interventions/{{ insp.checked_intervention.id }}/" class="fw-bold text-decoration-none">
|
||||||
|
{{ insp.checked_intervention.code }} {% if insp.checked_intervention.sub_type %}({{ insp.checked_intervention.sub_type.get_name }}){% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
<span class="text-muted"><i class="bi bi-calendar-event me-1"></i>{% translate "Date & Heure" %}</span>
|
<span class="text-muted"><i class="bi bi-calendar-event me-1"></i>{% translate "Date & Heure" %}</span>
|
||||||
<span class="fw-semibold">{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
<span class="fw-semibold">{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
||||||
|
|
@ -214,6 +303,7 @@
|
||||||
<span class="text-muted"><i class="bi bi-shield-check me-1"></i>{% translate "Résultat du contrôle" %}</span>
|
<span class="text-muted"><i class="bi bi-shield-check me-1"></i>{% translate "Résultat du contrôle" %}</span>
|
||||||
<span class="badge {{ insp.status_badge_class }}"><i class="bi {{ insp.status_icon }} me-1"></i>{{ insp.get_result_status_display }}</span>
|
<span class="badge {{ insp.status_badge_class }}"><i class="bi {{ insp.status_icon }} me-1"></i>{{ insp.get_result_status_display }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
<span class="text-muted"><i class="bi bi-eye me-1"></i>{% translate "Nature" %}</span>
|
<span class="text-muted"><i class="bi bi-eye me-1"></i>{% translate "Nature" %}</span>
|
||||||
<span class="badge bg-light text-dark border">{{ insp.get_nature_display }}</span>
|
<span class="badge bg-light text-dark border">{{ insp.get_nature_display }}</span>
|
||||||
|
|
@ -307,6 +397,11 @@
|
||||||
<div class="modal-footer bg-light py-2">
|
<div class="modal-footer bg-light py-2">
|
||||||
{% can_edit_inspection insp request.user as user_can_edit %}
|
{% can_edit_inspection insp request.user as user_can_edit %}
|
||||||
{% if user_can_edit %}
|
{% if user_can_edit %}
|
||||||
|
{% if insp.is_scheduled %}
|
||||||
|
<button type="button" class="btn btn-success btn-sm fw-bold px-3" onclick="openEditInspectionModal({{ insp.id }})" data-bs-dismiss="modal">
|
||||||
|
<i class="bi bi-clipboard-check me-1"></i>{% translate "Réaliser le contrôle" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% if insp.is_cancelled %}
|
{% if insp.is_cancelled %}
|
||||||
<button type="button" class="btn btn-outline-success btn-sm fw-semibold" onclick="handleToggleCancelInspection({{ insp.id }}, 'restore')">
|
<button type="button" class="btn btn-outline-success btn-sm fw-semibold" onclick="handleToggleCancelInspection({{ insp.id }}, 'restore')">
|
||||||
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Rétablir l'inspection" %}
|
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Rétablir l'inspection" %}
|
||||||
|
|
@ -316,9 +411,11 @@
|
||||||
<i class="bi bi-x-circle me-1"></i>{% translate "Annuler l'inspection" %}
|
<i class="bi bi-x-circle me-1"></i>{% translate "Annuler l'inspection" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not insp.is_scheduled %}
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm fw-semibold" onclick="openEditInspectionModal({{ insp.id }})" data-bs-dismiss="modal">
|
<button type="button" class="btn btn-outline-primary btn-sm fw-semibold" onclick="openEditInspectionModal({{ insp.id }})" data-bs-dismiss="modal">
|
||||||
<i class="bi bi-pencil me-1"></i>{% translate "Modifier l'inspection" %}
|
<i class="bi bi-pencil me-1"></i>{% translate "Modifier l'inspection" %}
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1481,3 +1481,153 @@ class GiepInspectionStatusCalculationTestCase(TestCase):
|
||||||
self.assertEqual(GiepInspection.compute_status_from_data(data_mix2), 'damaged')
|
self.assertEqual(GiepInspection.compute_status_from_data(data_mix2), 'damaged')
|
||||||
|
|
||||||
|
|
||||||
|
class PostInterventionDurabilityInspectionTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from datetime import timedelta
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username='road_inspector',
|
||||||
|
password='password123',
|
||||||
|
first_name='Pierre',
|
||||||
|
last_name='Dupont'
|
||||||
|
)
|
||||||
|
from common.models import UserConfig, Role, UserThematics
|
||||||
|
inspector_role, _ = Role.objects.get_or_create(name='inspector')
|
||||||
|
config = UserConfig.objects.create(user=self.user)
|
||||||
|
config.roles.add(inspector_role)
|
||||||
|
|
||||||
|
self.roads_thematic, _ = Thematic.objects.get_or_create(
|
||||||
|
code='roads',
|
||||||
|
defaults={'name_fr': 'Voirie', 'name_nl': 'Wegen'}
|
||||||
|
)
|
||||||
|
UserThematics.objects.create(
|
||||||
|
user_config=config,
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
can_view_assets=True,
|
||||||
|
can_edit_assets=True,
|
||||||
|
can_inspect=True
|
||||||
|
)
|
||||||
|
|
||||||
|
from notifications.models import Team
|
||||||
|
self.team = Team.objects.create(name='Équipe Contrôle Enrobés')
|
||||||
|
self.team.members.add(self.user)
|
||||||
|
|
||||||
|
from interventions.models import InterventionSubType
|
||||||
|
self.hotbox_subtype = InterventionSubType.objects.create(
|
||||||
|
code='test_roads_repair_hotbox',
|
||||||
|
name_fr='Hot Box (asphalte à chaud)',
|
||||||
|
name_nl='Hot Box (warm asfalt)',
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
intervention_type='repair',
|
||||||
|
requires_post_inspection=True,
|
||||||
|
post_inspection_delay_min_days=30,
|
||||||
|
post_inspection_delay_max_days=60,
|
||||||
|
post_inspection_nature='post_intervention'
|
||||||
|
)
|
||||||
|
self.hotbox_subtype.notification_teams.add(self.team)
|
||||||
|
|
||||||
|
self.standard_subtype = InterventionSubType.objects.create(
|
||||||
|
code='test_roads_repair_standard',
|
||||||
|
name_fr='Réparation standard',
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
intervention_type='repair',
|
||||||
|
requires_post_inspection=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_hotbox_intervention_finished_triggers_inspection(self):
|
||||||
|
from datetime import timedelta
|
||||||
|
now = timezone.now()
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
type='repair',
|
||||||
|
sub_type=self.hotbox_subtype,
|
||||||
|
status='in_progress',
|
||||||
|
end_time=now
|
||||||
|
)
|
||||||
|
self.assertEqual(Inspection.objects.filter(checked_intervention=intervention).count(), 0)
|
||||||
|
|
||||||
|
# Passage au statut finished
|
||||||
|
intervention.status = 'finished'
|
||||||
|
intervention.save()
|
||||||
|
|
||||||
|
self.assertEqual(Inspection.objects.filter(checked_intervention=intervention).count(), 1)
|
||||||
|
insp = Inspection.objects.get(checked_intervention=intervention)
|
||||||
|
self.assertEqual(insp.status, 'scheduled')
|
||||||
|
self.assertEqual(insp.result_status, 'pending')
|
||||||
|
self.assertEqual(insp.nature, 'post_intervention')
|
||||||
|
self.assertEqual(insp.scheduled_start_date, now.date() + timedelta(days=30))
|
||||||
|
self.assertEqual(insp.scheduled_due_date, now.date() + timedelta(days=60))
|
||||||
|
self.assertTrue(insp.is_scheduled)
|
||||||
|
self.assertEqual(insp.timeliness, 'future')
|
||||||
|
|
||||||
|
def test_zone_and_single_inspector_auto_assignment(self):
|
||||||
|
from common.models import Municipality
|
||||||
|
from interventions.models import InterventionLocation
|
||||||
|
from .models import InspectionZone
|
||||||
|
|
||||||
|
zone = InspectionZone.objects.create(
|
||||||
|
code='DIST_NORD_TEST',
|
||||||
|
name_fr='District Nord Test',
|
||||||
|
thematic=self.roads_thematic
|
||||||
|
)
|
||||||
|
zone.inspectors.add(self.user)
|
||||||
|
muni = Municipality.objects.create(name_fr='Schaerbeek Test', code='21099')
|
||||||
|
zone.municipalities.add(muni)
|
||||||
|
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
type='repair',
|
||||||
|
sub_type=self.hotbox_subtype,
|
||||||
|
status='in_progress',
|
||||||
|
end_time=timezone.now()
|
||||||
|
)
|
||||||
|
InterventionLocation.objects.create(
|
||||||
|
intervention=intervention,
|
||||||
|
content_type=ContentType.objects.get_for_model(muni),
|
||||||
|
object_id=muni.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
intervention.status = 'finished'
|
||||||
|
intervention.save()
|
||||||
|
|
||||||
|
insp = Inspection.objects.get(checked_intervention=intervention)
|
||||||
|
self.assertEqual(insp.zone, zone)
|
||||||
|
# Inspecteur unique pré-assigné
|
||||||
|
self.assertEqual(insp.inspector, self.user)
|
||||||
|
self.assertTrue(insp.can_edit(self.user))
|
||||||
|
|
||||||
|
def test_non_compliance_triggers_team_notification(self):
|
||||||
|
from notifications.models import TeamNotificationTarget
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
type='repair',
|
||||||
|
sub_type=self.hotbox_subtype,
|
||||||
|
status='finished',
|
||||||
|
end_time=timezone.now()
|
||||||
|
)
|
||||||
|
insp = Inspection.objects.get(checked_intervention=intervention)
|
||||||
|
self.assertFalse(insp.non_compliance_notified)
|
||||||
|
|
||||||
|
# Réalisation avec non-conformité
|
||||||
|
insp.status = 'completed'
|
||||||
|
insp.result_status = 'non_compliant'
|
||||||
|
insp.notes = 'Fissures apparues sur le joint de raccordement.'
|
||||||
|
insp.save()
|
||||||
|
|
||||||
|
insp.refresh_from_db()
|
||||||
|
self.assertTrue(insp.non_compliance_notified)
|
||||||
|
self.assertTrue(TeamNotificationTarget.objects.filter(team=self.team).exists())
|
||||||
|
|
||||||
|
def test_subtype_without_post_inspection_does_not_trigger(self):
|
||||||
|
intervention = Intervention.objects.create(
|
||||||
|
thematic=self.roads_thematic,
|
||||||
|
type='repair',
|
||||||
|
sub_type=self.standard_subtype,
|
||||||
|
status='in_progress',
|
||||||
|
end_time=timezone.now()
|
||||||
|
)
|
||||||
|
intervention.status = 'finished'
|
||||||
|
intervention.save()
|
||||||
|
|
||||||
|
self.assertEqual(Inspection.objects.filter(checked_intervention=intervention).count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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, ParkingSpotInspection, GiepInspection, SignPanelInspection, InspectionDocument,
|
Inspection, InspectionZone, ParkingSpotInspection, GiepInspection, SignPanelInspection, InspectionDocument,
|
||||||
INSPECTION_RESULT_CHOICES, INSPECTION_NATURE_CHOICES, INSPECTOR_ROLE_CHOICES
|
INSPECTION_RESULT_CHOICES, INSPECTION_NATURE_CHOICES, INSPECTOR_ROLE_CHOICES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -139,18 +139,52 @@ def _filter_inspections_queryset(request):
|
||||||
|
|
||||||
qs = qs.filter(combined)
|
qs = qs.filter(combined)
|
||||||
|
|
||||||
statuses = request.GET.getlist('status') or request.GET.getlist('result_status')
|
lifecycle_statuses = request.GET.getlist('lifecycle_status')
|
||||||
if statuses:
|
result_statuses = request.GET.getlist('result_status')
|
||||||
qs = qs.filter(result_status__in=statuses)
|
raw_statuses = request.GET.getlist('status')
|
||||||
|
for s in raw_statuses:
|
||||||
|
if s in ('scheduled', 'in_progress', 'completed', 'cancelled'):
|
||||||
|
if s not in lifecycle_statuses:
|
||||||
|
lifecycle_statuses.append(s)
|
||||||
|
else:
|
||||||
|
if s not in result_statuses:
|
||||||
|
result_statuses.append(s)
|
||||||
|
|
||||||
|
if lifecycle_statuses:
|
||||||
|
qs = qs.filter(status__in=lifecycle_statuses)
|
||||||
|
if result_statuses:
|
||||||
|
qs = qs.filter(result_status__in=result_statuses)
|
||||||
|
|
||||||
natures = request.GET.getlist('nature')
|
natures = request.GET.getlist('nature')
|
||||||
if natures:
|
if natures:
|
||||||
qs = qs.filter(nature__in=natures)
|
qs = qs.filter(nature__in=natures)
|
||||||
|
|
||||||
|
zone_id = request.GET.get('zone') or request.GET.get('zone_id')
|
||||||
|
if zone_id:
|
||||||
|
qs = qs.filter(zone_id=zone_id)
|
||||||
|
|
||||||
show_mine_only = request.GET.get('mine') == '1' or request.GET.get('tab') == 'my'
|
show_mine_only = request.GET.get('mine') == '1' or request.GET.get('tab') == 'my'
|
||||||
if show_mine_only:
|
if show_mine_only:
|
||||||
qs = qs.filter(inspector=request.user)
|
qs = qs.filter(inspector=request.user)
|
||||||
|
|
||||||
|
if request.GET.get('mine_zones') == '1':
|
||||||
|
qs = qs.filter(Q(zone__inspectors=request.user) | Q(inspector=request.user))
|
||||||
|
|
||||||
|
timeliness = request.GET.get('timeliness')
|
||||||
|
if timeliness:
|
||||||
|
today = timezone.now().date()
|
||||||
|
from datetime import timedelta
|
||||||
|
if timeliness == 'overdue':
|
||||||
|
qs = qs.filter(status='scheduled', scheduled_due_date__lt=today)
|
||||||
|
elif timeliness == 'urgent':
|
||||||
|
qs = qs.filter(status='scheduled', scheduled_due_date__gte=today, scheduled_due_date__lte=today + timedelta(days=7))
|
||||||
|
elif timeliness == 'current':
|
||||||
|
qs = qs.filter(status='scheduled', scheduled_due_date__gte=today).filter(
|
||||||
|
Q(scheduled_start_date__isnull=True) | Q(scheduled_start_date__lte=today)
|
||||||
|
)
|
||||||
|
elif timeliness == 'future':
|
||||||
|
qs = qs.filter(status='scheduled', scheduled_start_date__gt=today)
|
||||||
|
|
||||||
date_from = request.GET.get('date_from', '').strip()
|
date_from = request.GET.get('date_from', '').strip()
|
||||||
date_to = request.GET.get('date_to', '').strip()
|
date_to = request.GET.get('date_to', '').strip()
|
||||||
if date_from:
|
if date_from:
|
||||||
|
|
@ -166,7 +200,9 @@ def _filter_inspections_queryset(request):
|
||||||
if checked_interv_id:
|
if checked_interv_id:
|
||||||
qs = qs.filter(checked_intervention_id=checked_interv_id)
|
qs = qs.filter(checked_intervention_id=checked_interv_id)
|
||||||
|
|
||||||
return qs.order_by('-inspection_date')
|
if lifecycle_statuses == ['scheduled']:
|
||||||
|
return qs.order_by('scheduled_due_date', 'scheduled_start_date', '-id')
|
||||||
|
return qs.order_by('-inspection_date', '-id')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|
@ -183,24 +219,42 @@ def inspections_list(request):
|
||||||
can_inspect = bool(insp_thematics and insp_thematics.exists())
|
can_inspect = bool(insp_thematics and insp_thematics.exists())
|
||||||
|
|
||||||
if is_mobile:
|
if is_mobile:
|
||||||
default_tab = 'my' if can_inspect else 'all'
|
from .permissions import filter_viewable_inspections_for_user
|
||||||
|
base_viewable = filter_viewable_inspections_for_user(request.user)
|
||||||
|
|
||||||
|
# Compteurs rapides pour les onglets
|
||||||
|
scheduled_count = base_viewable.filter(status='scheduled').filter(
|
||||||
|
Q(inspector=request.user) | Q(zone__inspectors=request.user) | Q(inspector__isnull=True)
|
||||||
|
).count()
|
||||||
|
my_count = base_viewable.filter(inspector=request.user).exclude(status='scheduled').count()
|
||||||
|
|
||||||
|
default_tab = 'scheduled' if (can_inspect and scheduled_count > 0) else ('my' if can_inspect else 'all')
|
||||||
filter_type = request.GET.get('tab', default_tab)
|
filter_type = request.GET.get('tab', default_tab)
|
||||||
thematic_code = request.GET.get('thematic', '')
|
thematic_code = request.GET.get('thematic', '')
|
||||||
status_filter = request.GET.get('status', '')
|
status_filter = request.GET.get('status', '')
|
||||||
nature_filter = request.GET.get('nature', '')
|
nature_filter = request.GET.get('nature', '')
|
||||||
|
zone_filter = request.GET.get('zone', '')
|
||||||
mission_id = request.GET.get('mission_id', '')
|
mission_id = request.GET.get('mission_id', '')
|
||||||
checked_interv_id = request.GET.get('checked_interv_id', '')
|
checked_interv_id = request.GET.get('checked_interv_id', '')
|
||||||
search_q = request.GET.get('q', '').strip()
|
search_q = request.GET.get('q', '').strip()
|
||||||
|
|
||||||
from .permissions import filter_viewable_inspections_for_user
|
qs = base_viewable.select_related(
|
||||||
qs = filter_viewable_inspections_for_user(request.user)
|
|
||||||
qs = qs.select_related(
|
|
||||||
'asset_content_type', 'inspector', 'mission_intervention', 'checked_intervention',
|
'asset_content_type', 'inspector', 'mission_intervention', 'checked_intervention',
|
||||||
'parkingspotinspection', 'giepinspection', 'signpanelinspection'
|
'zone', 'parkingspotinspection', 'giepinspection', 'signpanelinspection'
|
||||||
).prefetch_related('documents').order_by('-inspection_date')
|
).prefetch_related('documents')
|
||||||
|
|
||||||
if filter_type == 'my':
|
if filter_type == 'scheduled':
|
||||||
qs = qs.filter(inspector=request.user)
|
qs = qs.filter(status='scheduled')
|
||||||
|
if request.GET.get('all_zones') != '1':
|
||||||
|
qs = qs.filter(Q(inspector=request.user) | Q(zone__inspectors=request.user) | Q(inspector__isnull=True))
|
||||||
|
qs = qs.order_by('scheduled_due_date', 'scheduled_start_date', '-id')
|
||||||
|
elif filter_type == 'my':
|
||||||
|
qs = qs.filter(inspector=request.user).exclude(status='scheduled').order_by('-inspection_date', '-id')
|
||||||
|
else:
|
||||||
|
qs = qs.order_by('-inspection_date', '-id')
|
||||||
|
|
||||||
|
if zone_filter:
|
||||||
|
qs = qs.filter(zone_id=zone_filter)
|
||||||
|
|
||||||
if thematic_code:
|
if thematic_code:
|
||||||
try:
|
try:
|
||||||
|
|
@ -224,7 +278,10 @@ def inspections_list(request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if status_filter:
|
if status_filter:
|
||||||
qs = qs.filter(result_status=status_filter)
|
if status_filter in ('scheduled', 'in_progress', 'completed', 'cancelled'):
|
||||||
|
qs = qs.filter(status=status_filter)
|
||||||
|
else:
|
||||||
|
qs = qs.filter(result_status=status_filter)
|
||||||
|
|
||||||
if nature_filter:
|
if nature_filter:
|
||||||
qs = qs.filter(nature=nature_filter)
|
qs = qs.filter(nature=nature_filter)
|
||||||
|
|
@ -270,15 +327,23 @@ def inspections_list(request):
|
||||||
|
|
||||||
qs = qs.filter(search_filter)
|
qs = qs.filter(search_filter)
|
||||||
|
|
||||||
|
zones = InspectionZone.objects.filter(is_active=True).order_by('thematic__name_fr', 'name_fr')
|
||||||
|
if thematics.exists():
|
||||||
|
zones = zones.filter(thematic__in=thematics)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'base_template': "mobile/mobile_base.html",
|
'base_template': "mobile/mobile_base.html",
|
||||||
'is_mobile': True,
|
'is_mobile': True,
|
||||||
'inspections': qs[:100],
|
'inspections': qs[:100],
|
||||||
'total_count': qs.count(),
|
'total_count': qs.count(),
|
||||||
'filter_type': filter_type,
|
'filter_type': filter_type,
|
||||||
|
'scheduled_count': scheduled_count,
|
||||||
|
'my_count': my_count,
|
||||||
'selected_thematic': thematic_code,
|
'selected_thematic': thematic_code,
|
||||||
'selected_status': status_filter,
|
'selected_status': status_filter,
|
||||||
'selected_nature': nature_filter,
|
'selected_nature': nature_filter,
|
||||||
|
'selected_zone': zone_filter,
|
||||||
|
'zones': zones,
|
||||||
'selected_mission_id': mission_id,
|
'selected_mission_id': mission_id,
|
||||||
'selected_checked_interv_id': checked_interv_id,
|
'selected_checked_interv_id': checked_interv_id,
|
||||||
'search_q': search_q,
|
'search_q': search_q,
|
||||||
|
|
@ -314,6 +379,7 @@ def inspections_list(request):
|
||||||
selected_categories = request.GET.getlist('category')
|
selected_categories = request.GET.getlist('category')
|
||||||
selected_statuses = request.GET.getlist('status') or request.GET.getlist('result_status')
|
selected_statuses = request.GET.getlist('status') or request.GET.getlist('result_status')
|
||||||
selected_natures = request.GET.getlist('nature')
|
selected_natures = request.GET.getlist('nature')
|
||||||
|
selected_zone = request.GET.get('zone', '')
|
||||||
search_query = request.GET.get('q', '')
|
search_query = request.GET.get('q', '')
|
||||||
show_mine_only = request.GET.get('mine') == '1' or request.GET.get('tab') == 'my'
|
show_mine_only = request.GET.get('mine') == '1' or request.GET.get('tab') == 'my'
|
||||||
date_from = request.GET.get('date_from', '')
|
date_from = request.GET.get('date_from', '')
|
||||||
|
|
@ -337,6 +403,10 @@ def inspections_list(request):
|
||||||
for cfg in result_choices_config
|
for cfg in result_choices_config
|
||||||
]
|
]
|
||||||
|
|
||||||
|
zones = InspectionZone.objects.filter(is_active=True).order_by('thematic__name_fr', 'name_fr')
|
||||||
|
if thematics.exists():
|
||||||
|
zones = zones.filter(thematic__in=thematics)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'thematic_choices': [(t.code, t.name, t.icon or 'bi-geo-alt') for t in thematics],
|
'thematic_choices': [(t.code, t.name, t.icon or 'bi-geo-alt') for t in thematics],
|
||||||
'selected_thematics': selected_thematics,
|
'selected_thematics': selected_thematics,
|
||||||
|
|
@ -348,6 +418,8 @@ def inspections_list(request):
|
||||||
'selected_result_statuses': selected_statuses,
|
'selected_result_statuses': selected_statuses,
|
||||||
'nature_choices': INSPECTION_NATURE_CHOICES,
|
'nature_choices': INSPECTION_NATURE_CHOICES,
|
||||||
'selected_natures': selected_natures,
|
'selected_natures': selected_natures,
|
||||||
|
'zones': zones,
|
||||||
|
'selected_zone': selected_zone,
|
||||||
'search_query': search_query,
|
'search_query': search_query,
|
||||||
'show_mine_only': show_mine_only,
|
'show_mine_only': show_mine_only,
|
||||||
'date_from': date_from,
|
'date_from': date_from,
|
||||||
|
|
@ -779,6 +851,19 @@ def get_inspection_detail_api(request, inspection_id):
|
||||||
'status_icon': leaf.status_icon,
|
'status_icon': leaf.status_icon,
|
||||||
'nature': leaf.nature,
|
'nature': leaf.nature,
|
||||||
'nature_display': str(leaf.get_nature_display()),
|
'nature_display': str(leaf.get_nature_display()),
|
||||||
|
'status': leaf.status,
|
||||||
|
'status_display': str(leaf.get_status_display()),
|
||||||
|
'is_scheduled': leaf.is_scheduled,
|
||||||
|
'scheduled_start_date': leaf.scheduled_start_date.strftime('%d/%m/%Y') if leaf.scheduled_start_date else '',
|
||||||
|
'scheduled_due_date': leaf.scheduled_due_date.strftime('%d/%m/%Y') if leaf.scheduled_due_date else '',
|
||||||
|
'zone_id': leaf.zone_id,
|
||||||
|
'zone_name': leaf.zone.get_name() if leaf.zone else '',
|
||||||
|
'timeliness': leaf.timeliness,
|
||||||
|
'timeliness_label': str(leaf.timeliness_label) if leaf.timeliness_label else '',
|
||||||
|
'timeliness_badge_class': leaf.timeliness_badge_class,
|
||||||
|
'checked_intervention_code': leaf.checked_intervention.code if leaf.checked_intervention else '',
|
||||||
|
'checked_intervention_id': leaf.checked_intervention_id,
|
||||||
|
'checked_intervention_subtype': leaf.checked_intervention.sub_type.get_name() if leaf.checked_intervention and leaf.checked_intervention.sub_type else '',
|
||||||
'inspector_name': get_short_name(leaf.inspector) if leaf.inspector else '-',
|
'inspector_name': get_short_name(leaf.inspector) if leaf.inspector else '-',
|
||||||
'inspector_role': leaf.inspector_role,
|
'inspector_role': leaf.inspector_role,
|
||||||
'inspector_role_display': str(leaf.get_inspector_role_display()) if leaf.inspector_role else '',
|
'inspector_role_display': str(leaf.get_inspector_role_display()) if leaf.inspector_role else '',
|
||||||
|
|
@ -846,6 +931,14 @@ def update_inspection_api(request, inspection_id):
|
||||||
}, status=403)
|
}, status=403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if 'status' in request.POST:
|
||||||
|
inspection.status = request.POST['status']
|
||||||
|
elif inspection.status == 'scheduled' and 'result_status' in request.POST and request.POST['result_status'] != 'pending':
|
||||||
|
inspection.status = 'completed'
|
||||||
|
if not inspection.inspector_id or inspection.inspector != request.user:
|
||||||
|
inspection.inspector = request.user
|
||||||
|
inspection.inspection_date = timezone.now()
|
||||||
|
|
||||||
if 'result_status' in request.POST:
|
if 'result_status' in request.POST:
|
||||||
new_status = request.POST['result_status']
|
new_status = request.POST['result_status']
|
||||||
is_zone_rwiz = (
|
is_zone_rwiz = (
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
openpyxl = None
|
openpyxl = None
|
||||||
|
|
||||||
from .models import Intervention, InterventionDocument, InterventionNote, InterventionLocation, InterventionOccupation
|
from .models import (
|
||||||
|
Intervention, InterventionSubType, InterventionDocument, InterventionNote,
|
||||||
|
InterventionLocation, InterventionOccupation
|
||||||
|
)
|
||||||
from. models import InterventionPlanificationTimeLine, InterventionRealizationTimeLine
|
from. models import InterventionPlanificationTimeLine, InterventionRealizationTimeLine
|
||||||
from .models import Symptom, SymptomRecommendation, ReflexSheet, SourceCategory, SymptomContractAutoClose
|
from .models import Symptom, SymptomRecommendation, ReflexSheet, SourceCategory, SymptomContractAutoClose
|
||||||
from .models import (
|
from .models import (
|
||||||
|
|
@ -42,10 +45,23 @@ from interventions.views.preventive_import import process_worksheet as _process_
|
||||||
from interventions.models import STATUS_CHOICES, DirectContractRouting
|
from interventions.models import STATUS_CHOICES, DirectContractRouting
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(InterventionSubType)
|
||||||
|
class InterventionSubTypeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"code", "name_fr", "thematic", "intervention_type",
|
||||||
|
"requires_post_inspection", "post_inspection_delay_min_days",
|
||||||
|
"post_inspection_delay_max_days", "is_active"
|
||||||
|
)
|
||||||
|
list_filter = ("thematic", "intervention_type", "requires_post_inspection", "is_active")
|
||||||
|
search_fields = ("code", "name_fr", "name_nl")
|
||||||
|
filter_horizontal = ("notification_teams",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Intervention)
|
@admin.register(Intervention)
|
||||||
class InterventionAdmin(admin.ModelAdmin):
|
class InterventionAdmin(admin.ModelAdmin):
|
||||||
search_fields = ("code", "title")
|
search_fields = ("code", "title")
|
||||||
raw_id_fields = ("intervention_manager",)
|
list_filter = ("thematic", "type", "sub_type", "status", "maintain_type")
|
||||||
|
raw_id_fields = ("intervention_manager", "sub_type")
|
||||||
|
|
||||||
change_list_template = "admin/interventions/intervention_changelist.html"
|
change_list_template = "admin/interventions/intervention_changelist.html"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-16 14:17
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('common', '0014_userconfig_inspection_list_filters_and_more'),
|
||||||
|
('interventions', '0008_symptom_notification_recipients'),
|
||||||
|
('notifications', '0004_discussion_flag_discussion_flag_set_at_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InterventionSubType',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(max_length=50, unique=True, verbose_name='Code')),
|
||||||
|
('name_fr', models.CharField(max_length=200, verbose_name='Nom (FR)')),
|
||||||
|
('name_nl', models.CharField(blank=True, max_length=200, verbose_name='Nom (NL)')),
|
||||||
|
('intervention_type', models.CharField(choices=[('fixing', 'Dépannage'), ('repair', 'Réparation'), ('repair_post_preventive', 'Réparation post préventif'), ('replace', 'Remplacement'), ('care', 'Entretien'), ('control', 'Contrôle'), ('inspection', 'Inspection'), ('build', 'Construction'), ('collection', 'Ramassage'), ('reprogramming', 'Reprogrammation')], max_length=50, verbose_name="Type d'intervention")),
|
||||||
|
('requires_post_inspection', models.BooleanField(default=False, help_text="Si coché, une inspection de durabilité est automatiquement planifiée quand l'intervention passe à 'Terminé'.", verbose_name='Déclenche une inspection de durabilité')),
|
||||||
|
('post_inspection_delay_min_days', models.PositiveIntegerField(default=30, help_text="Nombre de jours minimum après fin d'exécution pour réaliser l'inspection (ex: 30 jours).", verbose_name='Délai début fenêtre (jours)')),
|
||||||
|
('post_inspection_delay_max_days', models.PositiveIntegerField(default=60, help_text="Nombre de jours maximum après fin d'exécution marquant la date limite d'inspection (ex: 60 jours).", verbose_name='Délai fin fenêtre / échéance (jours)')),
|
||||||
|
('post_inspection_nature', models.CharField(choices=[('visual', 'Inspection visuelle'), ('technical', 'Inspection technique'), ('post_intervention', 'Suivi / Qualité post-intervention')], default='post_intervention', max_length=50, verbose_name="Nature de l'inspection")),
|
||||||
|
('order_number', models.PositiveIntegerField(blank=True, null=True, verbose_name="Ordre d'affichage")),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Actif')),
|
||||||
|
('notification_teams', models.ManyToManyField(blank=True, help_text="Équipes notifiées si l'inspection de durabilité révèle un constat non-conforme.", related_name='intervention_subtypes', to='notifications.team', verbose_name='Équipes à notifier')),
|
||||||
|
('thematic', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='intervention_subtypes', to='common.thematic', verbose_name='Thématique')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': "Sous-type d'intervention",
|
||||||
|
'verbose_name_plural': "Sous-types d'interventions",
|
||||||
|
'ordering': ['thematic', 'intervention_type', 'order_number', 'name_fr'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='intervention',
|
||||||
|
name='sub_type',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Précise la technique ou le sous-type de travail (ex: Hot Box pour une réparation voirie).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interventions', to='interventions.interventionsubtype', verbose_name="Sous-type d'intervention"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def seed_hotbox_subtype(apps, schema_editor):
|
||||||
|
Thematic = apps.get_model('common', 'Thematic')
|
||||||
|
InterventionSubType = apps.get_model('interventions', 'InterventionSubType')
|
||||||
|
|
||||||
|
roads_thematic = Thematic.objects.filter(code='roads').first()
|
||||||
|
if roads_thematic:
|
||||||
|
InterventionSubType.objects.update_or_create(
|
||||||
|
code='roads_repair_hotbox',
|
||||||
|
defaults={
|
||||||
|
'name_fr': 'Hot Box (asphalte à chaud)',
|
||||||
|
'name_nl': 'Hot Box (warm asfalt)',
|
||||||
|
'thematic': roads_thematic,
|
||||||
|
'intervention_type': 'repair',
|
||||||
|
'requires_post_inspection': True,
|
||||||
|
'post_inspection_delay_min_days': 30,
|
||||||
|
'post_inspection_delay_max_days': 60,
|
||||||
|
'post_inspection_nature': 'post_intervention',
|
||||||
|
'order_number': 10,
|
||||||
|
'is_active': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unseed_hotbox_subtype(apps, schema_editor):
|
||||||
|
InterventionSubType = apps.get_model('interventions', 'InterventionSubType')
|
||||||
|
InterventionSubType.objects.filter(code='roads_repair_hotbox').delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('interventions', '0009_interventionsubtype_intervention_sub_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(seed_hotbox_subtype, reverse_code=unseed_hotbox_subtype),
|
||||||
|
]
|
||||||
|
|
@ -185,6 +185,71 @@ SYMPTOM_MAINTENANCE_TYPE_CHOICES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
INSPECTION_NATURE_CHOICES = (
|
||||||
|
('visual', _('Inspection visuelle')),
|
||||||
|
('technical', _('Inspection technique')),
|
||||||
|
('post_intervention', _('Suivi / Qualité post-intervention')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InterventionSubType(models.Model):
|
||||||
|
"""
|
||||||
|
Sous-type de travaux d'intervention (ex: Roads -> Réparation -> Hot Box).
|
||||||
|
Permet de configurer les règles d'inspection de durabilité post-travaux et les équipes à notifier en cas de non-conformité.
|
||||||
|
"""
|
||||||
|
code = models.CharField(max_length=50, unique=True, verbose_name=_("Code"))
|
||||||
|
name_fr = models.CharField(max_length=200, verbose_name=_("Nom (FR)"))
|
||||||
|
name_nl = models.CharField(max_length=200, blank=True, verbose_name=_("Nom (NL)"))
|
||||||
|
thematic = models.ForeignKey(Thematic, on_delete=models.PROTECT, related_name='intervention_subtypes', verbose_name=_("Thématique"))
|
||||||
|
intervention_type = models.CharField(max_length=50, choices=TYPE_CHOICES, verbose_name=_("Type d'intervention"))
|
||||||
|
|
||||||
|
# Paramètres de durabilité / inspection post-travaux
|
||||||
|
requires_post_inspection = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("Déclenche une inspection de durabilité"),
|
||||||
|
help_text=_("Si coché, une inspection de durabilité est automatiquement planifiée quand l'intervention passe à 'Terminé'.")
|
||||||
|
)
|
||||||
|
post_inspection_delay_min_days = models.PositiveIntegerField(
|
||||||
|
default=30,
|
||||||
|
verbose_name=_("Délai début fenêtre (jours)"),
|
||||||
|
help_text=_("Nombre de jours minimum après fin d'exécution pour réaliser l'inspection (ex: 30 jours).")
|
||||||
|
)
|
||||||
|
post_inspection_delay_max_days = models.PositiveIntegerField(
|
||||||
|
default=60,
|
||||||
|
verbose_name=_("Délai fin fenêtre / échéance (jours)"),
|
||||||
|
help_text=_("Nombre de jours maximum après fin d'exécution marquant la date limite d'inspection (ex: 60 jours).")
|
||||||
|
)
|
||||||
|
post_inspection_nature = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=INSPECTION_NATURE_CHOICES,
|
||||||
|
default='post_intervention',
|
||||||
|
verbose_name=_("Nature de l'inspection")
|
||||||
|
)
|
||||||
|
notification_teams = models.ManyToManyField(
|
||||||
|
'notifications.Team',
|
||||||
|
blank=True,
|
||||||
|
related_name='intervention_subtypes',
|
||||||
|
verbose_name=_("Équipes à notifier"),
|
||||||
|
help_text=_("Équipes notifiées si l'inspection de durabilité révèle un constat non-conforme.")
|
||||||
|
)
|
||||||
|
order_number = models.PositiveIntegerField(null=True, blank=True, verbose_name=_("Ordre d'affichage"))
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name=_("Actif"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Sous-type d'intervention")
|
||||||
|
verbose_name_plural = _("Sous-types d'interventions")
|
||||||
|
ordering = ['thematic', 'intervention_type', 'order_number', 'name_fr']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.get_name()} ({self.thematic.name})"
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
lang = get_language()
|
||||||
|
if lang == 'nl' and self.name_nl:
|
||||||
|
return self.name_nl
|
||||||
|
return self.name_fr
|
||||||
|
|
||||||
|
|
||||||
class Symptom(models.Model):
|
class Symptom(models.Model):
|
||||||
code = models.CharField(max_length=50, unique=True, null=True, blank=True, help_text=_("Code unique optionnel pour identifier le symptôme (ex: pour les raccourcis)."))
|
code = models.CharField(max_length=50, unique=True, null=True, blank=True, help_text=_("Code unique optionnel pour identifier le symptôme (ex: pour les raccourcis)."))
|
||||||
name_fr = models.CharField(max_length=255)
|
name_fr = models.CharField(max_length=255)
|
||||||
|
|
@ -381,6 +446,15 @@ class Intervention(models.Model):
|
||||||
description = models.TextField(null=True,blank=True)
|
description = models.TextField(null=True,blank=True)
|
||||||
maintain_type = models.CharField(choices=MAINTAIN_CHOICES, default='corrective')
|
maintain_type = models.CharField(choices=MAINTAIN_CHOICES, default='corrective')
|
||||||
type = models.CharField(choices=TYPE_CHOICES,null=True,blank=True)
|
type = models.CharField(choices=TYPE_CHOICES,null=True,blank=True)
|
||||||
|
sub_type = models.ForeignKey(
|
||||||
|
'interventions.InterventionSubType',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='interventions',
|
||||||
|
verbose_name=_("Sous-type d'intervention"),
|
||||||
|
help_text=_("Précise la technique ou le sous-type de travail (ex: Hot Box pour une réparation voirie).")
|
||||||
|
)
|
||||||
origin_type = models.CharField(choices=ORIGIN_TYPE_CHOICES, default='damage')
|
origin_type = models.CharField(choices=ORIGIN_TYPE_CHOICES, default='damage')
|
||||||
begin_time = models.DateTimeField(null=True,blank=True)
|
begin_time = models.DateTimeField(null=True,blank=True)
|
||||||
end_time = models.DateTimeField(null=True,blank=True)
|
end_time = models.DateTimeField(null=True,blank=True)
|
||||||
|
|
@ -498,6 +572,7 @@ class Intervention(models.Model):
|
||||||
is_new = self._state.adding # True uniquement lors de la création
|
is_new = self._state.adding # True uniquement lors de la création
|
||||||
status_changed_to_closed = False
|
status_changed_to_closed = False
|
||||||
status_changed_to_validated = False
|
status_changed_to_validated = False
|
||||||
|
status_changed_to_finished = False
|
||||||
old_status = None
|
old_status = None
|
||||||
if not is_new:
|
if not is_new:
|
||||||
old_status = Intervention.objects.filter(pk=self.pk).values_list('status', flat=True).first()
|
old_status = Intervention.objects.filter(pk=self.pk).values_list('status', flat=True).first()
|
||||||
|
|
@ -505,9 +580,13 @@ class Intervention(models.Model):
|
||||||
status_changed_to_closed = True
|
status_changed_to_closed = True
|
||||||
if old_status and old_status != 'validated' and self.status == 'validated':
|
if old_status and old_status != 'validated' and self.status == 'validated':
|
||||||
status_changed_to_validated = True
|
status_changed_to_validated = True
|
||||||
|
if old_status and old_status != 'finished' and self.status == 'finished':
|
||||||
|
status_changed_to_finished = True
|
||||||
else:
|
else:
|
||||||
if self.status == 'validated':
|
if self.status == 'validated':
|
||||||
status_changed_to_validated = True
|
status_changed_to_validated = True
|
||||||
|
if self.status == 'finished':
|
||||||
|
status_changed_to_finished = True
|
||||||
|
|
||||||
if not self.code: # Generate code only if not defined
|
if not self.code: # Generate code only if not defined
|
||||||
next_number = InterventionSequence.get_next_number()
|
next_number = InterventionSequence.get_next_number()
|
||||||
|
|
@ -546,6 +625,17 @@ class Intervention(models.Model):
|
||||||
f"Error automatically generating preventive report for intervention {self.code}"
|
f"Error automatically generating preventive report for intervention {self.code}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if status_changed_to_finished:
|
||||||
|
if self.sub_type and getattr(self.sub_type, 'requires_post_inspection', False):
|
||||||
|
from inspections.services import schedule_post_intervention_inspection
|
||||||
|
import logging
|
||||||
|
try:
|
||||||
|
schedule_post_intervention_inspection(self)
|
||||||
|
except Exception:
|
||||||
|
logging.getLogger(__name__).exception(
|
||||||
|
f"Error automatically scheduling post-intervention inspection for intervention {self.code}"
|
||||||
|
)
|
||||||
|
|
||||||
if status_changed_to_closed:
|
if status_changed_to_closed:
|
||||||
from observations.models import Observation, ObservationComment
|
from observations.models import Observation, ObservationComment
|
||||||
linked_obs = self.observations.exclude(status='closed')
|
linked_obs = self.observations.exclude(status='closed')
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,14 @@ class InterventionForm(forms.ModelForm):
|
||||||
if 'type' in self.fields:
|
if 'type' in self.fields:
|
||||||
self.fields['type'].choices = get_type_choices(thematic_obj)
|
self.fields['type'].choices = get_type_choices(thematic_obj)
|
||||||
|
|
||||||
|
if 'sub_type' in self.fields:
|
||||||
|
from interventions.models import InterventionSubType
|
||||||
|
sub_type_qs = InterventionSubType.objects.filter(is_active=True)
|
||||||
|
if thematic_obj:
|
||||||
|
sub_type_qs = sub_type_qs.filter(thematic=thematic_obj)
|
||||||
|
self.fields['sub_type'].queryset = sub_type_qs
|
||||||
|
self.fields['sub_type'].required = False
|
||||||
|
|
||||||
type_value = self.initial.get('type') or self.data.get('type')
|
type_value = self.initial.get('type') or self.data.get('type')
|
||||||
maintain_type_value = self.initial.get('maintain_type') or self.data.get('maintain_type')
|
maintain_type_value = self.initial.get('maintain_type') or self.data.get('maintain_type')
|
||||||
if type_value == 'inspection':
|
if type_value == 'inspection':
|
||||||
|
|
@ -173,7 +181,7 @@ class InterventionForm(forms.ModelForm):
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
'code', 'title', 'status', 'priority', 'init_description',
|
'code', 'title', 'status', 'priority', 'init_description',
|
||||||
'maintain_type', 'type', 'origin_type', 'begin_time', 'end_time',
|
'maintain_type', 'type', 'sub_type', 'origin_type', 'begin_time', 'end_time',
|
||||||
'asset_category', 'symptom',
|
'asset_category', 'symptom',
|
||||||
'source_category', 'source_detail', 'source_ref',
|
'source_category', 'source_detail', 'source_ref',
|
||||||
'lat', 'lon', 'geom', 'address',
|
'lat', 'lon', 'geom', 'address',
|
||||||
|
|
@ -193,6 +201,7 @@ class InterventionForm(forms.ModelForm):
|
||||||
'symptom': forms.Select(attrs={'class': 'form-select'}),
|
'symptom': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'priority': forms.Select(attrs={'class': 'form-select'}),
|
'priority': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'type': forms.Select(attrs={'class': 'form-select'}),
|
'type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'sub_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'origin_type': forms.Select(attrs={'class': 'form-select'}),
|
'origin_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
|
||||||
# 'project': forms.Select(attrs={'class': 'form-select'}),
|
# 'project': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ import platform
|
||||||
# Bypass WMI queries on Windows which can hang indefinitely due to OS/WMI service issues
|
# Bypass WMI queries on Windows which can hang indefinitely due to OS/WMI service issues
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
platform.win32_ver = lambda: ('10', '10.0.19045', '', 'Multiprocessor Free')
|
platform.win32_ver = lambda: ('10', '10.0.19045', '', 'Multiprocessor Free')
|
||||||
|
osgeo4w_bin = r'C:\OSGeo4W\bin'
|
||||||
|
if os.path.exists(osgeo4w_bin):
|
||||||
|
if hasattr(os, 'add_dll_directory'):
|
||||||
|
try:
|
||||||
|
os.add_dll_directory(osgeo4w_bin)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
os.environ['PATH'] = osgeo4w_bin + ';' + os.environ.get('PATH', '')
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue