loko/loko/interventions/tests/test_preventive_templates.py

1248 lines
49 KiB
Python

import json
import time
from datetime import timedelta
from unittest import mock
from django.test import TestCase
from django.contrib.auth import get_user_model
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, UserContractAccess
from contracts.models import Company, CompanyTeam, CompanyMember, Contract, ContractPost, ContractOrder, ContractOrderQuote, ContractOrderQuoteItem
from assets.models import AssetCategory
from interventions.models import (
Intervention,
InterventionContractPost,
ContractOperationTemplatePost,
InterventionPlanificationTimeLine,
InterventionRealizationTimeLine,
Operation,
InterventionAsset,
OperationTemplate,
OperationTemplateItem,
OperationStatusSet,
OperationStatus,
STATUS_ORDERS,
InterventionDocument,
)
from django.contrib.contenttypes.models import ContentType
from interventions.views import _group_operations_by_asset_and_template, _create_equipment_operations_for_assets
from interventions.permissions import can_manage_occupations, can_delete_document
from django.urls import reverse
from django.utils import timezone
from decimal import Decimal
class OperationTemplateTests(TestCase):
"""Test the operation template functionality including template types."""
def setUp(self):
from interventions.models import OperationTemplate, OperationTemplateItem, OperationStatusSet
from common.models import Thematic
# Create a test thematic
self.thematic = Thematic.objects.create(
code='test',
name_fr='Test',
name_nl='Test'
)
# Create generic template
self.generic_template = OperationTemplate.objects.create(
name='Generic Inspection',
description='Generic inspection template',
thematic=self.thematic
)
# Create equipment template
self.equipment_template = OperationTemplate.objects.create(
name='Equipment Check',
description='Equipment check template',
thematic=self.thematic
)
# Create status set
self.status_set = OperationStatusSet.objects.create(name='Standard')
# Add items to templates
OperationTemplateItem.objects.create(
template=self.generic_template,
name_fr='Vérification générale',
name_nl='Algemene controle',
status_set=self.status_set,
order=1
)
OperationTemplateItem.objects.create(
template=self.equipment_template,
name_fr='Vérification équipement',
name_nl='Apparatuurcontrole',
status_set=self.status_set,
order=1
)
def test_template_items_creation(self):
"""Test that template items are created correctly."""
generic_items = self.generic_template.items.all()
equipment_items = self.equipment_template.items.all()
self.assertEqual(generic_items.count(), 1)
self.assertEqual(equipment_items.count(), 1)
self.assertEqual(generic_items.first().name_fr, 'Vérification générale')
self.assertEqual(equipment_items.first().name_fr, 'Vérification équipement')
def test_operation_with_template_group(self):
"""Test that operations can be created with template group."""
from interventions.models import Intervention, Operation
intervention = Intervention.objects.create(
title='Test Intervention',
status='in_preparation',
maintain_type='preventive'
)
# Create operation with template group
operation = Operation.objects.create(
intervention=intervention,
name_fr='Test operation',
name_nl='Test operatie',
template_group=self.generic_template.name,
order=1
)
self.assertEqual(operation.template_group, 'Generic Inspection')
self.assertEqual(intervention.operations.count(), 1)
# Test ordering by template_group
operation2 = Operation.objects.create(
intervention=intervention,
name_fr='Test operation 2',
name_nl='Test operatie 2',
template_group=self.equipment_template.name,
order=1
)
operations = intervention.operations.all()
self.assertEqual(operations.count(), 2)
# Operations should be ordered by template_group first
self.assertEqual(operations[0].template_group, 'Equipment Check')
self.assertEqual(operations[1].template_group, 'Generic Inspection')
def test_get_asset_model_filters(self):
"""Test that get_asset_model_filters returns the correct dictionary with asset model IDs."""
from interventions.models import OperationTemplateAssetModelFilter
from django.contrib.contenttypes.models import ContentType
from common.models import Thematic
ct = ContentType.objects.get_for_model(Thematic)
# Create a filter entry
filter_entry = OperationTemplateAssetModelFilter.objects.create(
template=self.equipment_template,
asset_model_content_type=ct,
asset_model_object_id=999
)
filters = self.equipment_template.get_asset_model_filters()
self.assertIsNotNone(filters)
self.assertIn('thematic', filters)
self.assertIn(999, filters['thematic'])
class OperationGroupingTests(TestCase):
def setUp(self):
self.intervention = Intervention.objects.create(
title='Grouped intervention',
status='in_preparation',
maintain_type='preventive'
)
self.company = Company.objects.create(name='Asset Company')
self.asset_ct = ContentType.objects.get_for_model(Company)
self.intervention_asset = InterventionAsset.objects.create(
intervention=self.intervention,
content_type=self.asset_ct,
object_id=self.company.id,
)
def test_operations_grouped_by_asset_and_template(self):
op_asset_a = Operation.objects.create(
intervention=self.intervention,
name_fr='Contrôle câble',
name_nl='Kabelcontrole',
template_group='Template A',
order=1,
asset_content_type=self.asset_ct,
asset_object_id=self.company.id,
)
op_asset_b = Operation.objects.create(
intervention=self.intervention,
name_fr='Contrôle boîtier',
name_nl='Kastcontrole',
template_group='Template B',
order=2,
asset_content_type=self.asset_ct,
asset_object_id=self.company.id,
)
op_generic = Operation.objects.create(
intervention=self.intervention,
name_fr='Inspection générale',
name_nl='Algemene inspectie',
template_group=None,
order=3,
)
linked_assets = [self.intervention_asset]
operations = [op_asset_a, op_asset_b, op_generic]
grouped = _group_operations_by_asset_and_template(operations, linked_assets)
self.assertEqual(len(grouped), 2)
self.assertFalse(grouped[0]['has_asset'])
asset_group = next(group for group in grouped if group['has_asset'])
self.assertEqual(asset_group['asset'], self.company)
self.assertEqual(len(asset_group['template_groups']), 2)
self.assertListEqual(
[tg['name'] for tg in asset_group['template_groups']],
['Template A', 'Template B']
)
self.assertEqual(asset_group['template_groups'][0]['operations'][0], op_asset_a)
generic_group = next(group for group in grouped if not group['has_asset'])
self.assertEqual(len(generic_group['template_groups']), 1)
self.assertIsNone(generic_group['template_groups'][0]['name'])
self.assertEqual(generic_group['template_groups'][0]['operations'][0], op_generic)
class PreventiveInterventionFormTests(TestCase):
def test_preventive_intervention_form_status_choices(self):
from interventions.views.forms import PreventiveInterventionForm
# Test default choices (type = 'care' or default)
form_default = PreventiveInterventionForm()
status_field_default = form_default.fields['status']
choices_default = [val for val, label in status_field_default.choices]
self.assertIn('in_preparation', choices_default)
self.assertIn('to_be_approved', choices_default)
self.assertIn('to_be_planned', choices_default)
self.assertNotIn('to_be_processed', choices_default)
self.assertNotIn('assigned', choices_default)
# Test choices when type = 'inspection' in initial data
form_inspection = PreventiveInterventionForm(initial={'type': 'inspection'})
status_field_inspection = form_inspection.fields['status']
choices_inspection = [val for val, label in status_field_inspection.choices]
self.assertIn('in_preparation', choices_inspection)
self.assertIn('to_be_approved', choices_inspection)
self.assertIn('to_be_planned', choices_inspection)
self.assertIn('to_be_processed', choices_inspection)
self.assertIn('assigned', choices_inspection)
# Test choices when type = 'inspection' in submitted data (POST)
form_post = PreventiveInterventionForm(data={'type': 'inspection'})
status_field_post = form_post.fields['status']
choices_post = [val for val, label in status_field_post.choices]
self.assertIn('in_preparation', choices_post)
self.assertIn('to_be_approved', choices_post)
self.assertIn('to_be_planned', choices_post)
self.assertIn('to_be_processed', choices_post)
self.assertIn('assigned', choices_post)
class PreventiveDraftFormDataTests(TestCase):
def test_multiple_operation_templates_are_preserved(self):
User = get_user_model()
user = User.objects.create_user(username='preventive_creator', password='pwd')
role_admin, _ = Role.objects.get_or_create(name='admin')
user_config = UserConfig.objects.create(user=user, is_intern=True)
user_config.roles.add(role_admin)
thematic = Thematic.objects.create(code='test', name_fr='Test', name_nl='Test')
UserThematics.objects.create(
user_config=user_config,
thematic=thematic,
can_view_assets=True,
can_edit_interventions=True,
)
self.client.login(username='preventive_creator', password='pwd')
url = reverse('interventions:intervention_preventive_draft', kwargs={'thematic_code': 'test'})
response = self.client.post(url, {
'title': 'Préventive',
'operation_templates': ['1', '2'],
})
self.assertEqual(response.status_code, 302)
session = self.client.session
drafts = session.get('intervention_drafts', {})
self.assertEqual(len(drafts), 1)
draft_data = next(iter(drafts.values()))
self.assertEqual(draft_data['form_data'].get('operation_templates'), ['1', '2'])
class EquipmentTemplateOperationsTests(TestCase):
def setUp(self):
self.thematic = Thematic.objects.create(code='thema', name_fr='Thema', name_nl='Thema')
self.status_set = OperationStatusSet.objects.create(name='Default')
self.template = OperationTemplate.objects.create(
name='Equipment Template',
thematic=self.thematic,
)
OperationTemplateItem.objects.create(
template=self.template,
name_fr='Inspection A',
name_nl='Inspectie A',
status_set=self.status_set,
order=1,
)
OperationTemplateItem.objects.create(
template=self.template,
name_fr='Inspection B',
name_nl='Inspectie B',
status_set=self.status_set,
order=2,
)
self.intervention = Intervention.objects.create(
title='Preventive',
status='in_preparation',
maintain_type='preventive',
)
self.company = Company.objects.create(name='Asset Company')
self.asset_ct = ContentType.objects.get_for_model(Company)
self.intervention.equipment_operation_templates.add(self.template)
def _call_helper(self):
return _create_equipment_operations_for_assets(
self.intervention,
[(self.asset_ct, self.company)],
)
def test_operations_created_for_new_asset(self):
created = self._call_helper()
self.assertEqual(created, 2)
operations = Operation.objects.filter(
intervention=self.intervention,
asset_content_type=self.asset_ct,
asset_object_id=self.company.id,
).order_by('order')
self.assertEqual(operations.count(), 2)
self.assertListEqual(
list(operations.values_list('name_fr', flat=True)),
['Inspection A', 'Inspection B'],
)
self.assertTrue(all(op.template_group == self.template.name for op in operations))
def test_operations_not_duplicated_for_same_asset(self):
first_run = self._call_helper()
self.assertEqual(first_run, 2)
second_run = self._call_helper()
self.assertEqual(second_run, 0)
operations = Operation.objects.filter(
intervention=self.intervention,
asset_content_type=self.asset_ct,
asset_object_id=self.company.id,
)
self.assertEqual(operations.count(), 2)
class AssetOperationCleanupTests(TestCase):
def setUp(self):
self.User = get_user_model()
self.user = self.User.objects.create_user(username='asset-admin', password='pwd')
self.role_admin = Role.objects.create(name='admin')
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
self.user_config.roles.add(self.role_admin)
self.thematic = Thematic.objects.create(code='road', name_fr='Route', name_nl='Weg')
self.intervention = Intervention.objects.create(
title='Asset cleanup',
status='in_preparation',
maintain_type='preventive',
thematic=self.thematic,
)
self.company = Company.objects.create(name='Linked asset company')
self.asset_ct = ContentType.objects.get_for_model(Company)
InterventionAsset.objects.create(
intervention=self.intervention,
content_type=self.asset_ct,
object_id=self.company.id,
)
self.status_set = OperationStatusSet.objects.create(name='Ops')
self.status_ok = OperationStatus.objects.create(
status_set=self.status_set,
label_fr='OK',
label_nl='OK',
value='ok',
order=1,
)
self.operation_one = Operation.objects.create(
intervention=self.intervention,
name_fr='Op FR 1',
name_nl='Op NL 1',
order=1,
template_group='Template A',
asset_content_type=self.asset_ct,
asset_object_id=self.company.id,
)
self.operation_two = Operation.objects.create(
intervention=self.intervention,
name_fr='Op FR 2',
name_nl='Op NL 2',
order=2,
template_group='Template A',
asset_content_type=self.asset_ct,
asset_object_id=self.company.id,
)
self.url = reverse('interventions:intervention_link_assets', args=[self.intervention.id])
def _post_assets(self, assets):
payload = json.dumps(assets)
return self.client.post(self.url, {'assets': payload})
def test_operations_removed_when_asset_detached(self):
self.client.login(username='asset-admin', password='pwd')
response = self._post_assets([])
self.assertEqual(response.status_code, 302)
remaining_assets = InterventionAsset.objects.filter(intervention=self.intervention)
self.assertFalse(remaining_assets.exists())
self.assertFalse(Operation.objects.filter(intervention=self.intervention).exists())
success_messages = [m for m in get_messages(response.wsgi_request) if m.level == messages.SUCCESS]
self.assertTrue(success_messages)
def test_warning_emitted_when_operations_had_values(self):
self.client.login(username='asset-admin', password='pwd')
self.operation_one.status = self.status_ok
self.operation_one.save(update_fields=['status'])
response = self._post_assets([])
warning_messages = [m for m in get_messages(response.wsgi_request) if m.level == messages.WARNING]
self.assertTrue(warning_messages)
# Aucun nettoyage car au moins une opération contenait des valeurs
remaining_ops = Operation.objects.filter(intervention=self.intervention)
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))
class PreventiveTemplateTeamAssignmentTests(TestCase):
"""Test automatic team assignment from OperationTemplate in preventive interventions."""
def setUp(self):
User = get_user_model()
self.thematic, _ = Thematic.objects.get_or_create(code='trafficlights', defaults={'name_fr': 'Signalisation', 'name_nl': 'Signalisatie'})
self.company = Company.objects.create(name='Provider Corp')
self.team_a = CompanyTeam.objects.create(name='Equipe Alpha', company=self.company)
self.team_a.thematics.add(self.thematic)
self.team_b = CompanyTeam.objects.create(name='Equipe Beta', company=self.company)
self.team_b.thematics.add(self.thematic)
from datetime import date, timedelta
today = date.today()
self.contract = Contract.objects.create(
contract_number='C-TEST-001',
company=self.company,
is_active=True,
start_date=today - timedelta(days=30),
end_date=today + timedelta(days=365)
)
self.contract.thematics.add(self.thematic)
self.user = User.objects.create_user(username='manager_user', password='pwd')
self.role_manager = Role.objects.create(name='manager')
self.config = UserConfig.objects.create(user=self.user, is_intern=True)
self.config.roles.add(self.role_manager)
self.config.contracts.add(self.contract)
UserThematics.objects.create(user_config=self.config, thematic=self.thematic, can_edit_interventions=True)
self.status_set = OperationStatusSet.objects.create(name='Standard Status Set')
# Template with team A
self.template_with_team = OperationTemplate.objects.create(
name='Gamme Entretien A',
description='Gamme avec équipe A',
thematic=self.thematic,
team=self.team_a,
)
OperationTemplateItem.objects.create(
template=self.template_with_team,
name_fr='Vérification A',
name_nl='Controle A',
status_set=self.status_set,
is_generic=True,
order=1,
)
# Template with team B
self.template_with_team_b = OperationTemplate.objects.create(
name='Gamme Entretien B',
description='Gamme avec équipe B',
thematic=self.thematic,
team=self.team_b,
)
OperationTemplateItem.objects.create(
template=self.template_with_team_b,
name_fr='Vérification B',
name_nl='Controle B',
status_set=self.status_set,
is_generic=True,
order=1,
)
# Template without team
self.template_without_team = OperationTemplate.objects.create(
name='Gamme Sans Equipe',
description='Gamme sans équipe',
thematic=self.thematic,
team=None,
)
OperationTemplateItem.objects.create(
template=self.template_without_team,
name_fr='Vérification C',
name_nl='Controle C',
status_set=self.status_set,
is_generic=True,
order=1,
)
def test_assign_team_from_templates_helper(self):
"""Test _assign_team_from_templates helper function directly."""
from interventions.views.helpers import _assign_team_from_templates
itv = Intervention.objects.create(
title='Test ITV',
status='in_preparation',
maintain_type='preventive',
thematic=self.thematic,
)
# No templates -> no change
_assign_team_from_templates(itv, [])
self.assertIsNone(itv.assigned_team)
# Template without team -> no change
_assign_team_from_templates(itv, [self.template_without_team])
self.assertIsNone(itv.assigned_team)
# Template with team -> assigned
_assign_team_from_templates(itv, [self.template_with_team])
itv.refresh_from_db()
self.assertEqual(itv.assigned_team, self.team_a)
self.assertEqual(itv.assigned_provider, self.company)
def test_preventive_creation_auto_assigns_team_single_mode(self):
"""Test preventive intervention creation assigns team from template in single_for_all mode."""
self.client.login(username='manager_user', password='pwd')
url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'trafficlights'})
session = self.client.session
session['intervention_drafts'] = {
'draft-team': {
'locations': [],
'assets': [],
'form_data': {},
'timestamp': time.time(),
}
}
session.save()
post_data = {
'title': 'Preventive with team',
'status': 'in_preparation',
'type': 'care',
'contract': self.contract.id,
'operation_templates': [str(self.template_with_team.id)],
'expected_begin_time': '2026-08-20T08:00',
'expected_end_time': '2026-08-20T17:00',
'recurrence_count': 0,
'type_of_selected_elements': 'locations',
'intervention_creation_mode': 'single_for_all',
'draft_id': 'draft-team',
}
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302)
intervention = Intervention.objects.filter(title='Preventive with team').first()
self.assertIsNotNone(intervention)
self.assertEqual(intervention.assigned_team, self.team_a)
self.assertEqual(intervention.assigned_provider, self.company)
self.assertIn(self.template_with_team, intervention.equipment_operation_templates.all())
def test_preventive_creation_multiple_templates_first_team_assigned(self):
"""When multiple templates are selected, the first one with a team sets the assigned_team."""
self.client.login(username='manager_user', password='pwd')
url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'trafficlights'})
session = self.client.session
session['intervention_drafts'] = {
'draft-multi': {
'locations': [],
'assets': [],
'form_data': {},
'timestamp': time.time(),
}
}
session.save()
# template_without_team first, then template_with_team_b
post_data = {
'title': 'Preventive multi templates',
'status': 'in_preparation',
'type': 'care',
'contract': self.contract.id,
'operation_templates': [str(self.template_without_team.id), str(self.template_with_team_b.id)],
'expected_begin_time': '2026-08-20T08:00',
'expected_end_time': '2026-08-20T17:00',
'recurrence_count': 0,
'type_of_selected_elements': 'locations',
'intervention_creation_mode': 'single_for_all',
'draft_id': 'draft-multi',
}
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302)
intervention = Intervention.objects.filter(title='Preventive multi templates').first()
self.assertIsNotNone(intervention)
self.assertEqual(intervention.assigned_team, self.team_b)
def test_preventive_creation_without_team_template(self):
"""When template has no team, assigned_team remains None."""
self.client.login(username='manager_user', password='pwd')
url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'trafficlights'})
session = self.client.session
session['intervention_drafts'] = {
'draft-noteam': {
'locations': [],
'assets': [],
'form_data': {},
'timestamp': time.time(),
}
}
session.save()
post_data = {
'title': 'Preventive without team',
'status': 'in_preparation',
'type': 'care',
'contract': self.contract.id,
'operation_templates': [str(self.template_without_team.id)],
'expected_begin_time': '2026-08-20T08:00',
'expected_end_time': '2026-08-20T17:00',
'recurrence_count': 0,
'type_of_selected_elements': 'locations',
'intervention_creation_mode': 'single_for_all',
'draft_id': 'draft-noteam',
}
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302)
intervention = Intervention.objects.filter(title='Preventive without team').first()
self.assertIsNotNone(intervention)
self.assertIsNone(intervention.assigned_team)
def test_preventive_creation_auto_assigns_team_one_per_element_locations(self):
"""Test preventive creation in one_per_element mode auto-assigns team to each intervention."""
from assets.models import TrafficLightIntersection
loc = TrafficLightIntersection.objects.create(code='TL-001', name_fr='Carrefour 1')
self.client.login(username='manager_user', password='pwd')
url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'trafficlights'})
session = self.client.session
session['intervention_drafts'] = {
'draft-loc': {
'locations': [loc.id],
'assets': [],
'form_data': {},
'timestamp': time.time(),
}
}
session.save()
post_data = {
'title': 'Preventive Location',
'status': 'in_preparation',
'type': 'care',
'contract': self.contract.id,
'operation_templates': [str(self.template_with_team.id)],
'expected_begin_time': '2026-08-20T08:00',
'expected_end_time': '2026-08-20T17:00',
'recurrence_count': 0,
'type_of_selected_elements': 'locations',
'intervention_creation_mode': 'one_per_element',
'draft_id': 'draft-loc',
}
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302)
intervention = Intervention.objects.filter(location_code='TL-001').first()
self.assertIsNotNone(intervention)
self.assertEqual(intervention.assigned_team, self.team_a)
self.assertEqual(intervention.assigned_provider, self.company)
def test_ajax_get_teams_by_thematic(self):
"""Test the AJAX endpoint ajax_get_teams_by_thematic."""
self.client.login(username='manager_user', password='pwd')
url = reverse('interventions:ajax_get_teams_by_thematic')
# Filter by thematic
response = self.client.get(f"{url}?thematic_id={self.thematic.id}")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('teams', data)
team_ids = [t['id'] for t in data['teams']]
self.assertIn(self.team_a.id, team_ids)
self.assertIn(self.team_b.id, team_ids)
# Without thematic -> all teams
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('teams', data)
self.assertGreaterEqual(len(data['teams']), 2)
class ContractOperationTemplatePostTests(TestCase):
"""Test associating ContractPosts with default quantities to OperationTemplates and preventive interventions."""
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user(username='test_user_posts', password='password123')
self.thematic, _ = Thematic.objects.get_or_create(
code='trafficlights',
defaults={'name_fr': 'Feux', 'name_nl': 'Verkeerslichten'}
)
self.role, _ = Role.objects.get_or_create(name='manager')
self.user_config = UserConfig.objects.create(user=self.user, is_intern=True)
self.user_config.roles.add(self.role)
self.company = Company.objects.create(name='Provider Co')
self.contract = Contract.objects.create(
contract_number='CTR-TEST-001',
company=self.company,
start_date=timezone.now().date(),
end_date=timezone.now().date() + timedelta(days=365),
is_active=True,
)
self.contract.thematics.add(self.thematic)
self.user_config.contracts.add(self.contract)
uca = UserContractAccess.objects.get(user_config=self.user_config, contract=self.contract)
uca.can_edit_interventions = True
uca.can_view_posts = True
uca.can_view_interventions = True
uca.save()
UserThematics.objects.create(user_config=self.user_config, thematic=self.thematic, can_edit_interventions=True)
self.contract_other = Contract.objects.create(
contract_number='CTR-OTHER-002',
company=self.company,
start_date=timezone.now().date(),
end_date=timezone.now().date() + timedelta(days=365),
is_active=True,
)
self.post_1 = ContractPost.objects.create(
contract=self.contract,
code='P01',
description_fr='Poste 1 FR',
description_nl='Post 1 NL',
order_number=1,
unit='pc',
unit_price=Decimal('10.00'),
)
self.post_2 = ContractPost.objects.create(
contract=self.contract,
code='P02',
description_fr='Poste 2 FR',
description_nl='Post 2 NL',
order_number=2,
unit='m',
unit_price=Decimal('25.50'),
)
self.post_3 = ContractPost.objects.create(
contract=self.contract,
code='P03',
description_fr='Poste 3 FR',
description_nl='Post 3 NL',
order_number=3,
unit='h',
unit_price=Decimal('50.00'),
)
self.post_other = ContractPost.objects.create(
contract=self.contract_other,
code='P_OTHER',
description_fr='Poste Autre Contrat',
description_nl='Post Andere',
order_number=1,
unit='pc',
unit_price=Decimal('100.00'),
)
self.template_a = OperationTemplate.objects.create(
name='Template A',
thematic=self.thematic
)
self.template_b = OperationTemplate.objects.create(
name='Template B',
thematic=self.thematic
)
def test_model_str_and_save_clean(self):
"""Test model validation, clean method, auto-contract and string representation."""
from django.core.exceptions import ValidationError
# Auto-populates contract if only contract_post is set
mapping = ContractOperationTemplatePost(
operation_template=self.template_a,
contract_post=self.post_1,
default_quantity=Decimal('2.00'),
comment='Commentaire test'
)
mapping.save()
self.assertEqual(mapping.contract, self.contract)
self.assertIn('CTR-TEST-001', str(mapping))
self.assertIn('Template A', str(mapping))
self.assertIn('P01', str(mapping))
# Invalid: contract_post belongs to another contract
invalid_mapping = ContractOperationTemplatePost(
contract=self.contract,
operation_template=self.template_a,
contract_post=self.post_other,
default_quantity=Decimal('1.00')
)
with self.assertRaises(ValidationError):
invalid_mapping.clean()
def test_assign_contract_posts_from_templates_single(self):
"""Test assigning default posts to an intervention from a single template."""
from interventions.views.helpers import _assign_contract_posts_from_templates
ContractOperationTemplatePost.objects.create(
contract=self.contract,
operation_template=self.template_a,
contract_post=self.post_1,
default_quantity=Decimal('3.00'),
comment='Nettoyage standard'
)
ContractOperationTemplatePost.objects.create(
contract=self.contract,
operation_template=self.template_a,
contract_post=self.post_2,
default_quantity=Decimal('1.50'),
)
intervention = Intervention.objects.create(
title='Intervention Test',
contract=self.contract,
thematic=self.thematic,
maintain_type='preventive',
created_by=self.user
)
_assign_contract_posts_from_templates(intervention, [self.template_a])
posts = list(intervention.interv_contract_posts.all().order_by('contract_post__order_number'))
self.assertEqual(len(posts), 2)
self.assertEqual(posts[0].contract_post, self.post_1)
self.assertEqual(posts[0].quantity, Decimal('3.00'))
self.assertEqual(posts[0].comment, 'Nettoyage standard')
self.assertEqual(posts[1].contract_post, self.post_2)
self.assertEqual(posts[1].quantity, Decimal('1.50'))
def test_assign_contract_posts_deduplication_first_found(self):
"""
When multiple templates are chosen and share a post,
the first one encountered must be kept, and subsequent duplicates ignored.
"""
from interventions.views.helpers import _assign_contract_posts_from_templates
# Template A defines Post 1 (qty 2) and Post 2 (qty 5)
ContractOperationTemplatePost.objects.create(
contract=self.contract,
operation_template=self.template_a,
contract_post=self.post_1,
default_quantity=Decimal('2.00'),
comment='From Template A'
)
ContractOperationTemplatePost.objects.create(
contract=self.contract,
operation_template=self.template_a,
contract_post=self.post_2,
default_quantity=Decimal('5.00'),
)
# Template B defines Post 1 (qty 10 with other comment) and Post 3 (qty 1)
ContractOperationTemplatePost.objects.create(
contract=self.contract,
operation_template=self.template_b,
contract_post=self.post_1,
default_quantity=Decimal('10.00'),
comment='From Template B'
)
ContractOperationTemplatePost.objects.create(
contract=self.contract,
operation_template=self.template_b,
contract_post=self.post_3,
default_quantity=Decimal('1.00'),
)
intervention = Intervention.objects.create(
title='Intervention Multi-Templates',
contract=self.contract,
thematic=self.thematic,
maintain_type='preventive',
created_by=self.user
)
# Pass templates in order [A, B]
_assign_contract_posts_from_templates(intervention, [self.template_a, self.template_b])
posts = list(intervention.interv_contract_posts.all().order_by('contract_post__order_number'))
self.assertEqual(len(posts), 3)
# Post 1 must come from Template A (qty 2, first found)
self.assertEqual(posts[0].contract_post, self.post_1)
self.assertEqual(posts[0].quantity, Decimal('2.00'))
self.assertEqual(posts[0].comment, 'From Template A')
# Post 2 from Template A
self.assertEqual(posts[1].contract_post, self.post_2)
self.assertEqual(posts[1].quantity, Decimal('5.00'))
# Post 3 from Template B
self.assertEqual(posts[2].contract_post, self.post_3)
self.assertEqual(posts[2].quantity, Decimal('1.00'))
def test_assign_contract_posts_empty_or_no_contract(self):
"""Test that helper handles empty templates or no contract gracefully."""
from interventions.views.helpers import _assign_contract_posts_from_templates
intervention_no_contract = Intervention.objects.create(
title='No Contract',
thematic=self.thematic,
created_by=self.user
)
_assign_contract_posts_from_templates(intervention_no_contract, [self.template_a])
self.assertEqual(intervention_no_contract.interv_contract_posts.count(), 0)
intervention = Intervention.objects.create(
title='With Contract',
contract=self.contract,
thematic=self.thematic,
created_by=self.user
)
_assign_contract_posts_from_templates(intervention, [])
self.assertEqual(intervention.interv_contract_posts.count(), 0)
def test_preventive_creation_flow_auto_creates_posts(self):
"""Test full preventive intervention creation flow automatically populates posts."""
from assets.models import TrafficLightIntersection
loc = TrafficLightIntersection.objects.create(code='TL-POST-01', name_fr='Carrefour Test')
ContractOperationTemplatePost.objects.create(
contract=self.contract,
operation_template=self.template_a,
contract_post=self.post_1,
default_quantity=Decimal('4.00'),
comment='Auto Post'
)
self.client.login(username='test_user_posts', password='password123')
url = reverse('interventions:intervention_add_preventive', kwargs={'thematic_code': 'trafficlights'})
session = self.client.session
session['intervention_drafts'] = {
'draft-posts': {
'locations': [loc.id],
'assets': [],
'form_data': {},
'timestamp': time.time(),
}
}
session.save()
post_data = {
'title': 'Preventive With Posts',
'status': 'in_preparation',
'type': 'care',
'contract': self.contract.id,
'operation_templates': [str(self.template_a.id)],
'expected_begin_time': '2026-08-20T08:00',
'expected_end_time': '2026-08-20T17:00',
'recurrence_count': 0,
'type_of_selected_elements': 'locations',
'intervention_creation_mode': 'one_per_element',
'draft_id': 'draft-posts',
}
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302)
intervention = Intervention.objects.filter(location_code='TL-POST-01').first()
self.assertIsNotNone(intervention)
posts = list(intervention.interv_contract_posts.all())
self.assertEqual(len(posts), 1)
self.assertEqual(posts[0].contract_post, self.post_1)
self.assertEqual(posts[0].quantity, Decimal('4.00'))
self.assertEqual(posts[0].comment, 'Auto Post')
def test_detail_view_includes_associated_order_and_posts_modal(self):
"""Test that the detail view context includes orders and correctly preselects the associated order."""
admin_role, _ = Role.objects.get_or_create(name='admin')
self.user_config.roles.add(admin_role)
from contracts.models import ContractOrder
order = ContractOrder.objects.create(
contract=self.contract,
order_code='CMD-2026-001',
order_date=timezone.now().date(),
delivery_date=timezone.now().date() + timedelta(days=30),
order_status='pending',
total_amount=Decimal('500.00'),
)
intervention = Intervention.objects.create(
title='Intervention With Order',
contract=self.contract,
order=order,
thematic=self.thematic,
status='in_preparation',
created_by=self.user,
)
self.client.login(username='test_user_posts', password='password123')
detail_url = reverse('interventions:intervention_detail', kwargs={'intervention_id': intervention.id})
response = self.client.get(detail_url)
self.assertEqual(response.status_code, 200)
# Check context
self.assertIn('orders', response.context)
orders = response.context['orders']
self.assertIsNotNone(orders)
self.assertIn(order, orders)
self.assertEqual(response.context['contract_id'], self.contract.id)
# Check rendered HTML for modal selector
content = response.content.decode('utf-8')
self.assertIn(f'data-initial-order-id="{order.id}"', content)
self.assertIn(f'value="{order.id}"', content)
self.assertIn('selected', content)
def test_ajax_get_posts_by_contract(self):
"""Test the AJAX endpoint filtering contract posts by contract."""
self.client.login(username='test_user_posts', password='password123')
url = reverse('interventions:ajax_get_posts_by_contract')
# Call with no contract_id
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.json().get('posts'), [])
# Call with self.contract
res = self.client.get(url, {'contract_id': self.contract.id})
self.assertEqual(res.status_code, 200)
posts_data = res.json().get('posts', [])
post_ids = [p['id'] for p in posts_data]
self.assertIn(self.post_1.id, post_ids)
self.assertIn(self.post_2.id, post_ids)
self.assertNotIn(self.post_other.id, post_ids)
def test_ajax_get_contracts_by_thematic(self):
"""Test the AJAX endpoint filtering contracts by thematic."""
self.client.login(username='test_user_posts', password='password123')
url = reverse('interventions:ajax_get_contracts_by_thematic')
res = self.client.get(url, {'thematic_id': self.thematic.id})
self.assertEqual(res.status_code, 200)
contracts_data = res.json().get('contracts', [])
contract_ids = [c['id'] for c in contracts_data]
self.assertIn(self.contract.id, contract_ids)
self.assertNotIn(self.contract_other.id, contract_ids)