feat: implement automatic contract post assignment for interventions based on operation templates
This commit is contained in:
parent
cfd11654ab
commit
be04a5e4c0
9 changed files with 482 additions and 3 deletions
|
|
@ -90,6 +90,35 @@ def _get_symptom_autoclose_inline():
|
|||
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)
|
||||
class CompanyAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'contact_email', 'phone_number')
|
||||
|
|
@ -102,7 +131,7 @@ class ContractAdmin(admin.ModelAdmin):
|
|||
filter_horizontal = ('thematics',)
|
||||
|
||||
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 = (
|
||||
(None, {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ from .models import (
|
|||
InspectionConfiguration, InspectionConfigurationContract,
|
||||
OperationTemplate, OperationTemplateItem, OperationType,
|
||||
OperationTemplateAssetModelFilter,
|
||||
ContractOperationTemplatePost,
|
||||
Operation, OperationPhoto
|
||||
)
|
||||
from .services import generate_interventions_excel
|
||||
|
|
@ -777,13 +778,23 @@ class OperationTemplateAdminForm(forms.ModelForm):
|
|||
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)
|
||||
class OperationTemplateAdmin(admin.ModelAdmin):
|
||||
form = OperationTemplateAdminForm
|
||||
list_display = ('code', 'name', 'description', 'thematic', 'team', 'categories_list', 'models_list')
|
||||
search_fields = ('name', 'code', 'description')
|
||||
list_filter = ('thematic', 'team', 'categories')
|
||||
filter_horizontal = ('categories',)
|
||||
inlines = [OperationTemplateItemInline]
|
||||
inlines = [OperationTemplateItemInline, OperationTemplateContractPostInline]
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'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 '')
|
||||
|
||||
|
||||
@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)
|
||||
class OperationTemplateItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('code', 'get_name', 'template', 'status_set', 'measurement_definition', 'is_generic', 'order')
|
||||
|
|
|
|||
|
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -2,6 +2,7 @@ from django.contrib.gis.db import models
|
|||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.db.models import Sum, F, Q
|
||||
|
|
@ -1678,6 +1679,65 @@ class OperationTemplateItem(models.Model):
|
|||
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 = {
|
||||
'transmission': _('Envoi de la fiche'),
|
||||
'reminder': _('Rappel'),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from assets.models import AssetCategory
|
|||
from interventions.models import (
|
||||
Intervention,
|
||||
InterventionContractPost,
|
||||
ContractOperationTemplatePost,
|
||||
InterventionPlanificationTimeLine,
|
||||
InterventionRealizationTimeLine,
|
||||
Operation,
|
||||
|
|
@ -882,6 +883,287 @@ class PreventiveTemplateTeamAssignmentTests(TestCase):
|
|||
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')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1868,6 +1868,7 @@ def interventions_add(request, thematic_code):
|
|||
intervention.equipment_operation_templates.add(*templates)
|
||||
_assign_guided_template(intervention, templates)
|
||||
_assign_team_from_templates(intervention, templates)
|
||||
_assign_contract_posts_from_templates(intervention, templates)
|
||||
_add_operations_from_templates(intervention, templates, only_generic=True)
|
||||
if asset_pairs:
|
||||
resolved_assets = _resolve_assets_from_pairs(asset_pairs)
|
||||
|
|
@ -1884,6 +1885,8 @@ def interventions_add(request, thematic_code):
|
|||
)
|
||||
if 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:
|
||||
intervention.assigned_provider = resolved_provider
|
||||
if resolved_contract or resolved_provider:
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ from interventions.models import (InterventionSubscription, Intervention, Interv
|
|||
InterventionOccupation,
|
||||
Operation, OperationTemplate, OperationTemplateItem, OperationStatus, OperationStatusSet, MeasurementDefinition,
|
||||
GuidedOperationTemplate, GuidedOperationData, GuidedOperationMedia,
|
||||
ContractOperationTemplatePost,
|
||||
InterventionPostPeriodQuantity, InterventionEmailLog, InterventionAssignment,
|
||||
DirectContractRouting)
|
||||
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',
|
||||
'_asset_matches_filters',
|
||||
'_asset_matches_template_filters',
|
||||
'_assign_contract_posts_from_templates',
|
||||
'_assign_guided_template',
|
||||
'_assign_team_from_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):
|
||||
templates = list(intervention.equipment_operation_templates.all())
|
||||
if templates:
|
||||
|
|
|
|||
|
|
@ -449,6 +449,7 @@ def intervention_add_preventive(request, thematic_code):
|
|||
_attach_equipment_templates(intervention, operation_templates)
|
||||
_assign_guided_template(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
|
||||
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)
|
||||
_assign_guided_template(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
|
||||
InterventionAsset.objects.get_or_create(
|
||||
|
|
@ -647,6 +649,7 @@ def intervention_add_preventive(request, thematic_code):
|
|||
_attach_equipment_templates(intervention, operation_templates)
|
||||
_assign_guided_template(intervention, operation_templates)
|
||||
_assign_team_from_templates(intervention, operation_templates)
|
||||
_assign_contract_posts_from_templates(intervention, operation_templates)
|
||||
|
||||
# Utiliser le titre de base
|
||||
intervention.title = title
|
||||
|
|
|
|||
|
|
@ -486,8 +486,9 @@ def process_worksheet(
|
|||
|
||||
# Lien template d'opérations
|
||||
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_contract_posts_from_templates(intervention, [template])
|
||||
|
||||
# Timelines
|
||||
InterventionTimeLine.objects.create(
|
||||
|
|
|
|||
Loading…
Reference in a new issue