feat: implement preventive intervention templates and operation grouping system
This commit is contained in:
parent
ca6d09c6b1
commit
b15326602b
11 changed files with 430 additions and 5 deletions
|
|
@ -639,6 +639,10 @@ class OperationTemplateItemInline(admin.TabularInline):
|
||||||
ordering = ('order',)
|
ordering = ('order',)
|
||||||
|
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
from contracts.models import CompanyTeam
|
||||||
|
|
||||||
|
|
||||||
class OperationTemplateAdminForm(forms.ModelForm):
|
class OperationTemplateAdminForm(forms.ModelForm):
|
||||||
"""Form for OperationTemplate with category filtering based on thematic."""
|
"""Form for OperationTemplate with category filtering based on thematic."""
|
||||||
|
|
||||||
|
|
@ -655,7 +659,7 @@ class OperationTemplateAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OperationTemplate
|
model = OperationTemplate
|
||||||
fields = ['code', 'name', 'description', 'thematic', 'categories']
|
fields = ['code', 'name', 'description', 'thematic', 'team', 'guided_template', 'categories']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
@ -687,6 +691,15 @@ class OperationTemplateAdminForm(forms.ModelForm):
|
||||||
elif 'categories' in self.fields:
|
elif 'categories' in self.fields:
|
||||||
self.fields['categories'].queryset = AssetCategory.objects.none()
|
self.fields['categories'].queryset = AssetCategory.objects.none()
|
||||||
|
|
||||||
|
# Filter teams by thematic
|
||||||
|
if 'team' in self.fields:
|
||||||
|
if thematic:
|
||||||
|
self.fields['team'].queryset = CompanyTeam.objects.filter(
|
||||||
|
Q(thematics=thematic) | Q(thematics__isnull=True)
|
||||||
|
).distinct().select_related('company').order_by('name')
|
||||||
|
else:
|
||||||
|
self.fields['team'].queryset = CompanyTeam.objects.select_related('company').order_by('name')
|
||||||
|
|
||||||
# Build choices for asset models
|
# Build choices for asset models
|
||||||
choices = []
|
choices = []
|
||||||
thematic_id = thematic.id if thematic else None
|
thematic_id = thematic.id if thematic else None
|
||||||
|
|
@ -767,13 +780,13 @@ class OperationTemplateAdminForm(forms.ModelForm):
|
||||||
@admin.register(OperationTemplate)
|
@admin.register(OperationTemplate)
|
||||||
class OperationTemplateAdmin(admin.ModelAdmin):
|
class OperationTemplateAdmin(admin.ModelAdmin):
|
||||||
form = OperationTemplateAdminForm
|
form = OperationTemplateAdminForm
|
||||||
list_display = ('code', 'name', 'description', 'thematic', 'categories_list', 'models_list')
|
list_display = ('code', 'name', 'description', 'thematic', 'team', 'categories_list', 'models_list')
|
||||||
list_filter = ('thematic', 'categories')
|
list_filter = ('thematic', 'team', 'categories')
|
||||||
filter_horizontal = ('categories',)
|
filter_horizontal = ('categories',)
|
||||||
inlines = [OperationTemplateItemInline]
|
inlines = [OperationTemplateItemInline]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'fields': ('code', 'name', 'description', 'thematic', 'guided_template')
|
'fields': ('code', 'name', 'description', 'thematic', 'team', 'guided_template')
|
||||||
}),
|
}),
|
||||||
('Catégories d\'équipements concernées (optionnel)', {
|
('Catégories d\'équipements concernées (optionnel)', {
|
||||||
'fields': ('categories',),
|
'fields': ('categories',),
|
||||||
|
|
|
||||||
20
loko/interventions/migrations/0003_operationtemplate_team.py
Normal file
20
loko/interventions/migrations/0003_operationtemplate_team.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-08-19 05:28
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contracts', '0002_initial'),
|
||||||
|
('interventions', '0002_remove_intervention_origin_fms_nbr_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='operationtemplate',
|
||||||
|
name='team',
|
||||||
|
field=models.ForeignKey(blank=True, help_text="Si définie, cette équipe sera automatiquement assignée lors de la création d'interventions préventives utilisant cette gamme.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='operation_templates', to='contracts.companyteam', verbose_name='Équipe associée'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1372,6 +1372,15 @@ class OperationTemplate(models.Model):
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True)
|
thematic = models.ForeignKey(Thematic, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
team = models.ForeignKey(
|
||||||
|
CompanyTeam,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='operation_templates',
|
||||||
|
verbose_name=_("Équipe associée"),
|
||||||
|
help_text=_("Si définie, cette équipe sera automatiquement assignée lors de la création d'interventions préventives utilisant cette gamme.")
|
||||||
|
)
|
||||||
guided_template = models.ForeignKey(
|
guided_template = models.ForeignKey(
|
||||||
GuidedOperationTemplate,
|
GuidedOperationTemplate,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var $thematic = $('#id_thematic');
|
var $thematic = $('#id_thematic');
|
||||||
|
var $team = $('#id_team');
|
||||||
var $categoriesFrom = $('#id_categories_from');
|
var $categoriesFrom = $('#id_categories_from');
|
||||||
var $categoriesTo = $('#id_categories_to');
|
var $categoriesTo = $('#id_categories_to');
|
||||||
var $assetModelsFrom = $('#id_asset_models_from');
|
var $assetModelsFrom = $('#id_asset_models_from');
|
||||||
|
|
@ -24,6 +25,41 @@
|
||||||
|
|
||||||
if (!$thematic.length) return;
|
if (!$thematic.length) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update teams based on selected thematic
|
||||||
|
*/
|
||||||
|
function updateTeams() {
|
||||||
|
if (!$team.length) return;
|
||||||
|
var thematicId = $thematic.val();
|
||||||
|
var currentTeamVal = $team.val();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/interventions/ajax/get-teams-by-thematic/',
|
||||||
|
data: { thematic_id: thematicId || '' },
|
||||||
|
success: function(data) {
|
||||||
|
$team.empty();
|
||||||
|
$team.append($('<option></option>').val('').text('---------'));
|
||||||
|
var hasCurrentVal = false;
|
||||||
|
data.teams.forEach(function(team) {
|
||||||
|
var option = $('<option></option>')
|
||||||
|
.val(team.id)
|
||||||
|
.text(team.name);
|
||||||
|
if (String(team.id) === String(currentTeamVal)) {
|
||||||
|
option.prop('selected', true);
|
||||||
|
hasCurrentVal = true;
|
||||||
|
}
|
||||||
|
$team.append(option);
|
||||||
|
});
|
||||||
|
if (!hasCurrentVal && currentTeamVal) {
|
||||||
|
$team.val('');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
console.error('Error fetching teams');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update categories based on selected thematic
|
* Update categories based on selected thematic
|
||||||
*/
|
*/
|
||||||
|
|
@ -131,6 +167,7 @@
|
||||||
// Listen to thematic changes
|
// Listen to thematic changes
|
||||||
$thematic.on('change', function() {
|
$thematic.on('change', function() {
|
||||||
updateCategories();
|
updateCategories();
|
||||||
|
updateTeams();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen to categories changes using MutationObserver
|
// Listen to categories changes using MutationObserver
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
|
@ -603,6 +604,286 @@ class ExternalManagerPreventivePermissionsTests(TestCase):
|
||||||
self.assertTrue(should_show_created_by_provider_badge(self.internal_manager, intervention))
|
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.create(code='trafficlights', 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ urlpatterns = [
|
||||||
path('<int:intervention_id>/mobile/transfer/', views.transfer_intervention, name='transfer_intervention'),
|
path('<int:intervention_id>/mobile/transfer/', views.transfer_intervention, name='transfer_intervention'),
|
||||||
# Admin AJAX endpoints
|
# Admin AJAX endpoints
|
||||||
path('ajax/get-categories-by-thematic/', views.ajax_get_categories_by_thematic, name='ajax_get_categories_by_thematic'),
|
path('ajax/get-categories-by-thematic/', views.ajax_get_categories_by_thematic, name='ajax_get_categories_by_thematic'),
|
||||||
|
path('ajax/get-teams-by-thematic/', views.ajax_get_teams_by_thematic, name='ajax_get_teams_by_thematic'),
|
||||||
path('ajax/get-subcategories/', views.ajax_get_subcategories, name='ajax_get_subcategories'),
|
path('ajax/get-subcategories/', views.ajax_get_subcategories, name='ajax_get_subcategories'),
|
||||||
path('ajax/get-asset-models/', views.ajax_get_asset_models, name='ajax_get_asset_models'),
|
path('ajax/get-asset-models/', views.ajax_get_asset_models, name='ajax_get_asset_models'),
|
||||||
path('ajax/get-replacement-models-by-category/', views.ajax_get_replacement_models_by_category, name='ajax_get_replacement_models_by_category'),
|
path('ajax/get-replacement-models-by-category/', views.ajax_get_replacement_models_by_category, name='ajax_get_replacement_models_by_category'),
|
||||||
|
|
|
||||||
|
|
@ -1677,6 +1677,7 @@ def interventions_add(request, thematic_code):
|
||||||
if templates:
|
if templates:
|
||||||
intervention.equipment_operation_templates.add(*templates)
|
intervention.equipment_operation_templates.add(*templates)
|
||||||
_assign_guided_template(intervention, templates)
|
_assign_guided_template(intervention, templates)
|
||||||
|
_assign_team_from_templates(intervention, templates)
|
||||||
_add_operations_from_templates(intervention, templates, only_generic=True)
|
_add_operations_from_templates(intervention, templates, only_generic=True)
|
||||||
if asset_pairs:
|
if asset_pairs:
|
||||||
resolved_assets = _resolve_assets_from_pairs(asset_pairs)
|
resolved_assets = _resolve_assets_from_pairs(asset_pairs)
|
||||||
|
|
|
||||||
|
|
@ -1145,6 +1145,32 @@ def ajax_get_categories_by_thematic(request):
|
||||||
return JsonResponse({'error': str(e)}, status=400)
|
return JsonResponse({'error': str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
def ajax_get_teams_by_thematic(request):
|
||||||
|
"""Get teams filtered by thematic for OperationTemplate admin."""
|
||||||
|
thematic_id = request.GET.get('thematic_id')
|
||||||
|
|
||||||
|
try:
|
||||||
|
from contracts.models import CompanyTeam
|
||||||
|
if thematic_id:
|
||||||
|
teams = CompanyTeam.objects.filter(
|
||||||
|
Q(thematics__id=thematic_id) | Q(thematics__isnull=True)
|
||||||
|
).distinct().select_related('company').order_by('name')
|
||||||
|
else:
|
||||||
|
teams = CompanyTeam.objects.select_related('company').order_by('name')
|
||||||
|
|
||||||
|
teams_data = [
|
||||||
|
{
|
||||||
|
'id': t.id,
|
||||||
|
'name': str(t),
|
||||||
|
}
|
||||||
|
for t in teams
|
||||||
|
]
|
||||||
|
|
||||||
|
return JsonResponse({'teams': teams_data})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'error': str(e)}, status=400)
|
||||||
|
|
||||||
|
|
||||||
def ajax_get_subcategories(request):
|
def ajax_get_subcategories(request):
|
||||||
"""AJAX endpoint to get subcategories (children) of a given category.
|
"""AJAX endpoint to get subcategories (children) of a given category.
|
||||||
Used by intervention add/edit forms to populate the sub-category select.
|
Used by intervention add/edit forms to populate the sub-category select.
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ __all__ = [
|
||||||
'_asset_matches_filters',
|
'_asset_matches_filters',
|
||||||
'_asset_matches_template_filters',
|
'_asset_matches_template_filters',
|
||||||
'_assign_guided_template',
|
'_assign_guided_template',
|
||||||
|
'_assign_team_from_templates',
|
||||||
'_attach_equipment_templates',
|
'_attach_equipment_templates',
|
||||||
'_build_guided_media_json',
|
'_build_guided_media_json',
|
||||||
'_build_link_groups',
|
'_build_link_groups',
|
||||||
|
|
@ -1102,6 +1103,26 @@ def _assign_guided_template(intervention, templates):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_team_from_templates(intervention, templates):
|
||||||
|
"""
|
||||||
|
If any selected OperationTemplate has a team FK set,
|
||||||
|
auto-assign the first one found to the intervention.
|
||||||
|
"""
|
||||||
|
if not templates:
|
||||||
|
return
|
||||||
|
for tmpl in templates:
|
||||||
|
if tmpl.team_id:
|
||||||
|
intervention.assigned_team = tmpl.team
|
||||||
|
fields_to_update = ['assigned_team']
|
||||||
|
if not intervention.assigned_provider_id and tmpl.team.company_id:
|
||||||
|
intervention.assigned_provider = tmpl.team.company
|
||||||
|
fields_to_update.append('assigned_provider')
|
||||||
|
intervention.save(update_fields=fields_to_update)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _get_equipment_templates_for_intervention(intervention):
|
def _get_equipment_templates_for_intervention(intervention):
|
||||||
templates = list(intervention.equipment_operation_templates.all())
|
templates = list(intervention.equipment_operation_templates.all())
|
||||||
if templates:
|
if templates:
|
||||||
|
|
|
||||||
|
|
@ -319,17 +319,28 @@ def intervention_add_preventive(request, thematic_code):
|
||||||
# Utiliser la même commande pour toutes les répétitions
|
# Utiliser la même commande pour toutes les répétitions
|
||||||
orders_list = [order] * recurrence_count
|
orders_list = [order] * recurrence_count
|
||||||
|
|
||||||
|
# --- Pré-déterminer l'équipe associée aux templates d'opérations
|
||||||
|
template_assigned_team = None
|
||||||
|
for tmpl in operation_templates:
|
||||||
|
if tmpl.team_id:
|
||||||
|
template_assigned_team = tmpl.team
|
||||||
|
break
|
||||||
|
|
||||||
# --- Fonction utilitaire pour créer une intervention
|
# --- Fonction utilitaire pour créer une intervention
|
||||||
def create_base_intervention(i: int, order_for_intervention):
|
def create_base_intervention(i: int, order_for_intervention):
|
||||||
start_date = expected_start + i * recurrence_delta
|
start_date = expected_start + i * recurrence_delta
|
||||||
end_date = expected_end + i * recurrence_delta
|
end_date = expected_end + i * recurrence_delta
|
||||||
|
assigned_provider = contract.company if contract else None
|
||||||
|
if not assigned_provider and template_assigned_team and template_assigned_team.company_id:
|
||||||
|
assigned_provider = template_assigned_team.company
|
||||||
intervention = Intervention.objects.create(
|
intervention = Intervention.objects.create(
|
||||||
title=title,
|
title=title,
|
||||||
status=status,
|
status=status,
|
||||||
status_order=STATUS_ORDERS[status],
|
status_order=STATUS_ORDERS[status],
|
||||||
contract=contract,
|
contract=contract,
|
||||||
order=order_for_intervention,
|
order=order_for_intervention,
|
||||||
assigned_provider=contract.company if contract else None,
|
assigned_provider=assigned_provider,
|
||||||
|
assigned_team=template_assigned_team,
|
||||||
expected_begin_time=start_date,
|
expected_begin_time=start_date,
|
||||||
expected_end_time=end_date,
|
expected_end_time=end_date,
|
||||||
maintain_type="preventive",
|
maintain_type="preventive",
|
||||||
|
|
@ -437,6 +448,7 @@ def intervention_add_preventive(request, thematic_code):
|
||||||
intervention = create_base_intervention(i, order_for_intervention)
|
intervention = create_base_intervention(i, order_for_intervention)
|
||||||
_attach_equipment_templates(intervention, operation_templates)
|
_attach_equipment_templates(intervention, operation_templates)
|
||||||
_assign_guided_template(intervention, operation_templates)
|
_assign_guided_template(intervention, operation_templates)
|
||||||
|
_assign_team_from_templates(intervention, operation_templates)
|
||||||
|
|
||||||
# 1) Lien générique intervention ↔ localisation
|
# 1) Lien générique intervention ↔ localisation
|
||||||
ct = ContentType.objects.get_for_model(loc.__class__)
|
ct = ContentType.objects.get_for_model(loc.__class__)
|
||||||
|
|
@ -563,6 +575,7 @@ def intervention_add_preventive(request, thematic_code):
|
||||||
intervention = create_base_intervention(i, order_for_intervention)
|
intervention = create_base_intervention(i, order_for_intervention)
|
||||||
_attach_equipment_templates(intervention, operation_templates)
|
_attach_equipment_templates(intervention, operation_templates)
|
||||||
_assign_guided_template(intervention, operation_templates)
|
_assign_guided_template(intervention, operation_templates)
|
||||||
|
_assign_team_from_templates(intervention, operation_templates)
|
||||||
|
|
||||||
# 1) Lien générique Asset ↔ Intervention
|
# 1) Lien générique Asset ↔ Intervention
|
||||||
InterventionAsset.objects.get_or_create(
|
InterventionAsset.objects.get_or_create(
|
||||||
|
|
@ -633,6 +646,7 @@ def intervention_add_preventive(request, thematic_code):
|
||||||
intervention = create_base_intervention(i, order_for_intervention)
|
intervention = create_base_intervention(i, order_for_intervention)
|
||||||
_attach_equipment_templates(intervention, operation_templates)
|
_attach_equipment_templates(intervention, operation_templates)
|
||||||
_assign_guided_template(intervention, operation_templates)
|
_assign_guided_template(intervention, operation_templates)
|
||||||
|
_assign_team_from_templates(intervention, operation_templates)
|
||||||
|
|
||||||
# Utiliser le titre de base
|
# Utiliser le titre de base
|
||||||
intervention.title = title
|
intervention.title = title
|
||||||
|
|
|
||||||
|
|
@ -486,6 +486,8 @@ def process_worksheet(
|
||||||
|
|
||||||
# Lien template d'opérations
|
# Lien template d'opérations
|
||||||
intervention.equipment_operation_templates.add(template)
|
intervention.equipment_operation_templates.add(template)
|
||||||
|
from interventions.views.helpers import _assign_team_from_templates
|
||||||
|
_assign_team_from_templates(intervention, [template])
|
||||||
|
|
||||||
# Timelines
|
# Timelines
|
||||||
InterventionTimeLine.objects.create(
|
InterventionTimeLine.objects.create(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue