feat: implement automatic contract post assignment for interventions based on operation templates

This commit is contained in:
kdeterme 2026-08-19 15:19:05 +02:00
parent cfd11654ab
commit be04a5e4c0
9 changed files with 482 additions and 3 deletions

View file

@ -90,6 +90,35 @@ def _get_symptom_autoclose_inline():
return SymptomAutoCloseInline return SymptomAutoCloseInline
def _get_operation_template_posts_inline():
from interventions.models import ContractOperationTemplatePost, OperationTemplate
from django.db.models import Q
class ContractOperationTemplatePostInline(admin.TabularInline):
model = ContractOperationTemplatePost
extra = 0
fields = ('operation_template', 'contract_post', 'default_quantity', 'comment')
verbose_name = "Poste par défaut (Template d'opérations)"
verbose_name_plural = "Postes par défaut (Templates d'opérations)"
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "contract_post":
contract_id = request.resolver_match.kwargs.get('object_id') if request.resolver_match else None
if contract_id:
kwargs["queryset"] = ContractPost.objects.filter(contract_id=contract_id).order_by('order_number')
elif db_field.name == "operation_template":
contract_id = request.resolver_match.kwargs.get('object_id') if request.resolver_match else None
if contract_id:
contract = Contract.objects.filter(pk=contract_id).first()
if contract and contract.thematics.exists():
kwargs["queryset"] = OperationTemplate.objects.filter(
Q(thematic__in=contract.thematics.all()) | Q(thematic__isnull=True)
).distinct().order_by('name')
return super().formfield_for_foreignkey(db_field, request, **kwargs)
return ContractOperationTemplatePostInline
@admin.register(Company) @admin.register(Company)
class CompanyAdmin(admin.ModelAdmin): class CompanyAdmin(admin.ModelAdmin):
list_display = ('name', 'contact_email', 'phone_number') list_display = ('name', 'contact_email', 'phone_number')
@ -102,7 +131,7 @@ class ContractAdmin(admin.ModelAdmin):
filter_horizontal = ('thematics',) filter_horizontal = ('thematics',)
def get_inlines(self, request, obj=None): def get_inlines(self, request, obj=None):
return [_get_pause_reason_inline(), _get_symptom_autoclose_inline()] + list(super().get_inlines(request, obj)) return [_get_pause_reason_inline(), _get_symptom_autoclose_inline(), _get_operation_template_posts_inline()] + list(super().get_inlines(request, obj))
fieldsets = ( fieldsets = (
(None, { (None, {

View file

@ -31,6 +31,7 @@ from .models import (
InspectionConfiguration, InspectionConfigurationContract, InspectionConfiguration, InspectionConfigurationContract,
OperationTemplate, OperationTemplateItem, OperationType, OperationTemplate, OperationTemplateItem, OperationType,
OperationTemplateAssetModelFilter, OperationTemplateAssetModelFilter,
ContractOperationTemplatePost,
Operation, OperationPhoto Operation, OperationPhoto
) )
from .services import generate_interventions_excel from .services import generate_interventions_excel
@ -777,13 +778,23 @@ class OperationTemplateAdminForm(forms.ModelForm):
js = ('interventions/admin_operation_template_dynamic.js',) js = ('interventions/admin_operation_template_dynamic.js',)
class OperationTemplateContractPostInline(admin.TabularInline):
model = ContractOperationTemplatePost
extra = 0
fields = ('contract', 'contract_post', 'default_quantity', 'comment')
verbose_name = "Poste de contrat par défaut"
verbose_name_plural = "Postes de contrats par défaut"
autocomplete_fields = ('contract', 'contract_post')
@admin.register(OperationTemplate) @admin.register(OperationTemplate)
class OperationTemplateAdmin(admin.ModelAdmin): class OperationTemplateAdmin(admin.ModelAdmin):
form = OperationTemplateAdminForm form = OperationTemplateAdminForm
list_display = ('code', 'name', 'description', 'thematic', 'team', 'categories_list', 'models_list') list_display = ('code', 'name', 'description', 'thematic', 'team', 'categories_list', 'models_list')
search_fields = ('name', 'code', 'description')
list_filter = ('thematic', 'team', 'categories') list_filter = ('thematic', 'team', 'categories')
filter_horizontal = ('categories',) filter_horizontal = ('categories',)
inlines = [OperationTemplateItemInline] inlines = [OperationTemplateItemInline, OperationTemplateContractPostInline]
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('code', 'name', 'description', 'thematic', 'team', 'guided_template') 'fields': ('code', 'name', 'description', 'thematic', 'team', 'guided_template')
@ -813,6 +824,19 @@ class OperationTemplateAdmin(admin.ModelAdmin):
return ', '.join(str(f) for f in filters) + ('...' if obj.asset_model_filters.count() > 3 else '') return ', '.join(str(f) for f in filters) + ('...' if obj.asset_model_filters.count() > 3 else '')
@admin.register(ContractOperationTemplatePost)
class ContractOperationTemplatePostAdmin(admin.ModelAdmin):
list_display = ('contract', 'operation_template', 'contract_post', 'default_quantity', 'comment')
list_filter = ('contract', 'operation_template', 'contract__thematics')
search_fields = (
'contract__contract_number',
'operation_template__name',
'contract_post__code',
'contract_post__description_fr',
)
autocomplete_fields = ('contract', 'operation_template', 'contract_post')
@admin.register(OperationTemplateItem) @admin.register(OperationTemplateItem)
class OperationTemplateItemAdmin(admin.ModelAdmin): class OperationTemplateItemAdmin(admin.ModelAdmin):
list_display = ('code', 'get_name', 'template', 'status_set', 'measurement_definition', 'is_generic', 'order') list_display = ('code', 'get_name', 'template', 'status_set', 'measurement_definition', 'is_generic', 'order')

View file

@ -0,0 +1,32 @@
# Generated by Django 6.0.7 on 2026-08-19 13:11
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contracts', '0003_contract_allow_team_members_edit'),
('interventions', '0004_alter_intervention_type'),
]
operations = [
migrations.CreateModel(
name='ContractOperationTemplatePost',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('default_quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Quantité par défaut')),
('comment', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Commentaire par défaut')),
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operation_template_posts', to='contracts.contract', verbose_name='Contrat')),
('contract_post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operation_template_posts', to='contracts.contractpost', verbose_name='Poste de contrat')),
('operation_template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contract_posts', to='interventions.operationtemplate', verbose_name="Template d'opérations")),
],
options={
'verbose_name': "Poste par défaut (Contrat × Template d'opérations)",
'verbose_name_plural': "Postes par défaut (Contrats × Templates d'opérations)",
'ordering': ['contract', 'operation_template', 'contract_post__order_number'],
'unique_together': {('contract', 'operation_template', 'contract_post')},
},
),
]

View file

@ -2,6 +2,7 @@ from django.contrib.gis.db import models
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import get_language, gettext_lazy as _ from django.utils.translation import get_language, gettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.db.models import Sum, F, Q from django.db.models import Sum, F, Q
@ -1678,6 +1679,65 @@ class OperationTemplateItem(models.Model):
return f"{self.template.name} - {self.name_fr}" return f"{self.template.name} - {self.name_fr}"
class ContractOperationTemplatePost(models.Model):
"""
Associe un poste de contrat à un template d'opérations avec une quantité par défaut.
Utilisé lors de la création d'interventions préventives pour pré-remplir les postes de contrat.
"""
contract = models.ForeignKey(
Contract,
on_delete=models.CASCADE,
related_name='operation_template_posts',
verbose_name=_("Contrat"),
)
operation_template = models.ForeignKey(
OperationTemplate,
on_delete=models.CASCADE,
related_name='contract_posts',
verbose_name=_("Template d'opérations"),
)
contract_post = models.ForeignKey(
ContractPost,
on_delete=models.CASCADE,
related_name='operation_template_posts',
verbose_name=_("Poste de contrat"),
)
default_quantity = models.DecimalField(
max_digits=10,
decimal_places=2,
default=1,
verbose_name=_("Quantité par défaut"),
)
comment = models.CharField(
max_length=1024,
blank=True,
null=True,
verbose_name=_("Commentaire par défaut"),
)
class Meta:
unique_together = [('contract', 'operation_template', 'contract_post')]
ordering = ['contract', 'operation_template', 'contract_post__order_number']
verbose_name = _("Poste par défaut (Contrat × Template d'opérations)")
verbose_name_plural = _("Postes par défaut (Contrats × Templates d'opérations)")
def __str__(self):
return f"{self.contract.contract_number} × {self.operation_template.name} → {self.contract_post.code} ({self.default_quantity})"
def clean(self):
super().clean()
if self.contract_post_id and self.contract_id:
if self.contract_post.contract_id != self.contract_id:
raise ValidationError({
'contract_post': _("Ce poste n'appartient pas au contrat sélectionné.")
})
def save(self, *args, **kwargs):
if self.contract_post_id and not self.contract_id:
self.contract = self.contract_post.contract
super().save(*args, **kwargs)
EMAIL_LOG_TYPE_CHOICES = { EMAIL_LOG_TYPE_CHOICES = {
'transmission': _('Envoi de la fiche'), 'transmission': _('Envoi de la fiche'),
'reminder': _('Rappel'), 'reminder': _('Rappel'),

View file

@ -15,6 +15,7 @@ from assets.models import AssetCategory
from interventions.models import ( from interventions.models import (
Intervention, Intervention,
InterventionContractPost, InterventionContractPost,
ContractOperationTemplatePost,
InterventionPlanificationTimeLine, InterventionPlanificationTimeLine,
InterventionRealizationTimeLine, InterventionRealizationTimeLine,
Operation, Operation,
@ -882,6 +883,287 @@ class PreventiveTemplateTeamAssignmentTests(TestCase):
self.assertGreaterEqual(len(data['teams']), 2) 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)
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')

View file

@ -1868,6 +1868,7 @@ def interventions_add(request, thematic_code):
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) _assign_team_from_templates(intervention, templates)
_assign_contract_posts_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)
@ -1884,6 +1885,8 @@ def interventions_add(request, thematic_code):
) )
if resolved_contract: if resolved_contract:
intervention.contract = resolved_contract intervention.contract = resolved_contract
if inspection_config and templates:
_assign_contract_posts_from_templates(intervention, templates)
if resolved_provider and not intervention.assigned_provider: if resolved_provider and not intervention.assigned_provider:
intervention.assigned_provider = resolved_provider intervention.assigned_provider = resolved_provider
if resolved_contract or resolved_provider: if resolved_contract or resolved_provider:

View file

@ -60,6 +60,7 @@ from interventions.models import (InterventionSubscription, Intervention, Interv
InterventionOccupation, InterventionOccupation,
Operation, OperationTemplate, OperationTemplateItem, OperationStatus, OperationStatusSet, MeasurementDefinition, Operation, OperationTemplate, OperationTemplateItem, OperationStatus, OperationStatusSet, MeasurementDefinition,
GuidedOperationTemplate, GuidedOperationData, GuidedOperationMedia, GuidedOperationTemplate, GuidedOperationData, GuidedOperationMedia,
ContractOperationTemplatePost,
InterventionPostPeriodQuantity, InterventionEmailLog, InterventionAssignment, InterventionPostPeriodQuantity, InterventionEmailLog, InterventionAssignment,
DirectContractRouting) DirectContractRouting)
from interventions.models import STATUS_CHOICES, STATUS_ORDERS, MAINTAIN_CHOICES, TYPE_CHOICES, ORIGIN_TYPE_CHOICES, PAUSE_REASON_CHOICES, get_type_choices from interventions.models import STATUS_CHOICES, STATUS_ORDERS, MAINTAIN_CHOICES, TYPE_CHOICES, ORIGIN_TYPE_CHOICES, PAUSE_REASON_CHOICES, get_type_choices
@ -145,6 +146,7 @@ __all__ = [
'_apply_realization_side_effects', '_apply_realization_side_effects',
'_asset_matches_filters', '_asset_matches_filters',
'_asset_matches_template_filters', '_asset_matches_template_filters',
'_assign_contract_posts_from_templates',
'_assign_guided_template', '_assign_guided_template',
'_assign_team_from_templates', '_assign_team_from_templates',
'_attach_equipment_templates', '_attach_equipment_templates',
@ -1123,6 +1125,49 @@ def _assign_team_from_templates(intervention, templates):
def _assign_contract_posts_from_templates(intervention, templates):
"""
Associe les postes de contrat par défaut configurés pour les OperationTemplates donnés
sur le contrat de l'intervention.
Si plusieurs templates ont des postes en doublon, le premier trouvé est conservé.
"""
if not templates:
return
contract = intervention.contract or (intervention.order.contract if intervention.order else None)
if not contract:
return
existing_post_ids = set(
intervention.interv_contract_posts.values_list('contract_post_id', flat=True)
)
posts_to_create = []
for template in templates:
template_posts = (
ContractOperationTemplatePost.objects
.filter(contract=contract, operation_template=template)
.select_related('contract_post')
.order_by('contract_post__order_number', 'id')
)
for tp in template_posts:
if tp.contract_post_id not in existing_post_ids:
posts_to_create.append(
InterventionContractPost(
intervention=intervention,
contract_post=tp.contract_post,
quantity=tp.default_quantity,
comment=tp.comment or '',
)
)
existing_post_ids.add(tp.contract_post_id)
if posts_to_create:
InterventionContractPost.objects.bulk_create(posts_to_create)
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:

View file

@ -449,6 +449,7 @@ def intervention_add_preventive(request, thematic_code):
_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) _assign_team_from_templates(intervention, operation_templates)
_assign_contract_posts_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__)
@ -576,6 +577,7 @@ def intervention_add_preventive(request, thematic_code):
_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) _assign_team_from_templates(intervention, operation_templates)
_assign_contract_posts_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(
@ -647,6 +649,7 @@ def intervention_add_preventive(request, thematic_code):
_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) _assign_team_from_templates(intervention, operation_templates)
_assign_contract_posts_from_templates(intervention, operation_templates)
# Utiliser le titre de base # Utiliser le titre de base
intervention.title = title intervention.title = title

View file

@ -486,8 +486,9 @@ 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 from interventions.views.helpers import _assign_team_from_templates, _assign_contract_posts_from_templates
_assign_team_from_templates(intervention, [template]) _assign_team_from_templates(intervention, [template])
_assign_contract_posts_from_templates(intervention, [template])
# Timelines # Timelines
InterventionTimeLine.objects.create( InterventionTimeLine.objects.create(