loko/streetup/interventions/tests/test_inspections.py

570 lines
24 KiB
Python

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})
from django.utils import translation
with translation.override('fr'):
# 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('inspection - GIEP', form.initial.get('title'))
# Verify title changes
self.assertContains(response, '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.assertIn(feature['geometry']['type'], ['Polygon', 'Point'])
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)