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 * 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}" ) 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}") 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) 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, })