1548 lines
No EOL
65 KiB
Python
1548 lines
No EOL
65 KiB
Python
from django.shortcuts import get_object_or_404
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.db.models import Q
|
|
from django.utils.translation import gettext as _
|
|
|
|
from functools import wraps
|
|
from datetime import timedelta
|
|
from django.utils import timezone
|
|
from django.shortcuts import get_object_or_404
|
|
|
|
from collections import defaultdict
|
|
from functools import reduce
|
|
from operator import or_
|
|
|
|
from common.models import (
|
|
UserConfig, UserContractAccess, UserThematics, Thematic,
|
|
UserThematicStatusPermission, UserContractStatusPermission,
|
|
)
|
|
from interventions.models import Intervention, InterventionTimeLine, STATUS_ORDERS, STATUS_CHOICES
|
|
from contracts.models import CompanyMember
|
|
|
|
# Ordered list of all status keys (used by get_allowed_statuses_for_user and admin UI)
|
|
ALL_STATUS_KEYS = list(STATUS_CHOICES.keys())
|
|
|
|
|
|
# Permissions for viewing intervention
|
|
VIEW_ROLE_STATUS_PERMISSIONS = {
|
|
'manager': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'technician': ['in_preparation','to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished'],
|
|
'external_manager': ['in_preparation','to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'controller': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'operator': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'admin': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'observer': ['to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'viewer': ['to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'editor': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'top_manager': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
'inspector': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
|
|
}
|
|
|
|
|
|
ALLOWED_TRANSITIONS = {
|
|
'in_preparation': ['to_be_approved', 'to_be_planned', 'to_be_processed', 'canceled'],
|
|
'to_be_approved': ['to_be_planned', 'to_be_processed', 'canceled'],
|
|
'to_be_planned': ['to_be_processed', 'canceled'],
|
|
'to_be_processed': ['to_be_planned', 'assigned', 'in_progress', 'processed', 'closed', 'canceled'],
|
|
'assigned': ['to_be_processed', 'in_progress', 'canceled'],
|
|
'in_progress': ['on_pause', 'finished', 'canceled'],
|
|
'on_pause': ['to_be_planned', 'to_be_processed', 'in_progress', 'finished', 'canceled'],
|
|
'finished': ['to_be_planned', 'to_be_processed', 'on_pause', 'processed', 'canceled'],
|
|
'processed': ['to_be_corrected', 'validated', 'closed'],
|
|
'to_be_corrected': ['to_be_planned', 'corrected'],
|
|
'corrected': ['to_be_corrected','validated'],
|
|
'validated': ['invoiced', 'closed'],
|
|
'invoiced': ['closed'],
|
|
'closed': [],
|
|
'canceled': [],
|
|
}
|
|
|
|
# Droits par rôle = sous-ensemble du graph
|
|
ROLE_TRANSITIONS = {
|
|
"manager": [
|
|
("in_preparation","to_be_approved"),
|
|
("in_preparation","to_be_planned"),
|
|
("in_preparation","to_be_processed"),
|
|
("in_preparation","canceled"),
|
|
("to_be_approved","to_be_planned"),
|
|
("to_be_approved","to_be_processed"),
|
|
("to_be_approved","canceled"),
|
|
("to_be_planned", "to_be_processed"),
|
|
("to_be_planned", "canceled"),
|
|
('to_be_processed', 'canceled'),
|
|
("processed","to_be_corrected"),
|
|
("processed","validated"),
|
|
("corrected","validated"),
|
|
("invoiced","closed"),
|
|
],
|
|
"technician": [
|
|
("in_preparation","to_be_approved"),
|
|
("in_preparation","to_be_planned"),
|
|
("in_preparation","to_be_processed"),
|
|
("in_preparation","canceled"),
|
|
("to_be_processed","assigned"),
|
|
("assigned","in_progress"),
|
|
("assigned","to_be_processed"),
|
|
("to_be_processed","in_progress"),
|
|
("in_progress","on_pause"),
|
|
("in_progress","finished"),
|
|
("on_pause","in_progress"),
|
|
("on_pause","finished"),
|
|
],
|
|
"external_manager": [
|
|
("in_preparation","to_be_approved"),
|
|
("in_preparation","to_be_planned"),
|
|
("in_preparation","to_be_processed"),
|
|
("in_preparation","canceled"),
|
|
("to_be_approved","canceled"),
|
|
("to_be_planned", "to_be_processed"),
|
|
("to_be_planned","canceled"),
|
|
("to_be_processed","assigned"),
|
|
("to_be_processed","in_progress"),
|
|
("to_be_processed","to_be_planned"),
|
|
("to_be_processed","canceled"),
|
|
("assigned","in_progress"),
|
|
("assigned","to_be_processed"),
|
|
("assigned","canceled"),
|
|
("in_progress","on_pause"),
|
|
("in_progress","finished"),
|
|
("in_progress","canceled"),
|
|
("on_pause","in_progress"),
|
|
("on_pause","finished"),
|
|
("on_pause","to_be_planned"),
|
|
("on_pause","to_be_processed"),
|
|
("on_pause","canceled"),
|
|
("finished","processed"),
|
|
("finished","to_be_processed"),
|
|
("finished","on_pause"),
|
|
("finished","canceled"),
|
|
("to_be_corrected","corrected"),
|
|
],
|
|
"controller": [
|
|
("in_preparation","to_be_approved"),
|
|
("in_preparation","to_be_planned"),
|
|
("in_preparation","to_be_processed"),
|
|
("to_be_approved","to_be_planned"),
|
|
("to_be_approved","to_be_processed"),
|
|
("to_be_approved","canceled"),
|
|
("to_be_planned", "to_be_processed"),
|
|
("to_be_planned", "canceled"),
|
|
('to_be_processed', 'canceled'),
|
|
("processed","validated"),
|
|
("processed","to_be_corrected"),
|
|
("corrected","to_be_corrected"),
|
|
("corrected","validated"),
|
|
],
|
|
"operator": [
|
|
("in_preparation","to_be_approved"),
|
|
("in_preparation","to_be_planned"),
|
|
("in_preparation","to_be_processed"),
|
|
("in_preparation","canceled"),
|
|
("to_be_approved","to_be_planned"),
|
|
("to_be_approved","to_be_processed"),
|
|
("to_be_approved","canceled"),
|
|
("to_be_planned", "to_be_processed"),
|
|
("to_be_planned", "canceled"),
|
|
('to_be_processed', 'canceled'),
|
|
],
|
|
"admin": "ALL", # interprété comme toutes les transitions du graph
|
|
}
|
|
|
|
TRANSITION_LABEL = {
|
|
"to_be_approved": _("Envoyer pour approbation"),
|
|
"to_be_planned": _("Envoyer pour planification"),
|
|
"to_be_processed": _("Envoyer pour traitement"),
|
|
"assigned": _("Prendre en charge"),
|
|
"in_progress": _("Démarrer l'intervention"),
|
|
"on_pause": _("Mettre en pause"),
|
|
"finished": _("Terminer l'intervention"),
|
|
"processed": _("Envoyer pour validation"),
|
|
"to_be_corrected": _("Demander correction"),
|
|
"corrected": _("Corriger"),
|
|
"validated": _("Valider"),
|
|
"invoiced": _("Facturer"),
|
|
"closed": _("Finaliser"),
|
|
"canceled": _("Annuler"),
|
|
}
|
|
|
|
def get_allowed_statuses_for_user(user):
|
|
"""
|
|
Retourne la liste des statuts que l'utilisateur peut voir,
|
|
basée sur les nouvelles permissions par statut (thématique + contrat).
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return []
|
|
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return ALL_STATUS_KEYS
|
|
|
|
statuses = set()
|
|
statuses.update(
|
|
UserThematicStatusPermission.objects.filter(
|
|
user_thematic__user_config=user_config,
|
|
can_view=True,
|
|
).values_list('status', flat=True)
|
|
)
|
|
statuses.update(
|
|
UserContractStatusPermission.objects.filter(
|
|
user_contract__user_config=user_config,
|
|
can_view=True,
|
|
).values_list('status', flat=True)
|
|
)
|
|
return list(statuses)
|
|
|
|
|
|
|
|
def can_view_intervention(user, intervention):
|
|
"""
|
|
Vérifie si l'utilisateur peut voir l'intervention.
|
|
|
|
Priorité : contrat > thématique.
|
|
Si une ligne UserContractStatusPermission existe pour ce contrat et ce statut,
|
|
c'est cette valeur qui fait foi (même si False) — pas de fallback thématique.
|
|
Si aucune ligne n'existe pour ce statut dans le contrat, on tombe sur la thématique.
|
|
En dernier recours : prestataire assigné (CompanyMember).
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return True
|
|
|
|
if intervention.created_by_id == user.id:
|
|
return True
|
|
|
|
# -- Restriction de visibilité quotidienne et par équipe --
|
|
company_member = CompanyMember.objects.filter(user=user).prefetch_related('teams').first()
|
|
if company_member:
|
|
user_roles = set(user_config.roles.values_list('name', flat=True))
|
|
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
|
|
if not (user_roles & privileged_roles):
|
|
# 1. Restriction de visibilité quotidienne par équipe
|
|
restricted_teams = company_member.teams.filter(restrict_today_interventions_visibility=True)
|
|
if restricted_teams.exists():
|
|
local_now = timezone.localtime(timezone.now())
|
|
today = local_now.date()
|
|
|
|
# Vérifier la restriction de date (doit être aujourd'hui)
|
|
itv_date = (
|
|
intervention.begin_time or
|
|
intervention.planned_begin_time or
|
|
intervention.expected_begin_time or
|
|
intervention.creation_time
|
|
)
|
|
if itv_date:
|
|
if timezone.is_aware(itv_date):
|
|
itv_date = timezone.localtime(itv_date)
|
|
itv_date_only = itv_date.date()
|
|
else:
|
|
itv_date_only = None
|
|
|
|
if itv_date_only != today:
|
|
return False
|
|
|
|
# Vérifier la restriction d'heure
|
|
start_hours = [t.visibility_start_hour for t in restricted_teams if t.visibility_start_hour is not None]
|
|
if start_hours:
|
|
min_start_hour = min(start_hours)
|
|
if local_now.time() < min_start_hour:
|
|
return False
|
|
|
|
# 2. Restriction aux interventions assignées
|
|
assigned_only_teams = company_member.teams.filter(restrict_to_assigned_interventions=True)
|
|
if assigned_only_teams.exists():
|
|
unrestricted_teams = company_member.teams.filter(restrict_to_assigned_interventions=False)
|
|
unrestricted_companies = set(unrestricted_teams.values_list('company_id', flat=True))
|
|
|
|
is_assigned_to_user = (intervention.assigned_member == company_member)
|
|
is_assigned_to_user_team = (intervention.assigned_team in company_member.teams.all())
|
|
is_unrestricted_company = (intervention.assigned_provider_id in unrestricted_companies)
|
|
|
|
if not (is_assigned_to_user or is_assigned_to_user_team or is_unrestricted_company):
|
|
return False
|
|
|
|
contract = intervention.contract
|
|
thematic = intervention.thematic
|
|
status = intervention.status
|
|
|
|
# -- Priorité contrat --
|
|
if contract:
|
|
uca = UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
contract=contract,
|
|
).first()
|
|
if uca:
|
|
perm = UserContractStatusPermission.objects.filter(
|
|
user_contract=uca,
|
|
status=status,
|
|
).first()
|
|
if perm is not None:
|
|
return perm.can_view # ligne définie → valeur finale, pas de fallback thématique
|
|
# Aucune ligne pour ce statut → fallback sur les droits par rôle (via l'accès contrat)
|
|
uca_roles = set(user_config.roles.values_list('name', flat=True))
|
|
for role in uca_roles:
|
|
if role in VIEW_ROLE_STATUS_PERMISSIONS and status in VIEW_ROLE_STATUS_PERMISSIONS[role]:
|
|
return True
|
|
|
|
# -- Accès thématique --
|
|
if thematic:
|
|
ut = UserThematics.objects.filter(
|
|
user_config=user_config,
|
|
thematic=thematic,
|
|
).first()
|
|
if ut:
|
|
return UserThematicStatusPermission.objects.filter(
|
|
user_thematic=ut,
|
|
status=status,
|
|
can_view=True,
|
|
).exists()
|
|
|
|
# -- Fallback prestataire assigné --
|
|
assigned_provider = intervention.assigned_provider
|
|
if assigned_provider:
|
|
company_member = CompanyMember.objects.filter(user=user).prefetch_related('teams__company').first()
|
|
if company_member:
|
|
return company_member.teams.filter(company=assigned_provider).exists()
|
|
|
|
return False
|
|
|
|
|
|
def can_view_intervention_summary(user, intervention):
|
|
"""
|
|
Vérifie si l'utilisateur peut voir le résumé de l'intervention (vue allégée).
|
|
Basé sur les permissions thématique uniquement (accès résumé = droit de visibilité
|
|
thématique sur le statut courant).
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return True
|
|
|
|
if not intervention.thematic:
|
|
return False
|
|
|
|
ut = UserThematics.objects.filter(
|
|
user_config=user_config,
|
|
thematic=intervention.thematic,
|
|
).first()
|
|
if not ut:
|
|
return False
|
|
|
|
return UserThematicStatusPermission.objects.filter(
|
|
user_thematic=ut,
|
|
status=intervention.status,
|
|
can_view=True,
|
|
).exists()
|
|
|
|
def intervention_permission_required(view_func):
|
|
@wraps(view_func)
|
|
def _wrapped_view(request, intervention_id, *args, **kwargs):
|
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
|
if not can_view_intervention(request.user, intervention):
|
|
raise PermissionDenied(_("Vous n'avez pas la permission de voir cette intervention."))
|
|
return view_func(request, intervention_id, *args, **kwargs)
|
|
return _wrapped_view
|
|
|
|
|
|
def can_control_intervention(user, intervention):
|
|
"""
|
|
Vérifie si l'utilisateur peut contrôler (valider/corriger) l'intervention.
|
|
Réservé aux utilisateurs internes ayant le droit d'édition sur le statut courant.
|
|
"""
|
|
if intervention.status not in ('processed', 'corrected'):
|
|
return False
|
|
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if not user_config.is_intern:
|
|
return False
|
|
|
|
return can_edit_intervention(user, intervention, user_config=user_config)
|
|
|
|
|
|
|
|
def get_allowed_update_fields(user, intervention):
|
|
"""
|
|
Get the fields that the user can update based on their roles and the intervention's status.
|
|
"""
|
|
allowed_fields = set()
|
|
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return []
|
|
|
|
if not can_edit_intervention(user, intervention, user_config=user_config):
|
|
return []
|
|
|
|
user_roles = user_config.roles.all().values_list('name', flat=True)
|
|
|
|
# Si l'utilisateur n'a pas de rôle 'admin', 'operator', 'manager', 'controller', 'external_manager', 'technician' ou 'inspector', il ne peut rien mettre à jour
|
|
if not any(role in ['admin', 'operator', 'manager', 'controller', 'external_manager', 'technician', 'inspector'] for role in user_roles):
|
|
return []
|
|
|
|
|
|
# Notes et Documents
|
|
allowed_fields.update(['notes', 'documents'])
|
|
|
|
# Opérations (statuts, mesures, commentaires) - uniquement pour technician, external_manager et inspector
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['technician', 'external_manager', 'admin', 'inspector']):
|
|
allowed_fields.add('operations')
|
|
|
|
|
|
# Prestataire, Contrat et Type de maintenance (uniquement en phase initiale)
|
|
# Exception : si la thématique autorise la planification sans contrat, ces champs
|
|
# restent également modifiables au statut 'to_be_planned'.
|
|
_thematic_allows_planning_without_contract = getattr(intervention.thematic, 'allow_planning_without_contract', False)
|
|
_initial_statuses = ('in_preparation', 'to_be_approved')
|
|
if _thematic_allows_planning_without_contract:
|
|
_initial_statuses = ('in_preparation', 'to_be_approved', 'to_be_planned')
|
|
if intervention.status in _initial_statuses and user_config.is_intern:
|
|
allowed_fields.update(['assigned_provider', 'contract', 'maintain_type'])
|
|
|
|
# Localisation : modifiable par les internes sur toutes les interventions non validées
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and user_config.is_intern:
|
|
allowed_fields.update(['location', 'address', 'lat', 'lon', 'intersections'])
|
|
|
|
# Type d'intervention, Osiris, PMO, Assets
|
|
if intervention.status_order < STATUS_ORDERS.get('validated'):
|
|
allowed_fields.update(['assets'])
|
|
|
|
# Type d'intervention, Osiris, PMO, Assets
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'inspector']):
|
|
allowed_fields.update(['type', 'osiris', 'pmo_number'])
|
|
|
|
if intervention.status_order < STATUS_ORDERS.get('validated'):
|
|
allowed_fields.add('category')
|
|
|
|
# Source, Référence, Détail, FMS, Type d'origine et Symptôme
|
|
if intervention.status_order < STATUS_ORDERS.get('to_be_processed') and user_config.is_intern:
|
|
allowed_fields.update(['source_category', 'source_ref', 'source_detail', 'origin_fms_nbr', 'origin_type', 'symptom'])
|
|
|
|
# Priorité : modifiable par les internes tant que le statut n'est pas 'démarré' (in_progress)
|
|
if intervention.status_order < STATUS_ORDERS.get('in_progress') and user_config.is_intern:
|
|
allowed_fields.add('priority')
|
|
|
|
# Dossier de référence interne
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and user_config.is_intern and any(role in user_roles for role in ['admin', 'manager', 'controller']):
|
|
allowed_fields.update(['linked_folder'])
|
|
|
|
# Projet
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and user_config.is_intern and any(role in user_roles for role in ['admin', 'manager']):
|
|
allowed_fields.update(['project'])
|
|
|
|
# Réalisation et Planification (pour tous)
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'technician', 'inspector']):
|
|
allowed_fields.update(['realization', 'planification'])
|
|
|
|
# Assignation (seulement pour admin, external_manager, technician, inspector - PAS manager)
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'external_manager', 'technician', 'inspector']):
|
|
allowed_fields.add('member_assignment')
|
|
|
|
# Rapports de suivi
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and user_config.is_intern and any(role in user_roles for role in ['admin', 'manager', 'controller']):
|
|
allowed_fields.update(['follow_up_reports'])
|
|
|
|
# Bon de commande et Titre
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'inspector']):
|
|
allowed_fields.update(['order', 'title'])
|
|
|
|
# Postes
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'external_manager']):
|
|
allowed_fields.update(['posts'])
|
|
|
|
# Traitement via une autre intervention
|
|
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager']):
|
|
allowed_fields.add('processed_via_other_intervention')
|
|
|
|
return list(allowed_fields)
|
|
|
|
|
|
|
|
def get_visible_maintain_types_for_user(user):
|
|
"""
|
|
Retourne la liste des codes de types de maintenance visibles pour l'utilisateur.
|
|
Si l'utilisateur n'est pas membre d'une équipe ou si le membre n'a pas de restrictions,
|
|
retourne None (tous les types sont visibles).
|
|
"""
|
|
company_member = CompanyMember.objects.prefetch_related('teams', 'visible_maintain_types').filter(user=user).first()
|
|
if not company_member:
|
|
return None # Pas de restriction si pas membre d'une équipe
|
|
|
|
return company_member.get_visible_maintain_type_codes()
|
|
|
|
|
|
def apply_maintain_type_filter(queryset, user):
|
|
"""
|
|
Applique le filtre des types de maintenance visibles au queryset.
|
|
Si l'utilisateur n'a pas de restrictions, le queryset est retourné tel quel.
|
|
Les interventions assignées à l'utilisateur restent toujours visibles.
|
|
"""
|
|
visible_types = get_visible_maintain_types_for_user(user)
|
|
if visible_types is None:
|
|
return queryset # Pas de restriction
|
|
# Inclure les interventions du type visible OU celles assignées à l'utilisateur
|
|
return queryset.filter(
|
|
Q(maintain_type__in=visible_types) | Q(assigned_member__user=user)
|
|
)
|
|
|
|
|
|
def get_accessible_interventions_for_technician(user):
|
|
"""
|
|
Retourne un queryset des interventions que l'utilisateur a le droit de voir :
|
|
- Celles qui lui sont directement assignées
|
|
- Ou celles qui sont non assignées mais associées à la société à laquelle il appartient
|
|
- Filtré par les types de maintenance visibles pour l'utilisateur (sauf interventions assignées)
|
|
"""
|
|
base_qs = Intervention.objects.all()
|
|
|
|
company_member = CompanyMember.objects.prefetch_related('teams__company', 'visible_maintain_types').filter(user=user).first()
|
|
if company_member:
|
|
user_companies = company_member.teams.values_list('company_id', flat=True).distinct()
|
|
qs = base_qs.filter(
|
|
Q(assigned_member__user=user) |
|
|
Q(assigned_team__in=company_member.teams.all(), status__in=['to_be_processed', 'assigned']) |
|
|
Q(assigned_member__isnull=True, assigned_team__isnull=True, assigned_provider_id__in=user_companies, status__in=['to_be_processed', 'assigned'])
|
|
)
|
|
# Appliquer le filtre des types de maintenance visibles (sauf pour les interventions assignées)
|
|
visible_types = company_member.get_visible_maintain_type_codes()
|
|
if visible_types is not None:
|
|
qs = qs.filter(
|
|
Q(maintain_type__in=visible_types) | Q(assigned_member__user=user)
|
|
)
|
|
|
|
# -- Restriction de visibilité quotidienne par équipe --
|
|
user_config = UserConfig.objects.filter(user=user).first()
|
|
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
|
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
|
|
if not (user_roles & privileged_roles):
|
|
restricted_teams = company_member.teams.filter(restrict_today_interventions_visibility=True)
|
|
if restricted_teams.exists():
|
|
local_now = timezone.localtime(timezone.now())
|
|
today = local_now.date()
|
|
|
|
# Vérifier la restriction d'heure
|
|
start_hours = [t.visibility_start_hour for t in restricted_teams if t.visibility_start_hour is not None]
|
|
if start_hours:
|
|
min_start_hour = min(start_hours)
|
|
if local_now.time() < min_start_hour:
|
|
return base_qs.none()
|
|
|
|
# Filtrer sur les interventions du jour uniquement
|
|
from django.db.models.functions import Coalesce, Cast
|
|
from django.db.models import DateTimeField, DateField
|
|
effective_begin_expr = Coalesce(
|
|
'begin_time',
|
|
'planned_begin_time',
|
|
'expected_begin_time',
|
|
'creation_time',
|
|
output_field=DateTimeField()
|
|
)
|
|
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
|
|
qs = qs.filter(effective_begin_date=today)
|
|
return qs
|
|
else:
|
|
qs = base_qs.filter(assigned_member__user=user)
|
|
# -- Restriction de visibilité quotidienne par équipe --
|
|
user_config = UserConfig.objects.filter(user=user).first()
|
|
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
|
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
|
|
if not (user_roles & privileged_roles):
|
|
company_member_basic = CompanyMember.objects.filter(user=user).first()
|
|
if company_member_basic:
|
|
restricted_teams = company_member_basic.teams.filter(restrict_today_interventions_visibility=True)
|
|
if restricted_teams.exists():
|
|
local_now = timezone.localtime(timezone.now())
|
|
today = local_now.date()
|
|
|
|
# Vérifier la restriction d'heure
|
|
start_hours = [t.visibility_start_hour for t in restricted_teams if t.visibility_start_hour is not None]
|
|
if start_hours:
|
|
min_start_hour = min(start_hours)
|
|
if local_now.time() < min_start_hour:
|
|
return base_qs.none()
|
|
|
|
# Filtrer sur les interventions du jour uniquement
|
|
from django.db.models.functions import Coalesce, Cast
|
|
from django.db.models import DateTimeField, DateField
|
|
effective_begin_expr = Coalesce(
|
|
'begin_time',
|
|
'planned_begin_time',
|
|
'expected_begin_time',
|
|
'creation_time',
|
|
output_field=DateTimeField()
|
|
)
|
|
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
|
|
qs = qs.filter(effective_begin_date=today)
|
|
return qs
|
|
|
|
|
|
def filter_viewable_interventions_for_user(user, limit_to_default_thematic=False):
|
|
"""
|
|
Renvoie un queryset des interventions visibles par l'utilisateur
|
|
selon les règles de can_view_intervention() avec le nouveau système de permissions.
|
|
|
|
Logique (priorité contrat > thématique) :
|
|
- Si une ligne UserContractStatusPermission existe pour (user_config, contrat, statut)
|
|
→ la valeur can_view de cette ligne fait foi — pas de fallback thématique.
|
|
- Si aucune ligne n'existe pour ce statut dans le contrat
|
|
→ fallback sur UserThematicStatusPermission(status).can_view
|
|
- Fallback : prestataire assigné (CompanyMember)
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.select_related('user').prefetch_related('roles').get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return Intervention.objects.none()
|
|
|
|
# Admin ou top_manager → accès global
|
|
if user_config.roles.filter(name__in=['admin', 'top_manager']).exists():
|
|
qs = Intervention.objects.all()
|
|
if limit_to_default_thematic and user_config.default_thematic:
|
|
qs = qs.filter(thematic=user_config.default_thematic)
|
|
return qs
|
|
|
|
# -- Précharger les permissions depuis les petites tables de permissions --
|
|
# Remplace les sous-requêtes corrélées (Exists + OuterRef) par des ensembles
|
|
# calculés une seule fois en Python, traduits en conditions Q non-corrélées.
|
|
|
|
# 1. Permissions contrat-statut pour ce user_config
|
|
cp_rows = list(
|
|
UserContractStatusPermission.objects.filter(
|
|
user_contract__user_config=user_config,
|
|
).values_list('user_contract__contract_id', 'status', 'can_view')
|
|
)
|
|
explicit_cs_allowed = set() # (contract_id, status) avec can_view=True
|
|
denied_contracts_by_status = defaultdict(set) # status → contract_ids avec can_view=False
|
|
for c_id, st, cv in cp_rows:
|
|
if cv:
|
|
explicit_cs_allowed.add((c_id, st))
|
|
else:
|
|
denied_contracts_by_status[st].add(c_id)
|
|
|
|
# 2. Permissions thématique-statut (can_view=True uniquement)
|
|
thematic_ids_by_status = defaultdict(set) # status → set de thematic_id autorisés
|
|
for t_id, st in UserThematicStatusPermission.objects.filter(
|
|
user_thematic__user_config=user_config,
|
|
can_view=True,
|
|
).values_list('user_thematic__thematic_id', 'status'):
|
|
thematic_ids_by_status[st].add(t_id)
|
|
|
|
# -- Construire les conditions Q sans sous-requêtes corrélées --
|
|
# Règle (priorité contrat > thématique) :
|
|
# (a) Ligne contrat can_view=True → visible
|
|
# (b) Ligne contrat can_view=False → refusé, pas de fallback
|
|
# (c)+(d) Si limit_interventions_to_contracts=False : fallback thématique
|
|
# pour toute intervention sans refus explicite de contrat.
|
|
conditions = []
|
|
|
|
# Cas (a) : contrat + can_view=True explicite
|
|
for c_id, st in explicit_cs_allowed:
|
|
conditions.append(Q(contract_id=c_id, status=st))
|
|
|
|
# Cas (c)+(d) : fallback thématique — uniquement si l'utilisateur n'est PAS
|
|
# limité aux contrats. Quand limit_interventions_to_contracts=True (défaut),
|
|
# seules les permissions contrat explicites (cas a) s'appliquent.
|
|
if not user_config.limit_interventions_to_contracts:
|
|
for st, t_ids in thematic_ids_by_status.items():
|
|
if not t_ids:
|
|
continue
|
|
base_q = Q(status=st, thematic_id__in=sorted(t_ids))
|
|
denied_for_st = denied_contracts_by_status.get(st, set())
|
|
if denied_for_st:
|
|
# Exclure les interventions dont le contrat est explicitement refusé.
|
|
# contract=NULL doit être géré séparément : NULL NOT IN (...) = UNKNOWN en SQL.
|
|
base_q &= Q(contract__isnull=True) | ~Q(contract_id__in=sorted(denied_for_st))
|
|
conditions.append(base_q)
|
|
|
|
# Fallback prestataire assigné (technicians via CompanyMember)
|
|
company_member = (
|
|
CompanyMember.objects
|
|
.prefetch_related('teams__company', 'visible_maintain_types')
|
|
.filter(user=user)
|
|
.first()
|
|
)
|
|
if company_member:
|
|
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
|
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
|
|
|
|
restricted_teams = company_member.teams.filter(restrict_to_assigned_interventions=True)
|
|
if restricted_teams.exists() and not (user_roles & privileged_roles):
|
|
unrestricted_teams = company_member.teams.filter(restrict_to_assigned_interventions=False)
|
|
unrestricted_companies = list(unrestricted_teams.values_list('company_id', flat=True).distinct())
|
|
|
|
company_q = Q(assigned_member=company_member) | Q(assigned_team__in=list(company_member.teams.all()))
|
|
if unrestricted_companies:
|
|
company_q |= Q(assigned_member__isnull=True, assigned_team__isnull=True, assigned_provider_id__in=unrestricted_companies)
|
|
else:
|
|
user_companies = company_member.teams.values_list('company_id', flat=True).distinct()
|
|
company_q = Q(assigned_provider_id__in=user_companies)
|
|
|
|
if user_config.limit_interventions_to_contracts:
|
|
# Limiter aux contrats configurés pour cet utilisateur afin d'exclure
|
|
# les interventions d'autres contrats de la même compagnie.
|
|
allowed_contract_ids = set(
|
|
UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
can_view_interventions=True,
|
|
).values_list('contract_id', flat=True)
|
|
)
|
|
company_q &= Q(contract__isnull=True) | Q(contract_id__in=sorted(allowed_contract_ids))
|
|
conditions.append(company_q)
|
|
|
|
conditions.append(Q(created_by=user))
|
|
|
|
if not conditions:
|
|
return Intervention.objects.none()
|
|
|
|
q = reduce(or_, conditions)
|
|
qs = Intervention.objects.filter(q)
|
|
|
|
if limit_to_default_thematic and user_config.default_thematic:
|
|
qs = qs.filter(thematic=user_config.default_thematic)
|
|
|
|
if company_member:
|
|
qs = apply_maintain_type_filter(qs, user)
|
|
|
|
# -- Restriction de visibilité quotidienne par équipe --
|
|
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
|
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
|
|
if not (user_roles & privileged_roles):
|
|
# 1. Restriction aux interventions assignées par équipe
|
|
assigned_only_teams = company_member.teams.filter(restrict_to_assigned_interventions=True)
|
|
if assigned_only_teams.exists():
|
|
unrestricted_teams = company_member.teams.filter(restrict_to_assigned_interventions=False)
|
|
unrestricted_companies = list(unrestricted_teams.values_list('company_id', flat=True).distinct())
|
|
|
|
assign_filter = Q(assigned_member=company_member) | Q(assigned_team__in=list(company_member.teams.all()))
|
|
if unrestricted_companies:
|
|
assign_filter |= Q(assigned_provider_id__in=unrestricted_companies)
|
|
qs = qs.filter(assign_filter)
|
|
|
|
# 2. Restriction de visibilité quotidienne par équipe
|
|
restricted_teams = company_member.teams.filter(restrict_today_interventions_visibility=True)
|
|
if restricted_teams.exists():
|
|
local_now = timezone.localtime(timezone.now())
|
|
today = local_now.date()
|
|
|
|
# Vérifier la restriction d'heure
|
|
start_hours = [t.visibility_start_hour for t in restricted_teams if t.visibility_start_hour is not None]
|
|
if start_hours:
|
|
min_start_hour = min(start_hours)
|
|
if local_now.time() < min_start_hour:
|
|
return Intervention.objects.none()
|
|
|
|
# Filtrer sur les interventions du jour uniquement
|
|
from django.db.models.functions import Coalesce, Cast
|
|
from django.db.models import DateTimeField, DateField
|
|
effective_begin_expr = Coalesce(
|
|
'begin_time',
|
|
'planned_begin_time',
|
|
'expected_begin_time',
|
|
'creation_time',
|
|
output_field=DateTimeField()
|
|
)
|
|
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
|
|
qs = qs.filter(effective_begin_date=today)
|
|
|
|
return qs
|
|
|
|
|
|
|
|
def can_add_intervention(user, thematic, contract=None):
|
|
"""
|
|
Vérifie si l'utilisateur peut créer une nouvelle intervention.
|
|
|
|
La création correspond au statut 'in_preparation' : on vérifie le droit
|
|
can_edit sur ce statut dans les permissions thématique ou contrat.
|
|
Pour les prestataires (external_manager), le flag can_create_interventions
|
|
du contrat est également vérifié (rétrocompatibilité).
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if user_config.roles.filter(name__in=['admin', 'top_manager']).exists():
|
|
return True
|
|
|
|
# Vérification via permissions thématique
|
|
if thematic:
|
|
ut = UserThematics.objects.filter(
|
|
user_config=user_config,
|
|
thematic=thematic,
|
|
).first()
|
|
if ut:
|
|
# can_edit_interventions=True est suffisant pour créer une intervention
|
|
# (les opérateurs n'ont pas forcément de UserThematicStatusPermission pour in_preparation)
|
|
if ut.can_edit_interventions:
|
|
return True
|
|
if UserThematicStatusPermission.objects.filter(
|
|
user_thematic=ut,
|
|
status='in_preparation',
|
|
can_edit=True,
|
|
).exists():
|
|
return True
|
|
|
|
# Vérification via permissions contrat
|
|
if contract:
|
|
uca = UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
contract=contract,
|
|
).first()
|
|
if uca:
|
|
if UserContractStatusPermission.objects.filter(
|
|
user_contract=uca,
|
|
status='in_preparation',
|
|
can_edit=True,
|
|
).exists():
|
|
return True
|
|
# Rétrocompatibilité : flag can_create_interventions
|
|
if uca.can_create_interventions:
|
|
return True
|
|
else:
|
|
# Pas de contrat spécifié : vérifier si au moins un contrat autorise la création
|
|
uca_qs = UserContractAccess.objects.filter(user_config=user_config)
|
|
if uca_qs.filter(can_create_interventions=True).exists():
|
|
return True
|
|
if UserContractStatusPermission.objects.filter(
|
|
user_contract__user_config=user_config,
|
|
status='in_preparation',
|
|
can_edit=True,
|
|
).exists():
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_contracts_for_intervention_creation(user):
|
|
"""
|
|
Retourne les contrats pour lesquels l'utilisateur peut créer des interventions.
|
|
Utilisé pour les external_managers.
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return []
|
|
|
|
if not user_config.roles.filter(name='external_manager').exists():
|
|
return []
|
|
|
|
contract_ids = UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
can_create_interventions=True
|
|
).values_list('contract_id', flat=True)
|
|
|
|
from contracts.models import Contract
|
|
return Contract.objects.filter(id__in=contract_ids, is_active=True)
|
|
|
|
def intervention_add_permission_required(view_func):
|
|
@wraps(view_func)
|
|
def _wrapped_view(request, thematic_code=None, *args, **kwargs):
|
|
thematic = None
|
|
if thematic_code:
|
|
thematic = get_object_or_404(Thematic, code=thematic_code)
|
|
|
|
if not can_add_intervention(request.user, thematic):
|
|
raise PermissionDenied(_("Vous n'avez pas la permission d'ajouter une intervention pour cette thématique."))
|
|
|
|
return view_func(request, thematic_code, *args, **kwargs)
|
|
|
|
return _wrapped_view
|
|
|
|
|
|
def intervention_add_preventive_permission_required(view_func):
|
|
"""
|
|
Décorateur pour les vues de création d'intervention préventive.
|
|
Seuls les admin, manager et controller peuvent créer des interventions préventives.
|
|
Les external_managers et operators ne peuvent pas en créer.
|
|
"""
|
|
@wraps(view_func)
|
|
def _wrapped_view(request, thematic_code=None, *args, **kwargs):
|
|
thematic = None
|
|
if thematic_code:
|
|
thematic = get_object_or_404(Thematic, code=thematic_code)
|
|
|
|
# Vérifie d'abord si l'utilisateur peut ajouter des interventions
|
|
if not can_add_intervention(request.user, thematic):
|
|
raise PermissionDenied(_("Vous n'avez pas la permission d'ajouter une intervention pour cette thématique."))
|
|
|
|
# Seuls admin, manager, controller peuvent créer des préventives
|
|
try:
|
|
user_config = UserConfig.objects.get(user=request.user)
|
|
user_roles = set(user_config.roles.values_list('name', flat=True))
|
|
if not (user_roles & {'admin', 'manager', 'controller', 'top_manager'}):
|
|
raise PermissionDenied(_("Vous n'avez pas la permission de créer des interventions préventives."))
|
|
except UserConfig.DoesNotExist:
|
|
raise PermissionDenied(_("Vous n'avez pas la permission de créer des interventions préventives."))
|
|
|
|
return view_func(request, thematic_code, *args, **kwargs)
|
|
|
|
return _wrapped_view
|
|
|
|
|
|
def can_add_repair_intervention(user, intervention):
|
|
"""
|
|
Vérifie si l'utilisateur peut ajouter une intervention de réparation liée à l'intervention donnée.
|
|
L'utilisateur doit avoir les droits d'édition sur l'intervention source ET pouvoir créer
|
|
une nouvelle intervention (can_edit sur in_preparation).
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return True
|
|
|
|
# Nécessite le droit d'édition sur l'intervention existante
|
|
if not can_edit_intervention(user, intervention, user_config=user_config):
|
|
return False
|
|
|
|
# Et le droit de créer sur la thématique/contrat de cette intervention
|
|
return can_add_intervention(user, intervention.thematic, contract=intervention.contract)
|
|
|
|
|
|
def can_edit_intervention(user, intervention, user_config=None):
|
|
"""
|
|
Vérifie si l'utilisateur peut modifier l'intervention.
|
|
|
|
Priorité : contrat > thématique.
|
|
Si une ligne UserContractStatusPermission existe pour ce contrat et ce statut,
|
|
c'est cette valeur qui fait foi (même si False) — pas de fallback thématique.
|
|
Si aucune ligne n'existe pour ce statut, fallback sur la thématique.
|
|
Pour les techniciens non-privilégiés, la contrainte d'assignation reste en place.
|
|
"""
|
|
if user_config is None:
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if user_config.roles.filter(name__in=['admin', 'top_manager']).exists():
|
|
return True
|
|
|
|
# Pour les techniciens sans rôle privilégié, vérifier l'assignation
|
|
roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
|
privileged_roles = {'admin', 'manager', 'controller', 'external_manager', 'operator'}
|
|
is_inspector_on_inspection = (intervention.type == 'inspection' and 'inspector' in roles)
|
|
|
|
if 'inspector' in roles:
|
|
# Si l'intervention n'est pas de type 'inspection', le rôle inspector ne permet pas de modifier.
|
|
# Mais si l'utilisateur a d'autres rôles d'édition valides, on poursuit.
|
|
other_edit_roles = {'admin', 'top_manager', 'manager', 'controller', 'external_manager', 'operator', 'technician'}
|
|
if intervention.type != 'inspection' and not (roles & other_edit_roles):
|
|
return False
|
|
|
|
if 'technician' in roles and not (roles & privileged_roles) and not is_inspector_on_inspection:
|
|
assigned_member = getattr(intervention, 'assigned_member', None)
|
|
assigned_user_id = getattr(assigned_member, 'user_id', None) if assigned_member else None
|
|
if assigned_user_id is not None and assigned_user_id != user.id:
|
|
return False
|
|
|
|
contract = intervention.contract
|
|
thematic = intervention.thematic
|
|
status = intervention.status
|
|
|
|
# -- Priorité contrat --
|
|
if contract:
|
|
uca = UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
contract=contract,
|
|
).first()
|
|
if uca:
|
|
perm = UserContractStatusPermission.objects.filter(
|
|
user_contract=uca,
|
|
status=status,
|
|
).first()
|
|
# Droit d'édition explicite sur le statut courant → valeur finale
|
|
if perm is not None and perm.can_edit:
|
|
return True
|
|
# Pas de droit d'édition (ligne absente ou can_edit=False) :
|
|
# vérifier si l'utilisateur peut changer le statut vers une cible autorisée
|
|
# (ex : 'to_be_processed' → 'assigned' = prise en charge)
|
|
target_statuses = set(ALLOWED_TRANSITIONS.get(status, []))
|
|
if target_statuses and UserContractStatusPermission.objects.filter(
|
|
user_contract=uca,
|
|
status__in=target_statuses,
|
|
can_change_status_to=True,
|
|
).exists():
|
|
return True
|
|
# Sinon, droits insuffisants (droits explicites obligatoires)
|
|
return False
|
|
|
|
# -- Accès thématique --
|
|
if thematic:
|
|
ut = UserThematics.objects.filter(
|
|
user_config=user_config,
|
|
thematic=thematic,
|
|
).first()
|
|
if ut:
|
|
return UserThematicStatusPermission.objects.filter(
|
|
user_thematic=ut,
|
|
status=status,
|
|
can_edit=True,
|
|
).exists()
|
|
|
|
return False
|
|
|
|
def intervention_edit_permission_required(view_func):
|
|
@wraps(view_func)
|
|
def _wrapped_view(request, intervention_id, *args, **kwargs):
|
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
|
|
|
if not can_edit_intervention(request.user, intervention):
|
|
raise PermissionDenied(_("Vous n'avez pas la permission de modifier cette intervention."))
|
|
|
|
return view_func(request, intervention_id, *args, **kwargs)
|
|
|
|
return _wrapped_view
|
|
|
|
|
|
|
|
|
|
|
|
def can_manage_occupations(user, intervention, user_config=None):
|
|
if not user.is_authenticated:
|
|
return False
|
|
|
|
if user_config is None:
|
|
try:
|
|
user_config = (UserConfig.objects
|
|
.select_related('user')
|
|
.prefetch_related('roles')
|
|
.get(user=user))
|
|
except UserConfig.DoesNotExist:
|
|
user_config = None
|
|
|
|
roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
|
|
|
|
if roles.intersection({'admin', 'external_manager'}):
|
|
return True
|
|
|
|
if 'technician' in roles:
|
|
company_member = (CompanyMember.objects
|
|
.prefetch_related('teams__company')
|
|
.filter(user=user)
|
|
.first())
|
|
if not company_member:
|
|
return False
|
|
|
|
member_team_ids = company_member.teams.values_list('id', flat=True)
|
|
|
|
if intervention.assigned_member_id == getattr(company_member, 'id', None):
|
|
return True
|
|
|
|
# Autoriser tous les techniciens de l'équipe assignée, même si un membre
|
|
# spécifique est aussi assigné (ex : intervention démarrée par un collègue)
|
|
if (intervention.assigned_team_id is not None
|
|
and intervention.assigned_team_id in member_team_ids):
|
|
return True
|
|
|
|
# Autoriser tous les techniciens qui ont démarré cette intervention, même si elle est ensuite réassignée à une autre équipe ou un autre membre
|
|
if InterventionTimeLine.objects.filter(
|
|
intervention=intervention,
|
|
event_type='status_change',
|
|
to_status='in_progress',
|
|
event_user=user,
|
|
).exists():
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
def intervention_manage_occupations_required(view_func):
|
|
@wraps(view_func)
|
|
def _wrapped_view(request, intervention_id, *args, **kwargs):
|
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
|
|
|
if not can_manage_occupations(request.user, intervention):
|
|
raise PermissionDenied(_("Vous n'avez pas la permission de gérer les occupations pour cette intervention."))
|
|
|
|
return view_func(request, intervention_id, *args, **kwargs)
|
|
|
|
return _wrapped_view
|
|
|
|
|
|
|
|
|
|
def user_allowed_transitions(user, current_status, intervention=None):
|
|
"""
|
|
Retourne la liste des statuts vers lesquels l'utilisateur peut transitionner
|
|
depuis current_status.
|
|
|
|
Si une intervention est fournie, utilise les permissions spécifiques au contrat
|
|
ou à la thématique de cette intervention (priorité contrat > thématique).
|
|
Sans intervention, retourne l'union de toutes les permissions de l'utilisateur.
|
|
|
|
Le résultat est toujours intersecté avec ALLOWED_TRANSITIONS[current_status]
|
|
pour garantir la validité du graphe global de transitions.
|
|
"""
|
|
graph_targets = set(ALLOWED_TRANSITIONS.get(current_status, []))
|
|
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return []
|
|
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return sorted(s for s in ALL_STATUS_KEYS if s != current_status)
|
|
|
|
if not graph_targets:
|
|
return []
|
|
|
|
def _filter_transitions(transitions):
|
|
if STATUS_ORDERS.get(current_status, 0) >= STATUS_ORDERS.get('assigned', 15):
|
|
return [s for s in transitions if s != 'canceled']
|
|
return transitions
|
|
|
|
# -- Cas avec intervention : priorité contrat > thématique --
|
|
if intervention is not None:
|
|
if intervention.contract_id:
|
|
uca = UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
contract_id=intervention.contract_id,
|
|
).first()
|
|
if uca:
|
|
# Récupérer toutes les lignes contrat pour les statuts cibles
|
|
contract_rows = {
|
|
row.status: row.can_change_status_to
|
|
for row in UserContractStatusPermission.objects.filter(
|
|
user_contract=uca,
|
|
status__in=graph_targets,
|
|
)
|
|
}
|
|
contract_permitted = {s for s, v in contract_rows.items() if v}
|
|
# Statuts cibles sans ligne contrat → fallback thématique puis rôle
|
|
uncovered = graph_targets - set(contract_rows.keys())
|
|
thematic_extra = set()
|
|
if uncovered and intervention.thematic_id:
|
|
ut = UserThematics.objects.filter(
|
|
user_config=user_config,
|
|
thematic_id=intervention.thematic_id,
|
|
).first()
|
|
if ut:
|
|
thematic_extra = set(
|
|
UserThematicStatusPermission.objects.filter(
|
|
user_thematic=ut,
|
|
status__in=uncovered,
|
|
can_change_status_to=True,
|
|
).values_list('status', flat=True)
|
|
)
|
|
# Réduire uncovered aux statuts sans ligne thématique explicite
|
|
thematic_covered = set(
|
|
UserThematicStatusPermission.objects.filter(
|
|
user_thematic=ut,
|
|
status__in=uncovered,
|
|
).values_list('status', flat=True)
|
|
)
|
|
uncovered = uncovered - thematic_covered
|
|
# Pour les statuts encore non couverts : fallback sur ROLE_TRANSITIONS
|
|
if uncovered:
|
|
user_roles_names = set(user_config.roles.values_list('name', flat=True))
|
|
for role in user_roles_names:
|
|
role_extra = {dst for src, dst in ROLE_TRANSITIONS.get(role, []) if src == current_status}
|
|
thematic_extra |= role_extra & uncovered
|
|
return sorted(_filter_transitions(graph_targets & (contract_permitted | thematic_extra)))
|
|
# Pas d'accès contrat → fallback thématique
|
|
|
|
if intervention.thematic_id:
|
|
ut = UserThematics.objects.filter(
|
|
user_config=user_config,
|
|
thematic_id=intervention.thematic_id,
|
|
).first()
|
|
if ut:
|
|
permitted = set(
|
|
UserThematicStatusPermission.objects.filter(
|
|
user_thematic=ut,
|
|
can_change_status_to=True,
|
|
).values_list('status', flat=True)
|
|
)
|
|
return sorted(_filter_transitions(graph_targets & permitted))
|
|
|
|
return []
|
|
|
|
# -- Sans intervention : union de toutes les permissions de l'utilisateur --
|
|
permitted = set()
|
|
permitted.update(
|
|
UserThematicStatusPermission.objects.filter(
|
|
user_thematic__user_config=user_config,
|
|
can_change_status_to=True,
|
|
).values_list('status', flat=True)
|
|
)
|
|
permitted.update(
|
|
UserContractStatusPermission.objects.filter(
|
|
user_contract__user_config=user_config,
|
|
can_change_status_to=True,
|
|
).values_list('status', flat=True)
|
|
)
|
|
return sorted(_filter_transitions(graph_targets & permitted))
|
|
|
|
|
|
def can_approve_intervention(user, intervention):
|
|
"""
|
|
Vérifie si l'utilisateur peut faire passer une intervention du statut 'to_be_approved'
|
|
vers un autre statut, dans le cas où un contrat est renseigné.
|
|
|
|
Avec le nouveau système : l'utilisateur doit avoir can_change_status_to=True
|
|
pour au moins un des statuts cibles valides depuis 'to_be_approved'
|
|
sur ses permissions contrat. Rétrocompatibilité : can_approve=True est aussi accepté.
|
|
"""
|
|
if not intervention.contract_id:
|
|
return True # Pas de contrat → pas de restriction sur l'approbation
|
|
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return True
|
|
|
|
uca = UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
contract_id=intervention.contract_id,
|
|
).first()
|
|
if not uca:
|
|
return False
|
|
|
|
# Rétrocompatibilité : flag can_approve
|
|
if uca.can_approve:
|
|
return True
|
|
|
|
# Nouveau système : peut transitionner vers un statut cible depuis to_be_approved
|
|
approval_targets = set(ALLOWED_TRANSITIONS.get('to_be_approved', []))
|
|
return UserContractStatusPermission.objects.filter(
|
|
user_contract=uca,
|
|
status__in=approval_targets,
|
|
can_change_status_to=True,
|
|
).exists()
|
|
|
|
|
|
def get_allowed_transitions(request, intervention):
|
|
current_status = intervention.status
|
|
next_candidates = user_allowed_transitions(request.user, current_status, intervention=intervention)
|
|
|
|
# Si l'intervention est au statut 'to_be_approved' et qu'un contrat est renseigné,
|
|
# seuls les utilisateurs avec can_approve=True (ou admin) peuvent changer le statut.
|
|
if current_status == 'to_be_approved' and intervention.contract_id:
|
|
if not can_approve_intervention(request.user, intervention):
|
|
return []
|
|
|
|
# Si l'intervention est en préparation, que le contrat exige une approbation,
|
|
# que la priorité n'est pas urgente, et que l'utilisateur est opérateur,
|
|
# alors seul le passage à 'to_be_approved' est autorisé.
|
|
if (
|
|
current_status == 'in_preparation'
|
|
and intervention.contract_id
|
|
and getattr(intervention.contract, 'needs_approval', False)
|
|
and intervention.priority != '1'
|
|
):
|
|
try:
|
|
user_config = UserConfig.objects.get(user=request.user)
|
|
is_operator = user_config.roles.filter(name='operator').exists()
|
|
is_only_operator = is_operator and not user_config.roles.filter(
|
|
name__in=['admin', 'manager', 'controller', 'external_manager']
|
|
).exists()
|
|
except UserConfig.DoesNotExist:
|
|
is_only_operator = False
|
|
|
|
if is_only_operator:
|
|
next_candidates = [s for s in next_candidates if s in ('to_be_approved', 'canceled')]
|
|
|
|
# Un external_manager (sans rôle privilégié interne) ne peut annuler que les interventions
|
|
# créées par un prestataire (created_by_provider=True). On masque le bouton sinon.
|
|
if 'canceled' in next_candidates:
|
|
try:
|
|
user_config = UserConfig.objects.get(user=request.user)
|
|
roles = set(user_config.roles.values_list('name', flat=True))
|
|
except UserConfig.DoesNotExist:
|
|
roles = set()
|
|
is_external_manager_only = (
|
|
'external_manager' in roles and
|
|
not roles.intersection({'admin', 'manager', 'operator', 'controller'})
|
|
)
|
|
if is_external_manager_only and not intervention.created_by_provider:
|
|
next_candidates = [s for s in next_candidates if s != 'canceled']
|
|
|
|
items = []
|
|
for nxt in next_candidates:
|
|
items.append({
|
|
"from": current_status,
|
|
"to": nxt,
|
|
"allowed": True, # Could be used later with transition guards (False + reason if not allowed)
|
|
"label": TRANSITION_LABEL.get(nxt) or f"Passer à {nxt}",
|
|
"reason": '',
|
|
})
|
|
|
|
return items
|
|
|
|
|
|
|
|
def can_edit_note(user, user_config, note):
|
|
"""
|
|
Règle:
|
|
- Admin: peut tout modifier.
|
|
- Thématique 'structures': l'auteur OU rôle manager/external_manager (pas de contrainte horaire).
|
|
- Sinon: uniquement l'auteur ET la note a été créée il y a < 1h.
|
|
"""
|
|
if not user.is_authenticated or user_config is None:
|
|
return False
|
|
|
|
# 1) Admin => OK
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return True
|
|
|
|
# 2) Thématique 'structures'
|
|
intervention = getattr(note, 'intervention', None)
|
|
thematic_code = getattr(getattr(intervention, 'thematic', None), 'code', None)
|
|
if thematic_code == 'structures':
|
|
# Auteur OU rôle manager/external_manager
|
|
if note.note_author_id == user.id:
|
|
return True
|
|
if user_config.roles.filter(name__in=['manager', 'external_manager']).exists():
|
|
return True
|
|
return False
|
|
|
|
# 3) Auteur + moins d'une heure (autres thématiques)
|
|
if note.note_author_id != user.id:
|
|
return False
|
|
|
|
created_at = getattr(note, 'note_time', None) or getattr(note, 'created_at', None)
|
|
if created_at is None:
|
|
return False
|
|
|
|
if timezone.is_naive(created_at):
|
|
created_at = timezone.make_aware(created_at, timezone.get_current_timezone())
|
|
|
|
return (timezone.now() - created_at) <= timedelta(hours=1)
|
|
|
|
|
|
|
|
def can_delete_document(user, user_config, document):
|
|
"""
|
|
Règles de suppression d'un document attaché à une intervention :
|
|
- Admin: peut tout supprimer.
|
|
- Thématique 'structures': l'auteur OU rôle manager/external_manager (pas de contrainte).
|
|
- Sinon (auteur uniquement) :
|
|
* Avant 'Pris en charge' (status_order < 15) : toujours autorisé.
|
|
* À partir de 'Pris en charge' : autorisé seulement si aucun changement de statut
|
|
n'a eu lieu depuis l'upload du document (i.e. le statut n'a pas bougé depuis
|
|
que le document a été ajouté).
|
|
"""
|
|
if not user.is_authenticated or user_config is None:
|
|
return False
|
|
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return True
|
|
|
|
if user_config.is_intern and user_config.roles.filter(name='manager').exists():
|
|
return True
|
|
|
|
intervention = getattr(document, 'intervention', None)
|
|
thematic_code = getattr(getattr(intervention, 'thematic', None), 'code', None)
|
|
|
|
# Thématique 'structures'
|
|
if thematic_code == 'structures':
|
|
# Auteur OU rôle manager/external_manager
|
|
if document.uploaded_by_id == user.id:
|
|
return True
|
|
if user_config.roles.filter(name__in=['manager', 'external_manager']).exists():
|
|
return True
|
|
return False
|
|
|
|
if document.uploaded_by_id != user.id:
|
|
return False
|
|
|
|
if intervention is None:
|
|
return False
|
|
|
|
# Avant 'Pris en charge' : toujours supprimable par l'auteur
|
|
if intervention.status_order < STATUS_ORDERS.get('assigned', 15):
|
|
return True
|
|
|
|
# À partir de 'Pris en charge' : supprimable uniquement si aucun changement de statut
|
|
# n'a eu lieu depuis que le document a été uploadé.
|
|
upload_date = getattr(document, 'upload_date', None)
|
|
if upload_date is None:
|
|
return False
|
|
|
|
return not InterventionTimeLine.objects.filter(
|
|
intervention=intervention,
|
|
event_type='status_change',
|
|
event_time__gt=upload_date,
|
|
).exists()
|
|
|
|
|
|
# ============================================================================
|
|
# Guards pour les fichiers privés (private_media)
|
|
# ============================================================================
|
|
|
|
def guard_intervention_document_file(obj, user):
|
|
"""
|
|
Guard pour InterventionDocument.file et thumbnail.
|
|
L'utilisateur doit pouvoir voir l'intervention associée,
|
|
ou au minimum avoir accès au résumé par thématique.
|
|
"""
|
|
if not user.is_authenticated:
|
|
return False
|
|
# Peut voir l'intervention en accès complet
|
|
if can_view_intervention(user, obj.intervention):
|
|
return True
|
|
# Ou a accès au résumé par thématique
|
|
return can_view_intervention_summary(user, obj.intervention)
|
|
|
|
|
|
def guard_intervention_sheet_file(obj, user):
|
|
"""
|
|
Guard pour Intervention.intervention_sheet.
|
|
L'utilisateur doit pouvoir voir l'intervention.
|
|
"""
|
|
if not user.is_authenticated:
|
|
return False
|
|
return can_view_intervention(user, obj)
|
|
|
|
|
|
def guard_operation_photo_file(obj, user):
|
|
"""
|
|
Guard pour OperationPhoto.photo et thumbnail.
|
|
L'utilisateur doit pouvoir voir l'intervention associée à l'opération.
|
|
"""
|
|
if not user.is_authenticated:
|
|
return False
|
|
return can_view_intervention(user, obj.operation.intervention)
|
|
|
|
|
|
def guard_operation_photo_file(obj, user):
|
|
"""
|
|
Guard pour OperationPhoto.photo et thumbnail.
|
|
L'utilisateur doit pouvoir voir l'intervention associée à l'opération.
|
|
"""
|
|
if not user.is_authenticated:
|
|
return False
|
|
return can_view_intervention(user, obj.operation.intervention)
|
|
|
|
|
|
def guard_guided_operation_media_file(obj, user):
|
|
"""
|
|
Guard pour GuidedOperationMedia.file.
|
|
L'utilisateur doit pouvoir voir l'intervention associée.
|
|
"""
|
|
if not user.is_authenticated:
|
|
return False
|
|
return can_view_intervention(user, obj.guided_data.intervention)
|
|
|
|
|
|
def guard_symptom_recommendation_document(obj, user):
|
|
"""
|
|
Guard pour SymptomRecommendation.document.
|
|
Accessible à tout utilisateur authentifié (données de référence).
|
|
"""
|
|
return user.is_authenticated
|
|
|
|
|
|
# ============================================================================
|
|
# Fonctions pour les interventions créées par les prestataires
|
|
# ============================================================================
|
|
|
|
def should_show_created_by_provider_badge(user, intervention):
|
|
"""
|
|
Vérifie si le badge 'créé par le prestataire' doit être affiché.
|
|
Le badge est visible pour les managers et controllers tant que l'intervention
|
|
n'a pas été validée.
|
|
"""
|
|
if not intervention.created_by_provider:
|
|
return False
|
|
|
|
# Le badge n'est plus visible après validation
|
|
if intervention.status_order >= STATUS_ORDERS.get('validated', 51):
|
|
return False
|
|
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
# Visible pour admin, manager et controller
|
|
return user_config.roles.filter(name__in=['admin', 'manager', 'controller']).exists()
|
|
|
|
|
|
def is_external_manager_with_contract_creation_rights(user, contract=None):
|
|
"""
|
|
Vérifie si l'utilisateur est un external_manager avec le droit de créer des interventions.
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if not user_config.roles.filter(name='external_manager').exists():
|
|
return False
|
|
|
|
if contract:
|
|
return UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
contract=contract,
|
|
can_create_interventions=True
|
|
).exists()
|
|
|
|
return UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
can_create_interventions=True
|
|
).exists()
|
|
|
|
|
|
def can_view_correction_messages(user):
|
|
"""
|
|
Vérifie si l'utilisateur a le droit de voir les messages de correction.
|
|
Rôles autorisés: admin, manager, controller, external_manager
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
allowed_roles = ['admin', 'manager', 'controller', 'external_manager']
|
|
return user_config.roles.filter(name__in=allowed_roles).exists()
|
|
|
|
|
|
def can_user_create_inspection_for_contract(user, contract):
|
|
"""
|
|
Vérifie si l'utilisateur détient les droits de changement de statut vers l'un des statuts :
|
|
in_preparation, to_be_approved, to_be_planned, assigned, ou in_progress
|
|
pour le contrat spécifié. Les administrateurs contournent toutes les restrictions.
|
|
"""
|
|
try:
|
|
user_config = UserConfig.objects.get(user=user)
|
|
except UserConfig.DoesNotExist:
|
|
return False
|
|
|
|
if user_config.roles.filter(name='admin').exists():
|
|
return True
|
|
|
|
uca = UserContractAccess.objects.filter(
|
|
user_config=user_config,
|
|
contract=contract,
|
|
).first()
|
|
if not uca:
|
|
return False
|
|
|
|
target_statuses = ['in_preparation', 'to_be_approved', 'to_be_planned', 'assigned', 'in_progress']
|
|
return UserContractStatusPermission.objects.filter(
|
|
user_contract=uca,
|
|
status__in=target_statuses,
|
|
can_change_status_to=True,
|
|
).exists() |