810 lines
32 KiB
Python
810 lines
32 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,
|
|
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 intervention_add_message(request, intervention_id):
|
|
"""
|
|
Poste un message dans la discussion liée à cette intervention.
|
|
Tout utilisateur ayant accès à l'intervention peut poster.
|
|
"""
|
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
|
|
|
from notifications.services import (
|
|
can_post_intervention_message,
|
|
get_or_create_intervention_discussion,
|
|
post_message,
|
|
)
|
|
if not can_post_intervention_message(request.user, intervention):
|
|
messages.error(request, _("Vous n'avez pas la permission de poster un message sur cette intervention."))
|
|
return redirect(
|
|
reverse('interventions:intervention_detail', args=[intervention_id]) + '#discussion'
|
|
)
|
|
|
|
content = (request.POST.get('content') or '').strip()
|
|
if not content:
|
|
messages.warning(request, _("Le message ne peut pas être vide."))
|
|
return redirect(
|
|
reverse('interventions:intervention_detail', args=[intervention_id]) + '#discussion'
|
|
)
|
|
|
|
discussion, _ = get_or_create_intervention_discussion(intervention)
|
|
files = request.FILES.getlist('attachments') or None
|
|
post_message(discussion=discussion, author=request.user, content=content, files=files)
|
|
|
|
return redirect(
|
|
reverse('interventions:intervention_detail', args=[intervention_id]) + '#discussion'
|
|
)
|
|
|
|
|
|
def intervention_add_note(request, intervention_id):
|
|
intervention = get_object_or_404(Intervention, id=intervention_id)
|
|
|
|
form = InterventionNoteForm(request.POST)
|
|
if form.is_valid():
|
|
note_instance = form.save(commit=False)
|
|
note_instance.intervention = intervention
|
|
note_instance.note_author = request.user
|
|
note_instance.note_time = now()
|
|
note_instance.save()
|
|
|
|
return JsonResponse({
|
|
'success': True,
|
|
'note': _serialize_note(note_instance, request.user)
|
|
})
|
|
|
|
return JsonResponse({'success': False, 'errors': form.errors}, status=400)
|
|
|
|
|
|
def intervention_update_note(request, intervention_id, note_id):
|
|
intervention = get_object_or_404(Intervention, id=intervention_id)
|
|
note = get_object_or_404(InterventionNote, id=note_id, intervention=intervention)
|
|
|
|
if not can_edit_note(request.user, request.user.config, note):
|
|
return JsonResponse({'success': False, 'error': _('Vous ne pouvez pas modifier cette note.')}, status=403)
|
|
|
|
form = InterventionNoteForm(request.POST, instance=note)
|
|
if form.is_valid():
|
|
note_instance = form.save(commit=False)
|
|
note_instance.updated_by = request.user
|
|
note_instance.save(update_fields=['content', 'note_type', 'updated_at', 'updated_by'])
|
|
note.refresh_from_db(fields=['content', 'note_type', 'updated_at', 'updated_by'])
|
|
|
|
return JsonResponse({
|
|
'success': True,
|
|
'note': _serialize_note(note, request.user)
|
|
})
|
|
|
|
return JsonResponse({'success': False, 'errors': form.errors}, status=400)
|
|
|
|
|
|
def intervention_delete_note(request, intervention_id, note_id):
|
|
intervention = get_object_or_404(Intervention, id=intervention_id)
|
|
note = get_object_or_404(InterventionNote, id=note_id, intervention=intervention)
|
|
|
|
if not can_edit_note(request.user, request.user.config, note):
|
|
return JsonResponse({'success': False, 'error': _('Vous ne pouvez pas supprimer cette note.')}, status=403)
|
|
|
|
note.delete()
|
|
return JsonResponse({'success': True})
|
|
|
|
|
|
def intervention_add_document(request, intervention_id):
|
|
intervention = get_object_or_404(Intervention, id=intervention_id)
|
|
variant = request.POST.get('v') or request.GET.get('v')
|
|
|
|
if request.method == 'POST':
|
|
form = InterventionDocumentForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
files = form.cleaned_data["documents"]
|
|
for file in files:
|
|
InterventionDocument.objects.create(intervention=intervention, file=file, uploaded_by=request.user)
|
|
|
|
messages.success(request, _("Document(s) sauvegardé(s) avec succès !"))
|
|
return redirect_with_variant('interventions:intervention_detail', variant=variant, url_kwargs={'intervention_id': intervention_id})
|
|
|
|
else:
|
|
messages.error(request, _("Erreur lors de la sauvegarde des documents."))
|
|
return redirect_with_variant('interventions:intervention_detail', variant=variant, url_kwargs={'intervention_id': intervention_id})
|
|
|
|
else:
|
|
form = InterventionDocumentForm()
|
|
|
|
template_name = 'interventions/add/add_intervention_document_mobile.html' if variant == 'mobile' else 'interventions/add/add_intervention_document.html'
|
|
return render(request, template_name, {'form': form, 'intervention': intervention})
|
|
|
|
|
|
def intervention_delete_document(request, intervention_id, document_id):
|
|
intervention = get_object_or_404(Intervention, id=intervention_id)
|
|
document = get_object_or_404(InterventionDocument, id=document_id, intervention=intervention)
|
|
|
|
try:
|
|
user_config = request.user.config
|
|
except UserConfig.DoesNotExist:
|
|
user_config = None
|
|
|
|
if not can_delete_document(request.user, user_config, document):
|
|
return JsonResponse({'success': False, 'error': _('Vous ne pouvez pas supprimer ce document.')}, status=403)
|
|
|
|
if document.file:
|
|
document.file.delete(save=False)
|
|
if document.thumbnail:
|
|
document.thumbnail.delete(save=False)
|
|
|
|
document.delete()
|
|
return JsonResponse({'success': True})
|
|
|
|
|
|
def intervention_get_copy_target_tags(request, intervention_id):
|
|
"""
|
|
Returns available DocumentTags for a given content-type target
|
|
(used in the copy-document modal).
|
|
GET ?ct_id=X&obj_id=Y
|
|
"""
|
|
from django.db.models import Count, Q
|
|
from documents.models import DocumentTag
|
|
|
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
|
|
|
ct_id = request.GET.get('ct_id')
|
|
obj_id_raw = request.GET.get('obj_id')
|
|
if not ct_id:
|
|
return JsonResponse({'tags': []})
|
|
|
|
try:
|
|
from django.contrib.contenttypes.models import ContentType as _CT
|
|
ct = _CT.objects.get(pk=int(ct_id))
|
|
except (Exception,):
|
|
return JsonResponse({'tags': []})
|
|
|
|
model_class = ct.model_class()
|
|
|
|
# --- Determine thematic ---
|
|
thematic = None
|
|
# 1) Check LOCATION_REGISTRY for location models
|
|
for _key, info in LOCATION_REGISTRY.items():
|
|
if info['model'] == model_class:
|
|
try:
|
|
from common.models import Thematic as _Thematic
|
|
thematic = _Thematic.objects.filter(code=info['thematic']).first()
|
|
except Exception:
|
|
pass
|
|
break
|
|
|
|
# 2) For asset models: get thematic via category
|
|
if thematic is None and model_class and obj_id_raw:
|
|
try:
|
|
obj = model_class.objects.select_related('category__thematic').get(pk=int(obj_id_raw))
|
|
cat = getattr(obj, 'category', None)
|
|
if cat:
|
|
thematic = getattr(cat, 'thematic', None)
|
|
except Exception:
|
|
pass
|
|
|
|
# 3) Fallback: intervention's own thematic
|
|
if thematic is None:
|
|
thematic = intervention.thematic
|
|
|
|
if thematic is None:
|
|
return JsonResponse({'tags': []})
|
|
|
|
# Filter tags: same logic as asset_documents template tag
|
|
qs = DocumentTag.objects.filter(thematics=thematic)
|
|
qs = (qs
|
|
.annotate(ct_count=Count('related_content_types'))
|
|
.filter(Q(ct_count=0) | Q(related_content_types=ct))
|
|
.distinct()
|
|
.order_by('order', 'name_fr'))
|
|
|
|
lang = get_language()
|
|
tags_data = [
|
|
{
|
|
'id': t.pk,
|
|
'name': t.name_nl if lang.startswith('nl') and t.name_nl else t.name_fr,
|
|
'extra_params_schema': t.extra_params_schema or [],
|
|
}
|
|
for t in qs
|
|
]
|
|
return JsonResponse({'tags': tags_data})
|
|
|
|
|
|
def intervention_copy_document(request, intervention_id, document_id):
|
|
"""
|
|
Copy an InterventionDocument to a linked location or asset as a ManagedDocument.
|
|
POST params:
|
|
ct_id, obj_id — target content-type / object
|
|
tag_ids[] — selected tag PKs (optional)
|
|
ep_{tag_id}_{key} — extra param values per tag (optional)
|
|
"""
|
|
import os
|
|
from django.core.files.base import ContentFile
|
|
from django.contrib.contenttypes.models import ContentType as _CT
|
|
from documents.models import ManagedDocument, DocumentVersion, DocumentAttachment, DocumentTag
|
|
from documents.services import DocumentManagementService
|
|
|
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
|
document = get_object_or_404(InterventionDocument, pk=document_id, intervention=intervention)
|
|
|
|
ct_id = request.POST.get('ct_id')
|
|
obj_id_raw = request.POST.get('obj_id')
|
|
tag_ids = request.POST.getlist('tag_ids')
|
|
|
|
if not ct_id or not obj_id_raw:
|
|
return JsonResponse({'success': False, 'error': _('Cible non spécifiée')}, status=400)
|
|
|
|
try:
|
|
ct = _CT.objects.get(pk=int(ct_id))
|
|
obj_id = int(obj_id_raw)
|
|
except Exception:
|
|
return JsonResponse({'success': False, 'error': _('Cible invalide')}, status=400)
|
|
|
|
# Verify the target is actually linked to this intervention
|
|
is_linked = (
|
|
InterventionLocation.objects.filter(
|
|
intervention=intervention, content_type=ct, object_id=obj_id
|
|
).exists()
|
|
or
|
|
InterventionAsset.objects.filter(
|
|
intervention=intervention, content_type=ct, object_id=obj_id
|
|
).exists()
|
|
)
|
|
if not is_linked:
|
|
return JsonResponse(
|
|
{'success': False, 'error': _("La cible n'est pas liée à cette intervention")},
|
|
status=400,
|
|
)
|
|
|
|
filename = os.path.basename(document.file.name)
|
|
title = os.path.splitext(filename)[0]
|
|
|
|
# Collect extra params per tag from POST keys ep_{tag_id}_{param_key}
|
|
tag_extra_params = {}
|
|
for key, value in request.POST.items():
|
|
if key.startswith('ep_'):
|
|
parts = key[3:].split('_', 1)
|
|
if len(parts) == 2:
|
|
t_id, param_key = parts
|
|
tag_extra_params.setdefault(t_id, {})[param_key] = value.strip()
|
|
|
|
# Resolve target object and its document folder (for service-based versioning)
|
|
target_obj = None
|
|
document_folder = None
|
|
try:
|
|
model_class = ct.model_class()
|
|
if model_class:
|
|
target_obj = model_class.objects.get(pk=obj_id)
|
|
document_folder = DocumentManagementService.get_default_folder_for_object(target_obj)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
if tag_ids and document_folder is not None:
|
|
# Service path: one document per tag with proper unique-tag versioning
|
|
valid_tags = DocumentTag.objects.filter(pk__in=tag_ids)
|
|
for tag in valid_tags:
|
|
with document.file.open('rb') as src:
|
|
file_copy = ContentFile(src.read(), name=filename)
|
|
extra = {k: v for k, v in tag_extra_params.get(str(tag.pk), {}).items() if v}
|
|
DocumentManagementService.add_document(
|
|
file=file_copy,
|
|
tag=tag,
|
|
folder=document_folder,
|
|
user=request.user,
|
|
title=title,
|
|
content_object=target_obj,
|
|
unique_params=extra or None,
|
|
)
|
|
else:
|
|
# Direct path: single document with all tags (no folder configured)
|
|
with document.file.open('rb') as src:
|
|
file_copy = ContentFile(src.read(), name=filename)
|
|
merged_params = {}
|
|
for params in tag_extra_params.values():
|
|
merged_params.update({k: v for k, v in params.items() if v})
|
|
|
|
doc = ManagedDocument.objects.create(
|
|
title=title,
|
|
created_by=request.user,
|
|
unique_params=merged_params,
|
|
)
|
|
if tag_ids:
|
|
valid_tags = DocumentTag.objects.filter(pk__in=tag_ids)
|
|
doc.tags.set(valid_tags)
|
|
|
|
version = DocumentVersion(
|
|
document=doc,
|
|
uploaded_by=request.user,
|
|
version_number=1,
|
|
)
|
|
version.file.save(filename, file_copy, save=True)
|
|
|
|
DocumentAttachment.objects.create(
|
|
document=doc,
|
|
content_type=ct,
|
|
object_id=obj_id,
|
|
attached_by=request.user,
|
|
)
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
|
|
|
return JsonResponse({'success': True})
|
|
|
|
|
|
def linked_interventions_summary(request):
|
|
"""
|
|
Renvoie le total d'interventions liées (en cours + récentes terminées):
|
|
- ?location_ct=<id>&location_id=<id>
|
|
- ?equipment_ct=<id>&equipment_id=<id>
|
|
- ?lon=<x>&lat=<y>[&radius=50]
|
|
Retourne: {total: int, total_recent: int}
|
|
"""
|
|
base_qs = filter_viewable_interventions_for_user(request.user)
|
|
|
|
thematic_code = request.GET.get('thematic')
|
|
if thematic_code:
|
|
base_qs = base_qs.filter(thematic__code=thematic_code)
|
|
|
|
location_ct = request.GET.get('location_ct')
|
|
location_id = request.GET.get('location_id')
|
|
equipment_ct = request.GET.get('equipment_ct')
|
|
equipment_id = request.GET.get('equipment_id')
|
|
category_id = request.GET.get('category_id') or request.GET.get('asset_category_id')
|
|
if category_id:
|
|
try:
|
|
base_qs = base_qs.filter(asset_category_id=int(category_id))
|
|
except (TypeError, ValueError):
|
|
pass
|
|
lon = request.GET.get('lon')
|
|
lat = request.GET.get('lat')
|
|
radius = float(request.GET.get('radius', 50.0))
|
|
|
|
if location_ct and location_id:
|
|
base_qs = base_qs.filter(
|
|
locations__content_type_id=location_ct,
|
|
locations__object_id=location_id
|
|
)
|
|
|
|
elif equipment_ct and equipment_id:
|
|
base_qs = base_qs.filter(
|
|
interventionasset__content_type_id=equipment_ct,
|
|
interventionasset__object_id=equipment_id
|
|
)
|
|
|
|
elif lon and lat:
|
|
try:
|
|
pt = Point(float(lon), float(lat), srid=4326)
|
|
pt.transform(3812)
|
|
base_qs = base_qs.filter(geom__dwithin=(pt, D(m=radius)))
|
|
except Exception:
|
|
return JsonResponse({'total': 0, 'total_recent': 0})
|
|
|
|
else:
|
|
return JsonResponse({'total': 0, 'total_recent': 0})
|
|
|
|
# Interventions en cours
|
|
total = base_qs.filter(status__in=LINKED_TARGET_STATUSES).count()
|
|
|
|
# Interventions récentes terminées (3 derniers mois)
|
|
three_months_ago = timezone.now() - timedelta(days=90)
|
|
total_recent = base_qs.filter(
|
|
status__in=LINKED_RECENT_STATUSES
|
|
).annotate(
|
|
effective_end=Coalesce('end_time', 'planned_end_time', 'expected_end_time', 'planned_begin_time', 'creation_time', output_field=DateTimeField())
|
|
).filter(
|
|
effective_end__gte=three_months_ago
|
|
).count()
|
|
|
|
return JsonResponse({'total': total, 'total_recent': total_recent})
|
|
|
|
|
|
def intervention_summary_modal(request, intervention_id):
|
|
intervention = get_object_or_404(
|
|
Intervention.objects.annotate(
|
|
effective_end=Coalesce(
|
|
'end_time',
|
|
'planned_end_time',
|
|
'expected_end_time',
|
|
'creation_time',
|
|
output_field=DateTimeField(),
|
|
),
|
|
),
|
|
pk=intervention_id,
|
|
)
|
|
|
|
if not can_view_intervention_summary(request.user, intervention):
|
|
return HttpResponseForbidden(_("Vous n'avez pas la permission de voir cette intervention."))
|
|
|
|
work_done_notes = (
|
|
InterventionNote.objects
|
|
.filter(intervention=intervention, note_type=InterventionNote.NoteType.WORK_DONE)
|
|
.select_related('note_author', 'updated_by')
|
|
.order_by('-note_time')
|
|
)
|
|
|
|
summary_documents = (
|
|
intervention.documents
|
|
.select_related('uploaded_by')
|
|
.order_by('-upload_date')
|
|
)
|
|
|
|
variant = request.GET.get('variant')
|
|
template_name = (
|
|
'interventions/partials/intervention_summary_modal_body_mobile.html'
|
|
if variant == 'mobile'
|
|
else 'interventions/partials/intervention_summary_modal_body.html'
|
|
)
|
|
|
|
return render(request, template_name, {
|
|
'intervention': intervention,
|
|
'work_done_notes': work_done_notes,
|
|
'summary_documents': summary_documents,
|
|
})
|
|
|
|
|
|
def linked_interventions_list(request):
|
|
base_qs = filter_viewable_interventions_for_user(request.user)
|
|
|
|
thematic_code = request.GET.get('thematic')
|
|
if thematic_code:
|
|
base_qs = base_qs.filter(thematic__code=thematic_code)
|
|
|
|
location_ct = request.GET.get('location_ct')
|
|
location_id = request.GET.get('location_id')
|
|
equipment_ct = request.GET.get('equipment_ct')
|
|
equipment_id = request.GET.get('equipment_id')
|
|
category_id = request.GET.get('category_id') or request.GET.get('asset_category_id')
|
|
if category_id:
|
|
try:
|
|
base_qs = base_qs.filter(asset_category_id=int(category_id))
|
|
except (TypeError, ValueError):
|
|
pass
|
|
lon = request.GET.get('lon')
|
|
lat = request.GET.get('lat')
|
|
radius = float(request.GET.get('radius', 50.0))
|
|
list_type = request.GET.get('list_type', 'ongoing') # 'ongoing' ou 'recent'
|
|
|
|
if location_ct and location_id:
|
|
base_qs = base_qs.filter(
|
|
locations__content_type_id=location_ct,
|
|
locations__object_id=location_id
|
|
)
|
|
elif equipment_ct and equipment_id:
|
|
base_qs = base_qs.filter(
|
|
interventionasset__content_type_id=equipment_ct,
|
|
interventionasset__object_id=equipment_id
|
|
)
|
|
elif lon and lat:
|
|
try:
|
|
pt = Point(float(lon), float(lat), srid=4326)
|
|
pt.transform(3812)
|
|
base_qs = base_qs.filter(geom__dwithin=(pt, D(m=radius)))
|
|
except Exception:
|
|
return JsonResponse({'items': [], 'counts': {}})
|
|
|
|
# ⚠️ Si les jointures du dessus peuvent dupliquer des interventions, protège avec distinct()
|
|
base_qs = base_qs.distinct()
|
|
|
|
# Filtrer selon le type de liste demandé
|
|
if list_type == 'recent':
|
|
# Interventions récentes terminées (3 derniers mois)
|
|
three_months_ago = timezone.now() - timedelta(days=90)
|
|
filtered_qs = base_qs.filter(
|
|
status__in=LINKED_RECENT_STATUSES
|
|
).annotate(
|
|
effective_end=Coalesce('end_time', 'planned_end_time', 'expected_end_time', 'planned_begin_time', 'creation_time', output_field=DateTimeField())
|
|
).filter(
|
|
effective_end__gte=three_months_ago
|
|
)
|
|
# ✅ Comptages pour interventions récentes
|
|
counts = filtered_qs.aggregate(
|
|
finished=Count('id', filter=Q(status='finished'), distinct=True),
|
|
processed=Count('id', filter=Q(status='processed'), distinct=True),
|
|
corrected=Count('id', filter=Q(status='corrected'), distinct=True),
|
|
validated=Count('id', filter=Q(status='validated'), distinct=True),
|
|
invoiced=Count('id', filter=Q(status='invoiced'), distinct=True),
|
|
closed=Count('id', filter=Q(status='closed'), distinct=True),
|
|
canceled=Count('id', filter=Q(status='canceled'), distinct=True),
|
|
)
|
|
else:
|
|
# Interventions en cours (par défaut)
|
|
filtered_qs = base_qs.filter(status__in=LINKED_TARGET_STATUSES)
|
|
# ✅ Comptages AVANT tout slice, en une requête
|
|
counts = filtered_qs.aggregate(
|
|
to_be_processed=Count('id', filter=Q(status='to_be_processed'), distinct=True),
|
|
in_progress=Count('id', filter=Q(status='in_progress'), distinct=True),
|
|
on_pause=Count('id', filter=Q(status='on_pause'), distinct=True),
|
|
)
|
|
|
|
# ✅ Liste (slice uniquement ici)
|
|
items_qs = (
|
|
filtered_qs.select_related('assigned_provider')
|
|
.order_by('-creation_time')
|
|
)[:200]
|
|
|
|
items = [{
|
|
'id': itv.id,
|
|
'code': itv.code,
|
|
'title': itv.title,
|
|
'status': itv.status,
|
|
'begin_time': dj_format_date(itv.begin_time or itv.planned_begin_time, 'd/m/Y H:i') if (itv.begin_time or itv.planned_begin_time) else '',
|
|
'url': reverse('interventions:intervention_detail', kwargs={'intervention_id': itv.id}),
|
|
} for itv in items_qs]
|
|
|
|
return JsonResponse({'items': items, 'counts': counts})
|
|
|
|
|
|
def linked_interventions_geojson(request):
|
|
"""
|
|
Retourne un GeoJSON FeatureCollection avec toutes les interventions liées
|
|
(en cours + récentes terminées), pour affichage sur la carte.
|
|
Les points n'ont pas de position si lon/lat est null.
|
|
"""
|
|
base_qs = filter_viewable_interventions_for_user(request.user)
|
|
|
|
thematic_code = request.GET.get('thematic')
|
|
if thematic_code:
|
|
base_qs = base_qs.filter(thematic__code=thematic_code)
|
|
|
|
location_ct = request.GET.get('location_ct')
|
|
location_id = request.GET.get('location_id')
|
|
equipment_ct = request.GET.get('equipment_ct')
|
|
equipment_id = request.GET.get('equipment_id')
|
|
category_id = request.GET.get('category_id') or request.GET.get('asset_category_id')
|
|
if category_id:
|
|
try:
|
|
base_qs = base_qs.filter(asset_category_id=int(category_id))
|
|
except (TypeError, ValueError):
|
|
pass
|
|
lon = request.GET.get('lon')
|
|
lat = request.GET.get('lat')
|
|
radius = float(request.GET.get('radius', 200.0))
|
|
|
|
if location_ct and location_id:
|
|
base_qs = base_qs.filter(
|
|
locations__content_type_id=location_ct,
|
|
locations__object_id=location_id
|
|
)
|
|
elif equipment_ct and equipment_id:
|
|
base_qs = base_qs.filter(
|
|
interventionasset__content_type_id=equipment_ct,
|
|
interventionasset__object_id=equipment_id
|
|
)
|
|
elif lon and lat:
|
|
try:
|
|
pt = Point(float(lon), float(lat), srid=4326)
|
|
pt.transform(3812)
|
|
base_qs = base_qs.filter(geom__dwithin=(pt, D(m=radius)))
|
|
except Exception:
|
|
return JsonResponse({'type': 'FeatureCollection', 'features': []})
|
|
else:
|
|
return JsonResponse({'type': 'FeatureCollection', 'features': []})
|
|
|
|
base_qs = base_qs.distinct()
|
|
|
|
exclude_id = request.GET.get('exclude_id')
|
|
if exclude_id:
|
|
try:
|
|
base_qs = base_qs.exclude(pk=int(exclude_id))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
three_months_ago = timezone.now() - timedelta(days=90)
|
|
|
|
# Ongoing
|
|
ongoing_qs = base_qs.filter(
|
|
status__in=LINKED_TARGET_STATUSES,
|
|
lon__isnull=False,
|
|
lat__isnull=False,
|
|
).values('id', 'code', 'title', 'status', 'lon', 'lat')[:500]
|
|
|
|
# Recent (terminated, within 3 months)
|
|
recent_qs = base_qs.filter(
|
|
status__in=LINKED_RECENT_STATUSES,
|
|
lon__isnull=False,
|
|
lat__isnull=False,
|
|
).annotate(
|
|
effective_end=Coalesce('end_time', 'planned_end_time', 'expected_end_time', 'planned_begin_time', 'creation_time', output_field=DateTimeField())
|
|
).filter(effective_end__gte=three_months_ago).values('id', 'code', 'title', 'status', 'lon', 'lat')[:500]
|
|
|
|
features = []
|
|
seen_ids = set()
|
|
|
|
for itv in ongoing_qs:
|
|
seen_ids.add(itv['id'])
|
|
features.append({
|
|
'type': 'Feature',
|
|
'geometry': {'type': 'Point', 'coordinates': [itv['lon'], itv['lat']]},
|
|
'properties': {
|
|
'id': itv['id'],
|
|
'code': itv['code'],
|
|
'title': itv['title'],
|
|
'status': itv['status'],
|
|
'layer': 'ongoing',
|
|
'url': reverse('interventions:intervention_detail', kwargs={'intervention_id': itv['id']}),
|
|
},
|
|
})
|
|
|
|
for itv in recent_qs:
|
|
if itv['id'] in seen_ids:
|
|
continue
|
|
features.append({
|
|
'type': 'Feature',
|
|
'geometry': {'type': 'Point', 'coordinates': [itv['lon'], itv['lat']]},
|
|
'properties': {
|
|
'id': itv['id'],
|
|
'code': itv['code'],
|
|
'title': itv['title'],
|
|
'status': itv['status'],
|
|
'layer': 'recent',
|
|
'url': reverse('interventions:intervention_detail', kwargs={'intervention_id': itv['id']}),
|
|
},
|
|
})
|
|
|
|
return JsonResponse({'type': 'FeatureCollection', 'features': features})
|
|
|
|
|
|
def intervention_add_occupation(request, intervention_id):
|
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
|
members_qs = _get_available_occupation_members(request.user, intervention)
|
|
form = InterventionOccupationForm(request.POST, members_queryset=members_qs, user=request.user)
|
|
|
|
if form.is_valid():
|
|
occupation = form.save(commit=False)
|
|
occupation.intervention = intervention
|
|
occupation.created_by = request.user
|
|
occupation.save()
|
|
occupation.refresh_from_db()
|
|
|
|
return JsonResponse({
|
|
'success': True,
|
|
'occupation': _serialize_occupation(occupation),
|
|
'total_minutes': intervention.get_total_occupation_minutes(),
|
|
})
|
|
|
|
return JsonResponse({'success': False, 'errors': form.errors}, status=400)
|
|
|
|
|
|
def intervention_update_occupation(request, intervention_id, occupation_id):
|
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
|
occupation = get_object_or_404(InterventionOccupation, pk=occupation_id, intervention=intervention)
|
|
members_qs = _get_available_occupation_members(request.user, intervention)
|
|
form = InterventionOccupationForm(request.POST, instance=occupation, members_queryset=members_qs, user=request.user)
|
|
|
|
if form.is_valid():
|
|
occupation = form.save(commit=False)
|
|
occupation.updated_by = request.user
|
|
occupation.save()
|
|
occupation.refresh_from_db()
|
|
|
|
return JsonResponse({
|
|
'success': True,
|
|
'occupation': _serialize_occupation(occupation),
|
|
'total_minutes': intervention.get_total_occupation_minutes(),
|
|
})
|
|
|
|
return JsonResponse({'success': False, 'errors': form.errors}, status=400)
|
|
|