import json 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 from contracts.models import Company, CompanyTeam, CompanyMember, Contract, ContractPost, ContractOrder, ContractOrderQuote, ContractOrderQuoteItem from assets.models import AssetCategory from interventions.models import ( Intervention, InterventionContractPost, 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 InspectionTests(TestCase): def setUp(self): from common.models import UserConfig, Role, Thematic, UserThematics from contracts.models import Company, Contract from interventions.models import InspectionConfiguration, OperationTemplate, OperationTemplateItem, OperationStatusSet User = get_user_model() self.user = User.objects.create_user(username='inspector_user', password='pwd') self.role_inspector = Role.objects.create(name='inspector') self.user_config = UserConfig.objects.create(user=self.user, is_intern=True) self.user_config.roles.add(self.role_inspector) self.thematic = Thematic.objects.create(code='roads', name_fr='Voirie', name_nl='Wegen') UserThematics.objects.create( user_config=self.user_config, thematic=self.thematic, can_view_interventions=True, can_edit_interventions=True, ) self.company = Company.objects.create(name='Provider A') from datetime import date today = date.today() self.contract = Contract.objects.create( company=self.company, contract_number='C-INSP-1', start_date=today - timedelta(days=30), end_date=today + timedelta(days=365) ) self.template = OperationTemplate.objects.create( name='GIEP Template', thematic=self.thematic, ) self.status_set = OperationStatusSet.objects.create(name='Default') OperationTemplateItem.objects.create( template=self.template, name_fr='Inspection Item FR', name_nl='Inspection Item NL', status_set=self.status_set, is_generic=True, order=1 ) self.inspection_config = InspectionConfiguration.objects.create( name_fr='Inspection GIEP', name_nl='GIEP Inspectie', description='Test description', thematic=self.thematic, maintain_type='preventive', ) self.inspection_config.operation_templates.add(self.template) from interventions.models import InspectionConfigurationContract InspectionConfigurationContract.objects.create( inspection_configuration=self.inspection_config, provider=self.company, contract=self.contract, ) from common.models import UserContractAccess, UserContractStatusPermission self.uca = UserContractAccess.objects.create( user_config=self.user_config, contract=self.contract, can_view_interventions=True, ) UserContractStatusPermission.objects.create( user_contract=self.uca, status='in_preparation', can_change_status_to=True, ) def test_inspection_configuration_properties(self): from django.utils import translation self.assertEqual(self.inspection_config.name_fr, 'Inspection GIEP') self.assertEqual(self.inspection_config.name_nl, 'GIEP Inspectie') # Test translation property with translation.override('fr'): self.assertEqual(self.inspection_config.name, 'Inspection GIEP') with translation.override('nl'): self.assertEqual(self.inspection_config.name, 'GIEP Inspectie') def test_intervention_form_inspection_status_field(self): from interventions.views.forms import InterventionForm # When type is inspection, status field is a visible Select and defaults to to_be_processed form = InterventionForm(thematic=self.thematic, user=self.user, initial={'type': 'inspection'}) self.assertFalse(form.fields['status'].widget.is_hidden) self.assertEqual(form.fields['status'].initial, 'to_be_processed') self.assertEqual(list(form.fields['status'].widget.choices), list(form.fields['status'].choices)) self.assertTrue(len(form.fields['status'].widget.choices) > 0) # When type is not inspection, status field is hidden widget and forced to in_preparation form_normal = InterventionForm(thematic=self.thematic, user=self.user, initial={'type': 'fixing'}) self.assertTrue(form_normal.fields['status'].widget.is_hidden) self.assertEqual(form_normal.fields['status'].initial, 'in_preparation') def test_interventions_add_get_with_inspection_config(self): self.client.login(username='inspector_user', password='pwd') url = reverse('interventions:intervention_add', kwargs={'thematic_code': self.thematic.code}) # GET with inspection_config_id response = self.client.get(url, {'inspection_config_id': self.inspection_config.id}) self.assertEqual(response.status_code, 200) # Initial dictionary check in form form = response.context['form'] self.assertEqual(form.initial.get('type'), 'inspection') self.assertEqual(form.initial.get('status'), 'assigned') self.assertIn('Nouvelle inspection - GIEP', form.initial.get('title')) # Verify title changes self.assertContains(response, 'Nouvelle inspection - GIEP') self.assertNotContains(response, 'Créer une intervention') def test_interventions_add_post_with_inspection_config(self): self.client.login(username='inspector_user', password='pwd') url = reverse('interventions:intervention_add', kwargs={'thematic_code': self.thematic.code}) from interventions.models import Symptom, AssetCategory from django.contrib.contenttypes.models import ContentType category = AssetCategory.objects.create( code='CAT-INSP', name_fr='Catégorie Inspection', name_nl='Inspectiecategorie', thematic=self.thematic ) symptom = Symptom.objects.create( code='SYMP-INSP', name_fr='Symptôme Inspection', name_nl='Inspectiesymptoom', thematic=self.thematic ) from assets.models import Structure, StructureLocation structure = Structure.objects.create(code='OA001') location_obj = StructureLocation.objects.create(code='LOC001', structure=structure) location_ct = ContentType.objects.get_for_model(StructureLocation) # Standard post parameters post_data = { 'title': 'Nouvelle Inspection GIEP', 'type': 'inspection', 'status': 'to_be_processed', 'symptom': symptom.id, 'asset_category': category.id, 'maintain_type': 'preventive', 'inspection_config_id': self.inspection_config.id, 'lon': '4.35', 'lat': '50.85', 'location_ct': location_ct.id, 'location_id': location_obj.id, 'priority': '3', 'origin_type': 'demand', } response = self.client.post(url, data=post_data) # If successfully created, it should redirect to intervention detail self.assertEqual(response.status_code, 302) # Retrieve the created intervention from interventions.models import Intervention, Operation intervention = Intervention.objects.get(title='Nouvelle Inspection GIEP') # Check field values self.assertEqual(intervention.type, 'inspection') self.assertEqual(intervention.status, 'to_be_processed') # Should auto-assign contract & provider from configuration self.assertEqual(intervention.contract, self.contract) self.assertEqual(intervention.assigned_provider, self.company) # Check generic operation from template is created operations = Operation.objects.filter(intervention=intervention) self.assertTrue(operations.exists()) self.assertEqual(operations.first().name_fr, 'Inspection Item FR') def test_homepage_filters_configurations_by_transition_rights(self): from common.models import UserConfig, Role from contracts.models import Company, Contract from interventions.models import InspectionConfiguration, InspectionConfigurationContract # 1. Create a second configuration that the user has NO rights on company_2 = Company.objects.create(name='Provider B') contract_2 = Contract.objects.create( company=company_2, contract_number='C-INSP-2', start_date=timezone.now().date() - timedelta(days=30), end_date=timezone.now().date() + timedelta(days=365) ) inspection_config_2 = InspectionConfiguration.objects.create( name_fr='Inspection OA', name_nl='OA Inspectie', description='Second config description', thematic=self.thematic, maintain_type='preventive', ) InspectionConfigurationContract.objects.create( inspection_configuration=inspection_config_2, provider=company_2, contract=contract_2, ) self.client.login(username='inspector_user', password='pwd') response = self.client.get(reverse('home:index')) self.assertEqual(response.status_code, 200) # The user has rights on self.contract (self.inspection_config), so it should be visible self.assertIn(self.inspection_config, response.context['inspection_configs']) # The user has NO rights on contract_2 (inspection_config_2), so it should NOT be visible self.assertNotIn(inspection_config_2, response.context['inspection_configs']) def test_interventions_add_get_forbidden_if_no_transition_rights(self): # Create an inspector with NO rights on self.contract User = get_user_model() unauthorized_user = User.objects.create_user(username='unauthorized_inspector', password='pwd') unauthorized_config = UserConfig.objects.create(user=unauthorized_user, is_intern=True) unauthorized_config.roles.add(self.role_inspector) from common.models import UserThematics UserThematics.objects.create( user_config=unauthorized_config, thematic=self.thematic, can_view_interventions=True, can_edit_interventions=True, ) self.client.login(username='unauthorized_inspector', password='pwd') url = reverse('interventions:intervention_add', kwargs={'thematic_code': self.thematic.code}) response = self.client.get(url, {'inspection_config_id': self.inspection_config.id}) self.assertEqual(response.status_code, 403) def test_interventions_add_post_forbidden_if_no_transition_rights(self): # Create an inspector with NO rights on self.contract User = get_user_model() unauthorized_user = User.objects.create_user(username='unauthorized_inspector_2', password='pwd') unauthorized_config = UserConfig.objects.create(user=unauthorized_user, is_intern=True) unauthorized_config.roles.add(self.role_inspector) from common.models import UserThematics UserThematics.objects.create( user_config=unauthorized_config, thematic=self.thematic, can_view_interventions=True, can_edit_interventions=True, ) self.client.login(username='unauthorized_inspector_2', password='pwd') url = reverse('interventions:intervention_add', kwargs={'thematic_code': self.thematic.code}) post_data = { 'title': 'Nouvelle Inspection GIEP 2', 'type': 'inspection', 'status': 'to_be_processed', 'maintain_type': 'preventive', 'inspection_config_id': self.inspection_config.id, 'lon': '4.35', 'lat': '50.85', 'priority': '3', 'origin_type': 'demand', } response = self.client.post(url, data=post_data) self.assertEqual(response.status_code, 403) def test_interventions_add_post_with_multiple_contracts_resolution(self): from contracts.models import Company, Contract from interventions.models import InspectionConfigurationContract from common.models import UserContractAccess, UserContractStatusPermission # Add a second allowed contract-provider couple to the configuration company_2 = Company.objects.create(name='Provider B') contract_2 = Contract.objects.create( company=company_2, contract_number='C-INSP-2', start_date=timezone.now().date() - timedelta(days=30), end_date=timezone.now().date() + timedelta(days=365) ) # Associate with config InspectionConfigurationContract.objects.create( inspection_configuration=self.inspection_config, provider=company_2, contract=contract_2, ) # Give status transition permissions to the inspector for contract_2 as well uca_2 = UserContractAccess.objects.create( user_config=self.user_config, contract=contract_2, can_view_interventions=True, ) UserContractStatusPermission.objects.create( user_contract=uca_2, status='in_preparation', can_change_status_to=True, ) # Let's perform a post self.client.login(username='inspector_user', password='pwd') url = reverse('interventions:intervention_add', kwargs={'thematic_code': self.thematic.code}) from interventions.models import Symptom, AssetCategory category = AssetCategory.objects.create( code='CAT-INSP-2', name_fr='Catégorie 2', name_nl='Categorie 2', thematic=self.thematic ) symptom = Symptom.objects.create( code='SYMP-INSP-2', name_fr='Symptôme 2', name_nl='Symptoom 2', thematic=self.thematic ) post_data = { 'title': 'Multiple Contracts Inspection', 'type': 'inspection', 'status': 'to_be_processed', 'symptom': symptom.id, 'asset_category': category.id, 'maintain_type': 'preventive', 'inspection_config_id': self.inspection_config.id, 'lon': '4.35', 'lat': '50.85', 'priority': '3', 'origin_type': 'demand', } response = self.client.post(url, data=post_data) self.assertEqual(response.status_code, 302) from interventions.models import Intervention intervention = Intervention.objects.get(title='Multiple Contracts Inspection') # Since we didn't match any specific contract via location or other means, # it should fall back to the first allowed couple in the list self.assertIn(intervention.contract, [self.contract, contract_2]) self.assertIn(intervention.assigned_provider, [self.company, company_2]) def test_interventions_add_get_with_inspection_config_default_category(self): from assets.models import AssetCategory category = AssetCategory.objects.create( code='CAT-DFLT', name_fr='Catégorie Par Défaut', name_nl='Standaardcategorie', thematic=self.thematic ) self.inspection_config.default_category = category self.inspection_config.save() self.client.login(username='inspector_user', password='pwd') url = reverse('interventions:intervention_add', kwargs={'thematic_code': self.thematic.code}) response = self.client.get(url, {'inspection_config_id': self.inspection_config.id}) self.assertEqual(response.status_code, 200) form = response.context['form'] self.assertEqual(form.initial.get('asset_category'), category.id) def test_interventions_add_get_with_inspection_config_default_subcategory(self): from assets.models import AssetCategory parent_category = AssetCategory.objects.create( code='CAT-PARENT', name_fr='Catégorie Parente', name_nl='Oudercategorie', thematic=self.thematic ) child_category = AssetCategory.objects.create( code='CAT-CHILD', name_fr='Catégorie Enfant', name_nl='Kindcategorie', thematic=self.thematic, parent=parent_category ) self.inspection_config.default_category = child_category self.inspection_config.save() self.client.login(username='inspector_user', password='pwd') url = reverse('interventions:intervention_add', kwargs={'thematic_code': self.thematic.code}) response = self.client.get(url, {'inspection_config_id': self.inspection_config.id}) self.assertEqual(response.status_code, 200) form = response.context['form'] # The parent category should be pre-filled in the asset_category field self.assertEqual(form.initial.get('asset_category'), parent_category.id) # The child category should be pre-filled in the hidden/initial interv_sub_category_id variable self.assertEqual(response.context['initial_interv_sub_category_id'], str(child_category.id)) # The subcategory dropdown should contain the child category as selected initial_subcategories = response.context['initial_subcategories'] self.assertTrue(any(sub['id'] == child_category.id and sub['selected'] for sub in initial_subcategories)) def test_inspection_configuration_geojson(self): from assets.models import Structure, StructureLocation from django.contrib.contenttypes.models import ContentType import json structure = Structure.objects.create(code='OA999') location_obj = StructureLocation.objects.create( code='LOC999', structure=structure, lon=4.40, lat=50.90, geojson=json.dumps({ "type": "Polygon", "coordinates": [[[4.40, 50.90], [4.41, 50.90], [4.41, 50.91], [4.40, 50.91], [4.40, 50.90]]] }) ) location_ct = ContentType.objects.get_for_model(StructureLocation) # Link the location ContentType class to the displayed location types ManyToMany self.inspection_config.displayed_location_types.add(location_ct) self.client.login(username='inspector_user', password='pwd') url = reverse('interventions:inspection_configuration_geojson', kwargs={'config_id': self.inspection_config.id}) response = self.client.get(url) self.assertEqual(response.status_code, 200) geojson_data = response.json() self.assertEqual(geojson_data['type'], 'FeatureCollection') self.assertEqual(len(geojson_data['features']), 1) feature = geojson_data['features'][0] self.assertEqual(feature['properties']['type'], 'location') self.assertEqual(feature['properties']['id'], location_obj.id) self.assertEqual(feature['properties']['code'], 'LOC999') self.assertEqual(feature['geometry']['type'], 'Polygon') def test_inspector_can_edit_inspection_with_permissions(self): from interventions.permissions import can_edit_intervention from common.models import UserContractStatusPermission UserContractStatusPermission.objects.filter( user_contract=self.uca, status='in_preparation' ).update(can_edit=True) intervention = Intervention.objects.create( title='Inspection Test Edit', type='inspection', contract=self.contract, thematic=self.thematic, status='in_preparation', ) self.assertTrue(can_edit_intervention(self.user, intervention)) def test_inspector_cannot_edit_inspection_without_permissions(self): from interventions.permissions import can_edit_intervention unauthorized_user = get_user_model().objects.create_user(username='unauthorized_inspector_edit', password='pwd') unauthorized_config = UserConfig.objects.create(user=unauthorized_user, is_intern=True) unauthorized_config.roles.add(self.role_inspector) intervention = Intervention.objects.create( title='Inspection Test Edit Forbidden', type='inspection', contract=self.contract, thematic=self.thematic, status='in_preparation', ) self.assertFalse(can_edit_intervention(unauthorized_user, intervention)) def test_inspector_cannot_edit_non_inspection_intervention(self): from interventions.permissions import can_edit_intervention from common.models import UserContractStatusPermission UserContractStatusPermission.objects.filter( user_contract=self.uca, status='in_preparation' ).update(can_edit=True) intervention = Intervention.objects.create( title='Fixing Test Edit Forbidden', type='fixing', contract=self.contract, thematic=self.thematic, status='in_preparation', ) self.assertFalse(can_edit_intervention(self.user, intervention)) def test_inspector_bypasses_technician_assignment_for_inspections(self): from interventions.permissions import can_edit_intervention from common.models import Role, UserContractStatusPermission UserContractStatusPermission.objects.filter( user_contract=self.uca, status='in_preparation' ).update(can_edit=True) role_technician, _ = Role.objects.get_or_create(name='technician') self.user_config.roles.add(role_technician) assigned_user = get_user_model().objects.create_user(username='other_tech', password='pwd') assigned_member = CompanyMember.objects.create(user=assigned_user, name='Other Tech') intervention = Intervention.objects.create( title='Inspection Assigned to Other Tech', type='inspection', contract=self.contract, thematic=self.thematic, status='in_preparation', assigned_member=assigned_member, ) self.assertTrue(can_edit_intervention(self.user, intervention)) def test_inspector_allowed_update_fields(self): from interventions.permissions import get_allowed_update_fields from common.models import UserContractStatusPermission UserContractStatusPermission.objects.filter( user_contract=self.uca, status='in_preparation' ).update(can_edit=True) intervention = Intervention.objects.create( title='Inspection Fields Test', type='inspection', contract=self.contract, thematic=self.thematic, status='in_preparation', ) allowed_fields = get_allowed_update_fields(self.user, intervention) self.assertIn('operations', allowed_fields) self.assertIn('realization', allowed_fields) self.assertIn('planification', allowed_fields) self.assertIn('title', allowed_fields) self.assertIn('member_assignment', allowed_fields)