loko/streetup/interventions/views/preventive.py

808 lines
41 KiB
Python

from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404, redirect
from django.template.loader import render_to_string
from django.urls import reverse, NoReverseMatch
from django.core.serializers import serialize
from django.core.exceptions import FieldDoesNotExist
from django.core.files.base import ContentFile
from django.contrib.gis.db.models.functions import Transform, Centroid, AsGeoJSON, Distance
from django.contrib.gis.geos import GEOSGeometry, Point, MultiPolygon
from django.contrib.gis.measure import D
from django.contrib.contenttypes.models import ContentType
from django.contrib import messages
from django.views.decorators.http import require_POST, require_GET, require_http_methods
from django.contrib.auth.decorators import login_required
from django.utils.timezone import now, localtime, make_aware, get_current_timezone, is_naive
from django.utils import timezone as django_timezone
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from django.utils.dateformat import format as dj_format_date
from django.utils.text import Truncator
from django.utils.html import strip_tags
from django.template.defaultfilters import linebreaksbr
from django.utils.translation import get_language, gettext as _, gettext_lazy as _lazy, ngettext
from django.apps import apps
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.db.models import F, DateTimeField, Q, Case, When, IntegerField, CharField, Value, Count, Prefetch, Sum, Max, QuerySet, OuterRef, Subquery, Exists
from django.db.models.functions import Coalesce, Lower, Cast
from django.db.models.expressions import ExpressionWrapper
from django.db.models import DurationField
from django.db.models.fields.related import ForeignKey
from django.db import transaction
from collections import defaultdict, OrderedDict
from decimal import Decimal, InvalidOperation
import json
import os
import io
import zipfile
from fpdf import FPDF
import pytz
from urllib.parse import urlencode
import re
from html import unescape
from typing import List, Iterable, Tuple, Dict, Optional, Set
import mimetypes
import csv
from io import BytesIO
from datetime import timedelta, datetime
from dateutil.relativedelta import relativedelta
from common.private_files.helpers import build_private_url
from common.utils import get_short_name
from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
InterventionLink,
ContractPauseReasonAssignment,
Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine,
InterventionOccupation,
Operation, OperationTemplate, OperationTemplateItem, OperationStatus, OperationStatusSet, MeasurementDefinition,
GuidedOperationTemplate, GuidedOperationData, GuidedOperationMedia,
InterventionPostPeriodQuantity, InterventionEmailLog, InterventionAssignment)
from interventions.models import STATUS_CHOICES, STATUS_ORDERS, MAINTAIN_CHOICES, TYPE_CHOICES, ORIGIN_TYPE_CHOICES, PAUSE_REASON_CHOICES
from interventions.views.forms import (InterventionForm, InterventionNoteForm, InterventionDocumentForm,
InterventionOccupationForm, PreventiveInterventionForm, AssetsLinkForm,
LinkExistingInterventionsForm)
from interventions.permissions import (intervention_permission_required, intervention_add_permission_required, intervention_edit_permission_required,
intervention_add_preventive_permission_required,
get_allowed_statuses_for_user, get_allowed_update_fields,
can_view_intervention, can_control_intervention, can_edit_intervention,
can_view_intervention_summary,
get_accessible_interventions_for_technician, filter_viewable_interventions_for_user,
can_edit_note, can_delete_document, can_manage_occupations,
can_add_intervention, can_add_repair_intervention,
intervention_manage_occupations_required,
get_contracts_for_intervention_creation, is_external_manager_with_contract_creation_rights,
should_show_created_by_provider_badge, can_view_correction_messages)
from interventions.permissions import ALLOWED_TRANSITIONS, get_allowed_transitions, user_allowed_transitions
from interventions.views.drafts import InterventionDraftManager
from interventions.utils import resolve_intervention_localisations
from common.models import UserConfig, UserContractAccess, UserContractStatusPermission, Thematic, UserThematics
from contracts.models import ContractOrder, ContractPost, Contract, Company, CompanyTeam, CompanyMember, UserCompanyTeamAccess, CompanyMemberHistory
from contracts.permissions import get_allowed_contracts_for_user, user_has_access_to_contract_order
from projects.models import Project, ProjectIntervention
from assets.models import (
PublicLightingStreet,
TrafficLightIntersection, TrafficLightContract,
NatureLocation, NatureRWIZ,
StructureLocation, Structure, StructureContract,
RoadStreet,
SignStreet,
AssetCategory,
AbstractAsset,
AbstractAssetModel,
ITSGeoAsset, ITSGeoAssetContract,
CleanLocation, CleanLocationContract,
)
from assets.permissions import get_allowed_actions_for_asset
from assets.views.locations import LOCATION_REGISTRY, registry_items_for_thematics, base_search_qs, SEARCH_LIMIT_PER_TYPE, SEARCH_LIMIT_TOTAL
from common.utils import redirect_with_variant, measure_time, strip_accents_lower
from observations.permissions import get_observation_access_context
from observations.models import Observation as ObservationModel
from controls.permissions import user_can_view_control
from interventions.views.views_pdf_reporting import (
generate_intervention_pdf,
preview_intervention_pdf,
download_intervention_report_pdf,
preview_intervention_report_pdf,
bulk_execution_report_pdf,
start_bulk_execution_report,
poll_bulk_execution_report,
download_bulk_execution_report,
)
import logging
import smtplib
import socket
import ssl
logger = logging.getLogger(__name__)
from interventions.views.helpers import *
@intervention_add_preventive_permission_required
def preventive_intervention_draft(request, thematic_code):
if request.method == 'POST':
draft = InterventionDraftManager(request.session)
draft_id = draft.get_or_create(request.POST.get('draft_id'))
form_data = {}
for key, values in request.POST.lists():
if key == 'csrfmiddlewaretoken':
continue
cleaned_values = [v for v in values if v not in (None, '')]
if not cleaned_values:
continue
form_data[key] = cleaned_values if len(cleaned_values) > 1 else cleaned_values[0]
draft.update_form_data(draft_id, form_data)
return redirect(
f"{reverse('assets:index')}"
f"?preventive_draft_id={draft_id}"
f"&thematic_code={thematic_code}"
)
@intervention_add_preventive_permission_required
def preventive_link_assets(request, thematic_code):
draft_id = request.POST.get("preventive_draft_id")
drafts = InterventionDraftManager(request.session)
preventive_type_of_selected_elements = request.POST.get("preventive_element_type", 'locations')
if preventive_type_of_selected_elements == 'assets':
form = AssetsLinkForm(request.POST)
if not form.is_valid():
messages.error(request, form.errors.as_text())
selected_assets = []
else:
selected_assets = form.cleaned_data["assets"] # [{'model', 'object_id'}, ...]
drafts.update_assets(draft_id, selected_assets)
else:
selected_locations_string = (
request.POST.get("preventive_locations_ids")
or request.POST.get("selected_ids")
or ""
)
selected_locations = [loc_id for loc_id in selected_locations_string.split(',') if loc_id]
drafts.update_locations(draft_id, selected_locations)
return redirect(f"{reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': thematic_code})}?draft_id={draft_id}")
@intervention_add_preventive_permission_required
def preventive_remove_element(request, thematic_code):
"""Supprime un élément (location ou asset) de la sélection du draft"""
draft_id = request.POST.get("draft_id")
element_type = request.POST.get("element_type") # 'location' ou 'asset'
element_id = request.POST.get("element_id")
drafts = InterventionDraftManager(request.session)
success = False
if element_type == 'location':
success = drafts.remove_location(draft_id, element_id)
elif element_type == 'asset':
asset_model = request.POST.get("asset_model")
success = drafts.remove_asset(draft_id, asset_model, element_id)
if success:
# Récupérer le nombre d'éléments restants
draft_data = drafts.get(draft_id)
locations_count = len(draft_data.get('locations', []))
assets_count = len(draft_data.get('assets', []))
total_count = locations_count + assets_count
return JsonResponse({
'success': True,
'total_count': total_count
})
else:
return JsonResponse({
'success': False,
'error': _("Impossible de supprimer l'élément")
}, status=400)
@intervention_add_preventive_permission_required
def intervention_add_preventive(request, thematic_code):
thematic = Thematic.objects.filter(code=thematic_code).first()
drafts = InterventionDraftManager(request.session)
draft_id = request.GET.get("draft_id") or request.POST.get("draft_id")
draft_data = drafts.get(draft_id)
draft_selected_locations_ids = draft_data.get('locations', [])
draft_selected_assets = draft_data.get('assets', []) # [{'model','object_id'}, ...]
form_data = draft_data.get('form_data', {})
operation_templates_initial = form_data.get('operation_templates')
if isinstance(operation_templates_initial, str):
form_data['operation_templates'] = [operation_templates_initial]
# Récupérer redirect_to_order depuis GET/POST ou le draft
redirect_to_order = (
request.GET.get("redirect_to_order") or
request.POST.get("redirect_to_order") or
form_data.get("redirect_to_order")
)
# Pré-remplissage contrat/commande si on arrive avec un order_id (ex: depuis une commande)
order_id_from_params = request.GET.get("order_id") or request.POST.get("order_id")
if order_id_from_params and str(order_id_from_params).isdigit():
order_obj = ContractOrder.objects.select_related("contract").filter(id=int(order_id_from_params)).first()
if order_obj:
# Ne pas écraser des valeurs déjà présentes dans le draft
form_data.setdefault("order", order_obj)
form_data.setdefault("contract", order_obj.contract)
select_elements_type = form_data.get('type_of_selected_elements', 'locations')
asset_denomination = _("Localisations")
# --- Localisations sélectionnées
selected_locations_qs = []
if thematic_code == 'trafficlights':
selected_locations_qs = TrafficLightIntersection.objects.filter(id__in=draft_selected_locations_ids)
elif thematic_code == 'publiclighting':
selected_locations_qs = PublicLightingStreet.objects.filter(id__in=draft_selected_locations_ids)
elif thematic_code == 'nature':
selected_locations_qs = NatureLocation.objects.filter(id__in=draft_selected_locations_ids)
elif thematic_code == 'structures':
selected_locations_qs = StructureLocation.objects.filter(id__in=draft_selected_locations_ids)
elif thematic_code == 'roads':
selected_locations_qs = RoadStreet.objects.filter(id__in=draft_selected_locations_ids)
elif thematic_code == 'sign':
selected_locations_qs = SignStreet.objects.filter(id__in=draft_selected_locations_ids)
elif thematic_code == 'water':
selected_locations_qs = NatureRWIZ.objects.filter(id__in=draft_selected_locations_ids)
selected_locations_array = [
{"id": loc.id, "code": loc.code, "name": loc.get_name()}
for loc in selected_locations_qs
]
# --- Assets sélectionnés (multi-modèles, ordre préservé)
selected_assets_array = resolve_selected_assets(draft_selected_assets) # [{'id','asset_type','code','name','lon','lat'}, ...]
if request.method == "POST":
form = PreventiveInterventionForm(request.POST, user=request.user, thematic=thematic)
if form.is_valid():
data = form.cleaned_data
recurrence_delta = relativedelta(months=0)
if data.get("recurrence_delta_months"):
recurrence_delta = relativedelta(months=data["recurrence_delta_months"])
user = request.user
title = data["title"]
status = data["status"]
intervention_type = data.get("type", "care")
contract = data["contract"]
order = data["order"]
operation_templates = list(data.get("operation_templates", []))
intervention_creation_mode = data.get("intervention_creation_mode", "one_per_element")
expected_start = data["expected_begin_time"]
expected_end = data["expected_end_time"]
recurrence_count = data["recurrence_count"] + 1
create_order_per_recurrence = data.get("create_order_per_recurrence", False)
# --- Créer des commandes pour chaque répétition si nécessaire
orders_list = []
if create_order_per_recurrence and order and recurrence_count > 1:
# Renommer la commande initiale avec le suffixe -1
original_order_code = order.order_code
order.order_code = f"{original_order_code}-1"
order.save(update_fields=['order_code'])
orders_list.append(order)
# Créer des commandes supplémentaires pour chaque répétition
for i in range(2, recurrence_count + 1):
new_order = ContractOrder.objects.create(
contract=contract,
order_code=f"{original_order_code}-{i}",
description=order.description,
order_date=order.order_date,
delivery_date=order.delivery_date,
total_amount=order.total_amount,
order_status=order.order_status,
ordered_by=user,
project=order.project
)
orders_list.append(new_order)
else:
# Utiliser la même commande pour toutes les répétitions
orders_list = [order] * recurrence_count
# --- Fonction utilitaire pour créer une intervention
def create_base_intervention(i: int, order_for_intervention):
start_date = expected_start + i * recurrence_delta
end_date = expected_end + i * recurrence_delta
intervention = Intervention.objects.create(
title=title,
status=status,
status_order=STATUS_ORDERS[status],
contract=contract,
order=order_for_intervention,
assigned_provider=contract.company if contract else None,
expected_begin_time=start_date,
expected_end_time=end_date,
maintain_type="preventive",
type=intervention_type,
created_by=user,
thematic=thematic
)
# Auto-vérification si l'utilisateur a le droit can_check_interventions
auto_check_if_eligible(user, intervention)
InterventionTimeLine.objects.create(
intervention=intervention,
event_user=request.user,
event_time=now(),
event_type='creation',
from_status=None,
to_status=intervention.status
)
InterventionPlanificationTimeLine.objects.create(
intervention=intervention,
event_user=user,
event_type='expected_begin',
event_time=start_date,
event_description="Début prévisionnel"
)
InterventionPlanificationTimeLine.objects.create(
intervention=intervention,
event_user=user,
event_type='expected_end',
event_time=end_date,
event_description="Fin prévisionnelle"
)
return intervention
interventions = []
# Les opérations génériques et équipement sont déterminées par l'attribut is_generic de chaque item
# helper : renvoie la première attr existante
def first_existing_attr(obj, names):
for n in names:
if hasattr(obj, n):
return getattr(obj, n)
return None
# helper : récupérer lon/lat en remontant la hiérarchie des parents si nécessaire
def resolve_coords_from_hierarchy(loc, max_depth=10):
"""
Remonte la hiérarchie parent_location pour trouver lon/lat.
Retourne (lon, lat, geom) - les valeurs peuvent être None.
"""
current = loc
depth = 0
lon, lat, geom = None, None, None
while current is not None and depth < max_depth:
# Essayer d'obtenir lon/lat de l'objet courant
current_lon = getattr(current, "lon", None)
current_lat = getattr(current, "lat", None)
current_geom = getattr(current, "geom", None)
if lon is None and current_lon is not None:
lon = current_lon
if lat is None and current_lat is not None:
lat = current_lat
if geom is None and current_geom is not None:
geom = current_geom
# Si on a trouvé lon et lat, on peut s'arrêter
if lon is not None and lat is not None:
break
# Essayer de remonter au parent hiérarchique
parent = getattr(current, "parent_location", None)
if parent is None:
# Fallback via structure → main_location
struct = getattr(current, "structure", None)
parent = getattr(struct, "main_location", None) if struct else None
current = parent
depth += 1
return lon, lat, geom
# helper : trouver la "meilleure" localisation d'un asset (objet modèle)
def resolve_linked_location(asset_obj):
if asset_obj is None:
return None
# cas 1 : FK directe vers une localisation
loc = first_existing_attr(asset_obj, ("intersection", "street", "section", "location"))
if loc is not None:
return loc
# cas 2 : fallback via Structure → main_location
struct = getattr(asset_obj, "structure", None)
main_loc = getattr(struct, "main_location", None) if struct else None
return main_loc or None
# -------------------------
# MODE: CRÉER UNE INTERVENTION PAR ÉLÉMENT
# -------------------------
if intervention_creation_mode == 'one_per_element':
# MODE LOCATIONS
for loc in selected_locations_qs:
for i in range(recurrence_count):
order_for_intervention = orders_list[i] if i < len(orders_list) else order
intervention = create_base_intervention(i, order_for_intervention)
_attach_equipment_templates(intervention, operation_templates)
_assign_guided_template(intervention, operation_templates)
# 1) Lien générique intervention ↔ localisation
ct = ContentType.objects.get_for_model(loc.__class__)
InterventionLocation.objects.get_or_create(
intervention=intervention,
content_type=ct,
object_id=loc.pk,
)
# 2) Titre
suffix = None
if hasattr(loc, "get_name"):
suffix = loc.get_name() or None
if thematic_code == "trafficlights":
suffix = None
prefix = loc.code
# Add prefix and suffix if they exist, otherwise fallback to what is available
if prefix and suffix:
intervention.title = f"{prefix} - {title} - {suffix}"
elif prefix:
intervention.title = f"{prefix} - {title}"
elif suffix:
intervention.title = f"{title} - {suffix}"
else:
intervention.title = title
# 3) lon/lat/geom avec fallbacks (remonte la hiérarchie des parents)
lon, lat, geom_source = resolve_coords_from_hierarchy(loc)
if geom_source:
if geom_source.geom_type in ['Point', 'MultiPoint', 'LineString', 'MultiLineString']:
intervention.geom = to_multipolygon(geom_source.buffer(1))
else:
intervention.geom = to_multipolygon(geom_source)
elif lon is not None and lat is not None:
pt = Point(lon, lat, srid=4326)
pt.transform(3812)
intervention.geom = to_multipolygon(pt.buffer(1))
if lon is not None:
intervention.lon = lon
if lat is not None:
intervention.lat = lat
# 4) location_code
if getattr(loc, "code", None):
intervention.location_code = loc.code
intervention.save()
# Ajouter les opérations génériques (items avec is_generic=True)
_add_operations_from_templates(intervention, operation_templates, only_generic=True)
# Filtrer les templates qui ont des filtres de catégories ou modèles
templates_with_filters = _get_templates_with_equipment_filters(operation_templates)
# Récupérer les assets enfants et créer les opérations équipement
# uniquement si des templates avec filtres existent
if templates_with_filters:
location_child_assets = list(_extract_assets_from_location(loc))
if location_child_assets:
ct_cache = {}
for child_asset in location_child_assets:
# Obtenir le ContentType pour cet asset
asset_class = child_asset.__class__
if asset_class not in ct_cache:
ct_cache[asset_class] = ContentType.objects.get_for_model(asset_class)
ct_asset = ct_cache[asset_class]
# Filtrer les templates applicables à cet asset
applicable_templates = [
t for t in templates_with_filters
if _asset_matches_template_filters(child_asset, t, allow_unfiltered=False)
]
if applicable_templates:
# Lier l'asset à l'intervention
InterventionAsset.objects.get_or_create(
intervention=intervention,
content_type=ct_asset,
object_id=child_asset.pk,
)
# Créer les opérations équipement pour cet asset
_add_operations_from_templates(
intervention,
applicable_templates,
child_asset,
ct_asset,
only_generic=False
)
interventions.append(intervention)
# MODE ASSETS
# Précharger les objets asset avec leurs fks de localisation
assets_map = batch_fetch_assets_with_locations(selected_assets_array)
for a in selected_assets_array:
model_name = a['asset_type']
asset_id = a['id']
# ContentType de l'asset
ct_asset = ContentType.objects.get(model=model_name)
# Objet asset préchargé (avec ses FKs)
asset_obj = assets_map.get(model_name, {}).get(asset_id)
# Label affiché
asset_code = a.get('code') or f"{model_name}#{asset_id}"
asset_name = a.get('name') or ''
if thematic_code == 'artworks' and asset_code and asset_name:
asset_label = f"{asset_code} - {asset_name}"
else:
asset_label = asset_code or asset_name or f"{model_name}#{asset_id}"
# Localisation liée (objet modèle ou None)
linked_location = resolve_linked_location(asset_obj)
for i in range(recurrence_count):
order_for_intervention = orders_list[i] if i < len(orders_list) else order
intervention = create_base_intervention(i, order_for_intervention)
_attach_equipment_templates(intervention, operation_templates)
_assign_guided_template(intervention, operation_templates)
# 1) Lien générique Asset ↔ Intervention
InterventionAsset.objects.get_or_create(
intervention=intervention,
content_type=ct_asset,
object_id=asset_id,
)
# 2) Titre
intervention.title = f"{title} - {asset_label}"
# 3) lon/lat/geom
lon, lat = a.get('lon'), a.get('lat')
if asset_obj is not None and (lon is None or lat is None):
lon = getattr(asset_obj, "lon", lon)
lat = getattr(asset_obj, "lat", lat)
# Si les coordonnées sont toujours manquantes, utiliser resolve_coords_from_hierarchy
# pour remonter la hiérarchie des localisations parentes
if (lon is None or lat is None) and linked_location is not None:
parent_lon, parent_lat, _geom = resolve_coords_from_hierarchy(linked_location)
if lon is None and parent_lon is not None:
lon = parent_lon
if lat is None and parent_lat is not None:
lat = parent_lat
if lon is not None and lat is not None:
pt = Point(lon, lat, srid=4326)
pt.transform(3812)
intervention.geom = to_multipolygon(pt.buffer(1))
intervention.lon, intervention.lat = lon, lat
intervention.save()
# 4) Lien générique Localisation ↔ Intervention
if linked_location is not None and hasattr(linked_location, "pk"):
ct_loc = ContentType.objects.get_for_model(linked_location.__class__)
InterventionLocation.objects.get_or_create(
intervention=intervention,
content_type=ct_loc,
object_id=linked_location.pk,
)
if hasattr(linked_location, "code") and linked_location.code:
intervention.location_code = linked_location.code
intervention.save(update_fields=["location_code"])
# Ajouter les opérations génériques (items avec is_generic=True)
_add_operations_from_templates(intervention, operation_templates, only_generic=True)
# Ajouter les opérations équipement pour cet asset (items avec is_generic=False)
# Filtrer les templates applicables à cet asset
applicable_templates = [
t for t in operation_templates
if _asset_matches_template_filters(asset_obj, t)
]
if applicable_templates:
_add_operations_from_templates(intervention, applicable_templates, asset_obj, ct_asset, only_generic=False)
interventions.append(intervention)
# -------------------------
# MODE: CRÉER UNE SEULE INTERVENTION POUR TOUS LES ÉLÉMENTS
# -------------------------
else: # single_for_all
for i in range(recurrence_count):
order_for_intervention = orders_list[i] if i < len(orders_list) else order
intervention = create_base_intervention(i, order_for_intervention)
_attach_equipment_templates(intervention, operation_templates)
_assign_guided_template(intervention, operation_templates)
# Utiliser le titre de base
intervention.title = title
# Lier toutes les localisations à l'intervention
for loc in selected_locations_qs:
ct = ContentType.objects.get_for_model(loc.__class__)
InterventionLocation.objects.get_or_create(
intervention=intervention,
content_type=ct,
object_id=loc.pk,
)
# Précharger les assets si nécessaire
if selected_assets_array:
assets_map = batch_fetch_assets_with_locations(selected_assets_array)
# Lier tous les assets à l'intervention
for a in selected_assets_array:
model_name = a['asset_type']
asset_id = a['id']
ct_asset = ContentType.objects.get(model=model_name)
InterventionAsset.objects.get_or_create(
intervention=intervention,
content_type=ct_asset,
object_id=asset_id,
)
# Lier également la localisation de l'asset si elle existe
asset_obj = assets_map.get(model_name, {}).get(asset_id)
linked_location = resolve_linked_location(asset_obj)
if linked_location is not None and hasattr(linked_location, "pk"):
ct_loc = ContentType.objects.get_for_model(linked_location.__class__)
InterventionLocation.objects.get_or_create(
intervention=intervention,
content_type=ct_loc,
object_id=linked_location.pk,
)
# Calculer une position moyenne pour l'intervention (premier élément ou centroïde)
first_loc = selected_locations_qs.first() if selected_locations_qs else None
if first_loc:
# Utiliser resolve_coords_from_hierarchy pour remonter la hiérarchie si nécessaire
lon, lat, _geom_unused = resolve_coords_from_hierarchy(first_loc)
if lon and lat:
intervention.lon = lon
intervention.lat = lat
pt = Point(lon, lat, srid=4326)
pt.transform(3812)
intervention.geom = to_multipolygon(pt.buffer(1))
intervention.location_code = getattr(first_loc, "code", None) or None
elif selected_assets_array:
# Utiliser le premier asset pour la position
first_asset = selected_assets_array[0]
lon, lat = first_asset.get('lon'), first_asset.get('lat')
# Récupérer l'objet asset et sa localisation liée
model_name = first_asset['asset_type']
asset_id = first_asset['id']
first_asset_obj = assets_map.get(model_name, {}).get(asset_id)
first_location = resolve_linked_location(first_asset_obj)
# Si les coordonnées sont manquantes, essayer depuis l'objet asset
if first_asset_obj is not None and (lon is None or lat is None):
lon = getattr(first_asset_obj, "lon", lon)
lat = getattr(first_asset_obj, "lat", lat)
# Si toujours manquantes, remonter la hiérarchie des localisations parentes
if (lon is None or lat is None) and first_location is not None:
parent_lon, parent_lat, _geom = resolve_coords_from_hierarchy(first_location)
if lon is None and parent_lon is not None:
lon = parent_lon
if lat is None and parent_lat is not None:
lat = parent_lat
if lon and lat:
intervention.lon = lon
intervention.lat = lat
pt = Point(lon, lat, srid=4326)
pt.transform(3812)
intervention.geom = to_multipolygon(pt.buffer(1))
if first_location is not None and hasattr(first_location, "code"):
intervention.location_code = getattr(first_location, "code", None) or None
intervention.save()
# Ajouter les opérations génériques (items avec is_generic=True, une seule fois)
_add_operations_from_templates(intervention, operation_templates, only_generic=True)
# Filtrer les templates qui ont des filtres de catégories ou modèles
templates_with_filters = _get_templates_with_equipment_filters(operation_templates)
# Récupérer les assets enfants de toutes les localisations et créer les opérations équipement
# uniquement si des templates avec filtres existent
if templates_with_filters:
ct_cache = {}
for loc in selected_locations_qs:
location_child_assets = list(_extract_assets_from_location(loc))
for child_asset in location_child_assets:
# Obtenir le ContentType pour cet asset
asset_class = child_asset.__class__
if asset_class not in ct_cache:
ct_cache[asset_class] = ContentType.objects.get_for_model(asset_class)
ct_asset = ct_cache[asset_class]
# Filtrer les templates applicables à cet asset
applicable_templates = [
t for t in templates_with_filters
if _asset_matches_template_filters(child_asset, t, allow_unfiltered=False)
]
if applicable_templates:
# Lier l'asset à l'intervention
InterventionAsset.objects.get_or_create(
intervention=intervention,
content_type=ct_asset,
object_id=child_asset.pk,
)
# Créer les opérations équipement pour cet asset
_add_operations_from_templates(
intervention,
applicable_templates,
child_asset,
ct_asset,
only_generic=False
)
# Ajouter les opérations équipement pour chaque asset sélectionné directement (items avec is_generic=False)
if selected_assets_array:
for a in selected_assets_array:
model_name = a['asset_type']
asset_id = a['id']
ct_asset = ContentType.objects.get(model=model_name)
asset_obj = assets_map.get(model_name, {}).get(asset_id)
# Filtrer les templates applicables à cet asset
applicable_templates = [
t for t in operation_templates
if _asset_matches_template_filters(asset_obj, t)
]
if applicable_templates:
_add_operations_from_templates(intervention, applicable_templates, asset_obj, ct_asset, only_generic=False)
interventions.append(intervention)
# Fin : on purge le draft et on redirige
drafts.delete(draft_id)
# Si on vient d'une commande et qu'on veut retourner vers elle après création
# redirect_to_order est déjà récupéré au début de la fonction
if redirect_to_order and order:
return redirect("contracts:contracts_orders_detail", order_id=order.id)
return redirect("interventions:interventions_index")
else:
form = PreventiveInterventionForm(initial=form_data, user=request.user, thematic=thematic)
if thematic_code == 'trafficlights':
asset_denomination = "Intersections"
return render(request, "interventions/add/intervention_add_preventive.html", {
"form": form,
"draft_id": draft_id,
"selected_elements_count": len(selected_locations_array) + len(selected_assets_array),
"selected_locations": selected_locations_array,
"selected_assets": selected_assets_array,
"select_elements_type": select_elements_type,
"thematic": thematic,
"asset_denomination": asset_denomination,
"redirect_to_order": redirect_to_order,
})