808 lines
41 KiB
Python
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,
|
|
})
|
|
|