feat: implement preventive intervention templates with grouping and associated forms

This commit is contained in:
Karl Determe 2026-08-13 13:52:08 +02:00
parent 7b519f16bd
commit 0521e2624e
7 changed files with 181 additions and 8 deletions

View file

@ -189,6 +189,9 @@ def contracts_orders_detail(request, order_id):
# Déterminer les droits de création d'interventions par type
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
can_create_preventive = bool(user_roles & {'admin', 'manager', 'controller'})
if not can_create_preventive and 'external_manager' in user_roles:
from interventions.permissions import is_external_manager_with_contract_creation_rights
can_create_preventive = is_external_manager_with_contract_creation_rights(request.user)
can_create_ameliorative = bool(user_roles & {'admin', 'manager', 'controller', 'external_manager'})
context = {

View file

@ -173,6 +173,9 @@ def index(request):
# Détermine les droits de création d'interventions par type
user_roles = set(user_config.roles.values_list('name', flat=True))
can_create_preventive = bool(user_roles & {'admin', 'manager', 'controller', 'top_manager'})
if not can_create_preventive and 'external_manager' in user_roles:
from interventions.permissions import is_external_manager_with_contract_creation_rights
can_create_preventive = is_external_manager_with_contract_creation_rights(request.user)
can_create_ameliorative = bool(user_roles & {'admin', 'manager', 'controller', 'external_manager', 'operator', 'top_manager'})
is_operator = 'operator' in user_roles
is_admin = 'admin' in user_roles

View file

@ -865,8 +865,9 @@ def intervention_add_permission_required(view_func):
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.
Seuls les admin, manager, controller, top_manager et les external_managers
avec droit de création d'interventions sur au moins un contrat peuvent créer des
interventions préventives.
"""
@wraps(view_func)
def _wrapped_view(request, thematic_code=None, *args, **kwargs):
@ -878,11 +879,14 @@ def intervention_add_preventive_permission_required(view_func):
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
# Seuls admin, manager, controller, top_manager, et external_manager avec droits
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'}):
is_allowed = bool(user_roles & {'admin', 'manager', 'controller', 'top_manager'})
if not is_allowed and 'external_manager' in user_roles:
is_allowed = is_external_manager_with_contract_creation_rights(request.user)
if not is_allowed:
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."))
@ -1478,8 +1482,8 @@ def should_show_created_by_provider_badge(user, intervention):
except UserConfig.DoesNotExist:
return False
# Visible pour admin, manager et controller
return user_config.roles.filter(name__in=['admin', 'manager', 'controller']).exists()
# Visible pour admin, manager, controller et top_manager
return user_config.roles.filter(name__in=['admin', 'manager', 'controller', 'top_manager']).exists()
def is_external_manager_with_contract_creation_rights(user, contract=None):

View file

@ -8,7 +8,7 @@ from django.contrib import messages
from django.contrib.messages import get_messages
from django.core.files.uploadedfile import SimpleUploadedFile
from common.models import UserConfig, Role, Thematic, UserThematics
from common.models import UserConfig, Role, Thematic, UserThematics, UserContractAccess
from contracts.models import Company, CompanyTeam, CompanyMember, Contract, ContractPost, ContractOrder, ContractOrderQuote, ContractOrderQuoteItem
from assets.models import AssetCategory
from interventions.models import (
@ -449,5 +449,160 @@ class AssetOperationCleanupTests(TestCase):
self.assertEqual(remaining_ops.count(), 2)
class ExternalManagerPreventivePermissionsTests(TestCase):
def setUp(self):
User = get_user_model()
self.role_ext_manager, _ = Role.objects.get_or_create(name='external_manager')
self.role_manager, _ = Role.objects.get_or_create(name='manager')
self.thematic = Thematic.objects.create(code='test_thema', name_fr='Test', name_nl='Test')
self.provider = Company.objects.create(name="Test Provider")
from datetime import date, timedelta
today = date.today()
self.contract_with_rights = Contract.objects.create(
contract_number="C-001",
company=self.provider,
is_active=True,
start_date=today - timedelta(days=30),
end_date=today + timedelta(days=365)
)
self.contract_with_rights.thematics.add(self.thematic)
self.contract_without_rights = Contract.objects.create(
contract_number="C-002",
company=self.provider,
is_active=True,
start_date=today - timedelta(days=30),
end_date=today + timedelta(days=365)
)
self.contract_without_rights.thematics.add(self.thematic)
# 1. User with creation rights
self.user_with_rights = User.objects.create_user(username='ext_with_rights', password='pwd')
self.uc_with_rights = UserConfig.objects.create(user=self.user_with_rights, is_intern=False)
self.uc_with_rights.roles.add(self.role_ext_manager)
UserThematics.objects.create(
user_config=self.uc_with_rights,
thematic=self.thematic,
can_view_assets=True,
can_edit_interventions=True,
)
UserContractAccess.objects.create(
user_config=self.uc_with_rights,
contract=self.contract_with_rights,
can_create_interventions=True
)
# 2. User without creation rights
self.user_without_rights = User.objects.create_user(username='ext_without_rights', password='pwd')
self.uc_without_rights = UserConfig.objects.create(user=self.user_without_rights, is_intern=False)
self.uc_without_rights.roles.add(self.role_ext_manager)
UserThematics.objects.create(
user_config=self.uc_without_rights,
thematic=self.thematic,
can_view_assets=True,
can_edit_interventions=True,
)
UserContractAccess.objects.create(
user_config=self.uc_without_rights,
contract=self.contract_without_rights,
can_create_interventions=False
)
# 3. An internal manager to view the detail page and verify the badge
self.internal_manager = User.objects.create_user(username='int_manager', password='pwd')
self.uc_int = UserConfig.objects.create(user=self.internal_manager, is_intern=True)
self.uc_int.roles.add(self.role_manager)
def test_access_to_add_preventive_view(self):
# User with rights should access
self.client.login(username='ext_with_rights', password='pwd')
url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'test_thema'})
# We need to set up a mock draft in the session
session = self.client.session
session['intervention_drafts'] = {
'draft-1': {
'locations': [],
'assets': [],
'form_data': {}
}
}
session.save()
response = self.client.get(f"{url}?draft_id=draft-1")
self.assertEqual(response.status_code, 200)
# User without rights should be Forbidden (403)
self.client.login(username='ext_without_rights', password='pwd')
response = self.client.get(f"{url}?draft_id=draft-1")
self.assertEqual(response.status_code, 403)
def test_contracts_filtered_in_form(self):
self.client.login(username='ext_with_rights', password='pwd')
url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'test_thema'})
session = self.client.session
session['intervention_drafts'] = {
'draft-1': {
'locations': [],
'assets': [],
'form_data': {}
}
}
session.save()
response = self.client.get(f"{url}?draft_id=draft-1")
self.assertEqual(response.status_code, 200)
# Verify that contract_with_rights is in form's queryset, but contract_without_rights is not
contracts_queryset = response.context['form'].fields['contract'].queryset
self.assertIn(self.contract_with_rights, contracts_queryset)
self.assertNotIn(self.contract_without_rights, contracts_queryset)
def test_preventive_intervention_created_by_provider(self):
self.client.login(username='ext_with_rights', password='pwd')
url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'test_thema'})
session = self.client.session
session['intervention_drafts'] = {
'draft-1': {
'locations': [],
'assets': [],
'form_data': {}
}
}
session.save()
# Submit creation form
post_data = {
'title': 'Test Preventive Creation',
'status': 'in_preparation',
'type': 'care',
'contract': self.contract_with_rights.id,
'expected_begin_time': '2026-08-13T12:00',
'expected_end_time': '2026-08-13T13:00',
'recurrence_count': 0,
'type_of_selected_elements': 'locations',
'intervention_creation_mode': 'single_for_all',
'draft_id': 'draft-1',
}
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302)
# Check created intervention
intervention = Intervention.objects.filter(title='Test Preventive Creation').first()
self.assertIsNotNone(intervention)
self.assertEqual(intervention.created_by, self.user_with_rights)
self.assertTrue(intervention.created_by_provider)
self.assertEqual(intervention.maintain_type, 'preventive')
# Check that the badge is shown for internal manager
from interventions.permissions import should_show_created_by_provider_badge
self.assertTrue(should_show_created_by_provider_badge(self.internal_manager, intervention))

View file

@ -540,6 +540,9 @@ class PreventiveInterventionForm(forms.Form):
# 2. Contrats associés à au moins une thématique que l'utilisateur peut éditer
# OU contrats sans thématiques (pour la rétrocompatibilité)
base_contracts = user_config.contracts.filter(is_active=True)
user_roles = set(user_config.roles.values_list('name', flat=True))
if 'external_manager' in user_roles and not (user_roles & {'admin', 'manager', 'controller', 'top_manager'}):
base_contracts = base_contracts.filter(usercontractaccess__can_create_interventions=True)
# Si une thématique spécifique est fournie, filtrer aussi par cette thématique
if thematic:

View file

@ -220,8 +220,9 @@ def index(request):
# - Améliorative : admin, manager, controller, external_manager, operator
user_roles = set(user_config.roles.values_list('name', flat=True))
# Préventive : uniquement admin, manager, controller
can_create_preventive = bool(user_roles & {'admin', 'manager', 'controller'})
if not can_create_preventive and 'external_manager' in user_roles:
can_create_preventive = is_external_manager_with_contract_creation_rights(request.user)
# Améliorative : admin, manager, controller, external_manager, operator
can_create_ameliorative = bool(user_roles & {'admin', 'manager', 'controller', 'external_manager', 'operator'})

View file

@ -126,6 +126,7 @@ logger = logging.getLogger(__name__)
from interventions.views.helpers import *
@intervention_add_preventive_permission_required
def preventive_intervention_draft(request, thematic_code):
if request.method == 'POST':
draft = InterventionDraftManager(request.session)
@ -147,6 +148,7 @@ def preventive_intervention_draft(request, thematic_code):
)
@intervention_add_preventive_permission_required
def preventive_link_assets(request, thematic_code):
draft_id = request.POST.get("preventive_draft_id")
drafts = InterventionDraftManager(request.session)
@ -174,6 +176,7 @@ def preventive_link_assets(request, thematic_code):
return redirect(f"{reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': thematic_code})}?draft_id={draft_id}")
@intervention_add_preventive_permission_required
def preventive_remove_element(request, thematic_code):
"""Supprime un élément (location ou asset) de la sélection du draft"""
draft_id = request.POST.get("draft_id")
@ -206,6 +209,7 @@ def preventive_remove_element(request, thematic_code):
}, status=400)
@intervention_add_preventive_permission_required
def intervention_add_preventive(request, thematic_code):
thematic = Thematic.objects.filter(code=thematic_code).first()