feat: implement intervention duplication functionality with mobile UI support and comprehensive unit tests
This commit is contained in:
parent
86979307ae
commit
1671d9e449
7 changed files with 437 additions and 9 deletions
|
|
@ -1551,4 +1551,24 @@ def can_user_create_inspection_for_contract(user, contract):
|
||||||
user_contract=uca,
|
user_contract=uca,
|
||||||
status__in=target_statuses,
|
status__in=target_statuses,
|
||||||
can_change_status_to=True,
|
can_change_status_to=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def can_duplicate_intervention(user, intervention):
|
||||||
|
"""
|
||||||
|
Vérifie si l'utilisateur peut dupliquer l'intervention donnée.
|
||||||
|
L'utilisateur doit pouvoir voir l'intervention source et pouvoir créer une nouvelle intervention
|
||||||
|
pour la thématique concernée.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_config = UserConfig.objects.get(user=user)
|
||||||
|
except UserConfig.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if user_config.roles.filter(name__in=['admin', 'top_manager']).exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not can_view_intervention(user, intervention):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return can_add_intervention(user, intervention.thematic)
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
{% load i18n interventions_tags %}
|
{% load i18n interventions_tags %}
|
||||||
{% if linked_groups or can_manage_links %}
|
{% if linked_groups or can_manage_links or can_duplicate_intervention %}
|
||||||
<div id="section-liens" class="card mt-3 mb-4">
|
<div id="section-liens" class="card mt-3 mb-4">
|
||||||
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
||||||
<h4>
|
<h4 class="mb-0">
|
||||||
<i class="bi bi-link-45deg"></i>
|
<i class="bi bi-link-45deg"></i>
|
||||||
{% translate "Interventions liées" %}
|
{% translate "Interventions liées" %}
|
||||||
</h4>
|
</h4>
|
||||||
|
{% if can_duplicate_intervention %}
|
||||||
|
<form method="post" action="{% url 'interventions:duplicate_intervention' intervention.id %}" class="d-inline" onsubmit="return confirm('{% translate "Êtes-vous sûr de vouloir dupliquer cette intervention ?" %}');">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="bi bi-copy"></i>
|
||||||
|
{% translate "Dupliquer" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if linked_groups %}
|
{% if linked_groups %}
|
||||||
|
|
|
||||||
|
|
@ -1762,12 +1762,23 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if linked_groups %}
|
{% if linked_groups or can_duplicate_intervention %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="text-uppercase text-muted small mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<i class="bi bi-link-45deg"></i> {% translate "Interventions liées" %}
|
<h6 class="text-uppercase text-muted small mb-0">
|
||||||
</h6>
|
<i class="bi bi-link-45deg"></i> {% translate "Interventions liées" %}
|
||||||
|
</h6>
|
||||||
|
{% if can_duplicate_intervention %}
|
||||||
|
<form method="post" action="{% url 'interventions:duplicate_intervention' intervention.id %}" class="d-inline" onsubmit="return confirm('{% translate "Êtes-vous sûr de vouloir dupliquer cette intervention ?" %}');">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="variant" value="mobile">
|
||||||
|
<button type="submit" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="bi bi-copy"></i> {% translate "Dupliquer" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% for group in linked_groups %}
|
{% for group in linked_groups %}
|
||||||
<p class="text-muted mb-1 fw-semibold">{{ group.label }}</p>
|
<p class="text-muted mb-1 fw-semibold">{{ group.label }}</p>
|
||||||
<ul class="list-unstyled {% if not forloop.last %}mb-3{% else %}mb-0{% endif %}">
|
<ul class="list-unstyled {% if not forloop.last %}mb-3{% else %}mb-0{% endif %}">
|
||||||
|
|
@ -1782,6 +1793,8 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-muted mb-0 small">{% translate "Aucune intervention liée pour l'instant." %}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ from interventions.models import (
|
||||||
InterventionContractPost,
|
InterventionContractPost,
|
||||||
InterventionPlanificationTimeLine,
|
InterventionPlanificationTimeLine,
|
||||||
InterventionRealizationTimeLine,
|
InterventionRealizationTimeLine,
|
||||||
|
InterventionTimeLine,
|
||||||
|
InterventionLink,
|
||||||
|
InterventionNote,
|
||||||
|
Symptom,
|
||||||
Operation,
|
Operation,
|
||||||
InterventionAsset,
|
InterventionAsset,
|
||||||
OperationTemplate,
|
OperationTemplate,
|
||||||
|
|
@ -25,6 +29,7 @@ from interventions.models import (
|
||||||
STATUS_ORDERS,
|
STATUS_ORDERS,
|
||||||
InterventionDocument,
|
InterventionDocument,
|
||||||
)
|
)
|
||||||
|
from django.db.models import Q
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from interventions.views import _group_operations_by_asset_and_template, _create_equipment_operations_for_assets
|
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 interventions.permissions import can_manage_occupations, can_delete_document
|
||||||
|
|
@ -1367,5 +1372,195 @@ class InterventionSubscriptionTests(TestCase):
|
||||||
self.assertNotIn(itv2, filtered_qs)
|
self.assertNotIn(itv2, filtered_qs)
|
||||||
|
|
||||||
|
|
||||||
|
class InterventionDuplicationTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.admin_user = User.objects.create_superuser(username='dup-admin', password='pwd', email='admin@example.com')
|
||||||
|
self.admin_role = Role.objects.create(name='admin')
|
||||||
|
self.admin_config = UserConfig.objects.create(user=self.admin_user, is_intern=True)
|
||||||
|
self.admin_config.roles.add(self.admin_role)
|
||||||
|
|
||||||
|
self.thematic = Thematic.objects.create(code='roads_dup', name_fr='Routes Dup', name_nl='Wegen Dup')
|
||||||
|
self.symptom = Symptom.objects.create(thematic=self.thematic, name_fr='Nid de poule', name_nl='Kuil', is_active=True)
|
||||||
|
|
||||||
|
self.company = Company.objects.create(name='Provider Co')
|
||||||
|
self.team = CompanyTeam.objects.create(name='Team A', company=self.company)
|
||||||
|
self.member = CompanyMember.objects.create(user=self.admin_user, name='Member A')
|
||||||
|
self.member.teams.add(self.team)
|
||||||
|
|
||||||
|
from datetime import date, timedelta
|
||||||
|
self.contract = Contract.objects.create(
|
||||||
|
company=self.company,
|
||||||
|
contract_number='C-DUP-01',
|
||||||
|
start_date=date.today() - timedelta(days=10),
|
||||||
|
end_date=date.today() + timedelta(days=100),
|
||||||
|
)
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
self.itv = Intervention.objects.create(
|
||||||
|
title='Intervention originale',
|
||||||
|
description='Description détaillée',
|
||||||
|
init_description='Origine signalement',
|
||||||
|
maintain_type='corrective',
|
||||||
|
type='repair',
|
||||||
|
origin_type='damage',
|
||||||
|
priority='1',
|
||||||
|
thematic=self.thematic,
|
||||||
|
symptom=self.symptom,
|
||||||
|
contract=self.contract,
|
||||||
|
assigned_provider=self.company,
|
||||||
|
assigned_team=self.team,
|
||||||
|
assigned_member=self.member,
|
||||||
|
status='in_progress',
|
||||||
|
status_order=21,
|
||||||
|
begin_time=now - timedelta(hours=2),
|
||||||
|
end_time=now + timedelta(hours=1),
|
||||||
|
planned_begin_time=now - timedelta(hours=3),
|
||||||
|
planned_end_time=now + timedelta(hours=2),
|
||||||
|
expected_begin_time=now - timedelta(hours=4),
|
||||||
|
expected_end_time=now + timedelta(hours=3),
|
||||||
|
transmission_time=now - timedelta(days=1),
|
||||||
|
address='Rue Royale 10, 1000 Bruxelles',
|
||||||
|
location_code='LOC-001',
|
||||||
|
lon=4.35,
|
||||||
|
lat=50.85,
|
||||||
|
created_by=self.admin_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
self.origin_note = InterventionNote.objects.create(
|
||||||
|
intervention=self.itv,
|
||||||
|
content='Note origine importante',
|
||||||
|
note_time=now,
|
||||||
|
note_type=InterventionNote.NoteType.ORIGIN,
|
||||||
|
note_author=self.admin_user,
|
||||||
|
)
|
||||||
|
self.work_note = InterventionNote.objects.create(
|
||||||
|
intervention=self.itv,
|
||||||
|
content='Note travaux réalisés à ne pas copier',
|
||||||
|
note_time=now,
|
||||||
|
note_type=InterventionNote.NoteType.WORK_DONE,
|
||||||
|
note_author=self.admin_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Document
|
||||||
|
self.doc = InterventionDocument.objects.create(
|
||||||
|
intervention=self.itv,
|
||||||
|
file=SimpleUploadedFile('test_dup_doc.txt', b'hello world duplicated file'),
|
||||||
|
uploaded_by=self.admin_user,
|
||||||
|
description='Doc description',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Operation template & Operation
|
||||||
|
self.template = OperationTemplate.objects.create(name='Template Dup', thematic=self.thematic)
|
||||||
|
self.itv.equipment_operation_templates.add(self.template)
|
||||||
|
self.op = Operation.objects.create(
|
||||||
|
intervention=self.itv,
|
||||||
|
name_fr='Vérifier enrobé',
|
||||||
|
name_nl='Asfalt controleren',
|
||||||
|
template_group='Template Dup',
|
||||||
|
order=1,
|
||||||
|
is_custom=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_duplication_success(self):
|
||||||
|
self.client.force_login(self.admin_user)
|
||||||
|
url = reverse('interventions:duplicate_intervention', kwargs={'intervention_id': self.itv.id})
|
||||||
|
response = self.client.post(url)
|
||||||
|
|
||||||
|
# Should redirect to the new intervention detail
|
||||||
|
new_itv = Intervention.objects.exclude(id=self.itv.id).first()
|
||||||
|
self.assertIsNotNone(new_itv)
|
||||||
|
self.assertRedirects(response, reverse('interventions:intervention_detail', kwargs={'intervention_id': new_itv.id}))
|
||||||
|
|
||||||
|
# General properties copied
|
||||||
|
self.assertEqual(new_itv.title, self.itv.title)
|
||||||
|
self.assertEqual(new_itv.description, self.itv.description)
|
||||||
|
self.assertEqual(new_itv.init_description, self.itv.init_description)
|
||||||
|
self.assertEqual(new_itv.thematic, self.itv.thematic)
|
||||||
|
self.assertEqual(new_itv.symptom, self.itv.symptom)
|
||||||
|
self.assertEqual(new_itv.priority, self.itv.priority)
|
||||||
|
self.assertEqual(new_itv.maintain_type, self.itv.maintain_type)
|
||||||
|
self.assertEqual(new_itv.type, self.itv.type)
|
||||||
|
self.assertEqual(new_itv.origin_type, self.itv.origin_type)
|
||||||
|
self.assertEqual(new_itv.address, self.itv.address)
|
||||||
|
self.assertEqual(new_itv.location_code, self.itv.location_code)
|
||||||
|
self.assertEqual(new_itv.lon, self.itv.lon)
|
||||||
|
self.assertEqual(new_itv.lat, self.itv.lat)
|
||||||
|
|
||||||
|
# Status reset to in_preparation
|
||||||
|
self.assertEqual(new_itv.status, 'in_preparation')
|
||||||
|
self.assertEqual(new_itv.status_order, 1)
|
||||||
|
|
||||||
|
# Contract, provider, team, members and dates NOT copied
|
||||||
|
self.assertIsNone(new_itv.contract)
|
||||||
|
self.assertIsNone(new_itv.order)
|
||||||
|
self.assertIsNone(new_itv.assigned_provider)
|
||||||
|
self.assertIsNone(new_itv.assigned_team)
|
||||||
|
self.assertIsNone(new_itv.assigned_member)
|
||||||
|
self.assertIsNone(new_itv.intervention_manager)
|
||||||
|
self.assertIsNone(new_itv.begin_time)
|
||||||
|
self.assertIsNone(new_itv.end_time)
|
||||||
|
self.assertIsNone(new_itv.planned_begin_time)
|
||||||
|
self.assertIsNone(new_itv.planned_end_time)
|
||||||
|
self.assertIsNone(new_itv.expected_begin_time)
|
||||||
|
self.assertIsNone(new_itv.expected_end_time)
|
||||||
|
self.assertIsNone(new_itv.transmission_time)
|
||||||
|
|
||||||
|
# Template & operations copied
|
||||||
|
self.assertEqual(list(new_itv.equipment_operation_templates.all()), [self.template])
|
||||||
|
self.assertEqual(new_itv.operations.count(), 1)
|
||||||
|
new_op = new_itv.operations.first()
|
||||||
|
self.assertEqual(new_op.name_fr, self.op.name_fr)
|
||||||
|
self.assertIsNone(new_op.status)
|
||||||
|
|
||||||
|
# Notes: only origin note copied
|
||||||
|
self.assertEqual(new_itv.notes.count(), 1)
|
||||||
|
copied_note = new_itv.notes.first()
|
||||||
|
self.assertEqual(copied_note.note_type, InterventionNote.NoteType.ORIGIN)
|
||||||
|
self.assertEqual(copied_note.content, self.origin_note.content)
|
||||||
|
|
||||||
|
# Documents copied
|
||||||
|
self.assertEqual(new_itv.documents.count(), 1)
|
||||||
|
copied_doc = new_itv.documents.first()
|
||||||
|
self.assertIn('test_dup_doc', copied_doc.file.name)
|
||||||
|
self.assertEqual(copied_doc.description, self.doc.description)
|
||||||
|
|
||||||
|
# Link exists between original and new
|
||||||
|
link_exists = InterventionLink.objects.filter(
|
||||||
|
Q(first=self.itv, second=new_itv) | Q(first=new_itv, second=self.itv)
|
||||||
|
).exists()
|
||||||
|
self.assertTrue(link_exists)
|
||||||
|
|
||||||
|
# Timeline creation event exists
|
||||||
|
timeline_exists = InterventionTimeLine.objects.filter(
|
||||||
|
intervention=new_itv,
|
||||||
|
event_type='creation',
|
||||||
|
).exists()
|
||||||
|
self.assertTrue(timeline_exists)
|
||||||
|
|
||||||
|
def test_duplication_mobile_redirect(self):
|
||||||
|
self.client.force_login(self.admin_user)
|
||||||
|
url = reverse('interventions:duplicate_intervention', kwargs={'intervention_id': self.itv.id})
|
||||||
|
response = self.client.post(url, {'variant': 'mobile'})
|
||||||
|
|
||||||
|
new_itv = Intervention.objects.exclude(id=self.itv.id).first()
|
||||||
|
self.assertIsNotNone(new_itv)
|
||||||
|
self.assertRedirects(response, reverse('mobile:intervention_detail_mobile', kwargs={'intervention_id': new_itv.id}))
|
||||||
|
|
||||||
|
def test_duplication_unauthorized(self):
|
||||||
|
User = get_user_model()
|
||||||
|
unauth_user = User.objects.create_user(username='unauth', password='pwd')
|
||||||
|
UserConfig.objects.create(user=unauth_user, is_intern=False)
|
||||||
|
|
||||||
|
self.client.force_login(unauth_user)
|
||||||
|
url = reverse('interventions:duplicate_intervention', kwargs={'intervention_id': self.itv.id})
|
||||||
|
response = self.client.post(url)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ urlpatterns = [
|
||||||
path('<int:intervention_id>/notes/<int:note_id>/update/', views.intervention_update_note, name='intervention_update_note'),
|
path('<int:intervention_id>/notes/<int:note_id>/update/', views.intervention_update_note, name='intervention_update_note'),
|
||||||
path('<int:intervention_id>/notes/<int:note_id>/delete/', views.intervention_delete_note, name='intervention_delete_note'),
|
path('<int:intervention_id>/notes/<int:note_id>/delete/', views.intervention_delete_note, name='intervention_delete_note'),
|
||||||
path('<int:intervention_id>/create-repair/', views.create_repair_intervention, name='create_repair_intervention'),
|
path('<int:intervention_id>/create-repair/', views.create_repair_intervention, name='create_repair_intervention'),
|
||||||
|
path('<int:intervention_id>/duplicate/', views.duplicate_intervention, name='duplicate_intervention'),
|
||||||
path('<int:intervention_id>/link-existing/', views.link_existing_interventions, name='link_existing_interventions'),
|
path('<int:intervention_id>/link-existing/', views.link_existing_interventions, name='link_existing_interventions'),
|
||||||
path('<int:intervention_id>/occupations/add/', views.intervention_add_occupation, name='intervention_add_occupation'),
|
path('<int:intervention_id>/occupations/add/', views.intervention_add_occupation, name='intervention_add_occupation'),
|
||||||
path('<int:intervention_id>/occupations/<int:occupation_id>/update/', views.intervention_update_occupation, name='intervention_update_occupation'),
|
path('<int:intervention_id>/occupations/<int:occupation_id>/update/', views.intervention_update_occupation, name='intervention_update_occupation'),
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ from interventions.permissions import (intervention_permission_required, interve
|
||||||
can_view_intervention_summary,
|
can_view_intervention_summary,
|
||||||
get_accessible_interventions_for_technician, filter_viewable_interventions_for_user,
|
get_accessible_interventions_for_technician, filter_viewable_interventions_for_user,
|
||||||
can_edit_note, can_delete_document, can_manage_occupations,
|
can_edit_note, can_delete_document, can_manage_occupations,
|
||||||
can_add_intervention, can_add_repair_intervention,
|
can_add_intervention, can_add_repair_intervention, can_duplicate_intervention,
|
||||||
intervention_manage_occupations_required,
|
intervention_manage_occupations_required,
|
||||||
get_contracts_for_intervention_creation, is_external_manager_with_contract_creation_rights,
|
get_contracts_for_intervention_creation, is_external_manager_with_contract_creation_rights,
|
||||||
should_show_created_by_provider_badge, can_view_correction_messages)
|
should_show_created_by_provider_badge, can_view_correction_messages)
|
||||||
|
|
@ -672,6 +672,7 @@ def intervention_detail(request, intervention_id):
|
||||||
'can_manage_links': can_manage_links,
|
'can_manage_links': can_manage_links,
|
||||||
'link_existing_form': LinkExistingInterventionsForm(),
|
'link_existing_form': LinkExistingInterventionsForm(),
|
||||||
'can_create_repair': can_create_repair,
|
'can_create_repair': can_create_repair,
|
||||||
|
'can_duplicate_intervention': can_duplicate_intervention(request.user, intervention),
|
||||||
'can_send_reminder': can_send_reminder,
|
'can_send_reminder': can_send_reminder,
|
||||||
# Historique des interventions aux mêmes localisations
|
# Historique des interventions aux mêmes localisations
|
||||||
'location_history_interventions': location_history_interventions,
|
'location_history_interventions': location_history_interventions,
|
||||||
|
|
@ -966,6 +967,194 @@ def create_repair_intervention(request, intervention_id):
|
||||||
return redirect('interventions:intervention_detail', intervention_id=intervention.id)
|
return redirect('interventions:intervention_detail', intervention_id=intervention.id)
|
||||||
|
|
||||||
|
|
||||||
|
def duplicate_intervention(request, intervention_id):
|
||||||
|
"""
|
||||||
|
Duplique une intervention existante:
|
||||||
|
- Recopie la plupart des éléments (titre, description, localisation, équipement, thématique, etc.).
|
||||||
|
- Recopie les opérations (statut réinitialisé).
|
||||||
|
- Recopie les notes de type 'origine' (note_type='origin').
|
||||||
|
- Recopie les documents.
|
||||||
|
- Ne recopie pas : prestataire, contrat, équipe/membre assigné, dates.
|
||||||
|
- Statut par défaut : in_preparation (En préparation).
|
||||||
|
- Relie automatiquement la nouvelle intervention à la précédente (InterventionLink).
|
||||||
|
"""
|
||||||
|
intervention = get_object_or_404(Intervention, pk=intervention_id)
|
||||||
|
|
||||||
|
if not can_duplicate_intervention(request.user, intervention):
|
||||||
|
return HttpResponseForbidden(_("Vous n'avez pas la permission de dupliquer cette intervention."))
|
||||||
|
|
||||||
|
variant = request.POST.get('variant')
|
||||||
|
if "/mobile/" in request.path:
|
||||||
|
variant = 'mobile'
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
new_intervention = Intervention(
|
||||||
|
title=intervention.title,
|
||||||
|
description=intervention.description,
|
||||||
|
init_description=intervention.init_description,
|
||||||
|
maintain_type=intervention.maintain_type,
|
||||||
|
type=intervention.type,
|
||||||
|
origin_type=intervention.origin_type,
|
||||||
|
priority=intervention.priority,
|
||||||
|
thematic=intervention.thematic,
|
||||||
|
asset_category=intervention.asset_category,
|
||||||
|
symptom=intervention.symptom,
|
||||||
|
source_category=intervention.source_category,
|
||||||
|
source_detail=intervention.source_detail,
|
||||||
|
source_ref=intervention.source_ref,
|
||||||
|
pmo_number=intervention.pmo_number,
|
||||||
|
osiris=intervention.osiris,
|
||||||
|
lon=intervention.lon,
|
||||||
|
lat=intervention.lat,
|
||||||
|
address=intervention.address,
|
||||||
|
location_code=intervention.location_code,
|
||||||
|
geom=intervention.geom,
|
||||||
|
guided_operation_template=intervention.guided_operation_template,
|
||||||
|
status='in_preparation',
|
||||||
|
status_order=STATUS_ORDERS.get('in_preparation', 1),
|
||||||
|
created_by=request.user,
|
||||||
|
last_modified_by=request.user,
|
||||||
|
contract=None,
|
||||||
|
order=None,
|
||||||
|
intervention_manager=None,
|
||||||
|
assigned_provider=None,
|
||||||
|
assigned_team=None,
|
||||||
|
assigned_member=None,
|
||||||
|
begin_time=None,
|
||||||
|
end_time=None,
|
||||||
|
planned_begin_time=None,
|
||||||
|
planned_end_time=None,
|
||||||
|
expected_begin_time=None,
|
||||||
|
expected_end_time=None,
|
||||||
|
transmission_time=None,
|
||||||
|
init_call_confirmed_time=None,
|
||||||
|
init_call_received=False,
|
||||||
|
pause_reason=None,
|
||||||
|
pause_reason_other=None,
|
||||||
|
cancellation_type=None,
|
||||||
|
period_validation_enabled=False,
|
||||||
|
period_number=1,
|
||||||
|
period_is_final=False,
|
||||||
|
processed_via_other_intervention=False,
|
||||||
|
reason_for_correction=None,
|
||||||
|
correction_details=None,
|
||||||
|
follow_up_performed=None,
|
||||||
|
is_checked=False,
|
||||||
|
)
|
||||||
|
new_intervention.save()
|
||||||
|
auto_check_if_eligible(request.user, new_intervention)
|
||||||
|
|
||||||
|
# Copier les templates d'opérations équipements (M2M)
|
||||||
|
templates = list(intervention.equipment_operation_templates.all())
|
||||||
|
if templates:
|
||||||
|
new_intervention.equipment_operation_templates.set(templates)
|
||||||
|
|
||||||
|
# Copier les localisations liées
|
||||||
|
for location in intervention.locations.all():
|
||||||
|
InterventionLocation.objects.get_or_create(
|
||||||
|
intervention=new_intervention,
|
||||||
|
content_type=location.content_type,
|
||||||
|
object_id=location.object_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier les équipements liés
|
||||||
|
for asset in intervention.interventionasset_set.all():
|
||||||
|
InterventionAsset.objects.get_or_create(
|
||||||
|
intervention=new_intervention,
|
||||||
|
content_type=asset.content_type,
|
||||||
|
object_id=asset.object_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier les liaisons aux projets
|
||||||
|
for project_link in ProjectIntervention.objects.filter(intervention=intervention):
|
||||||
|
ProjectIntervention.objects.create(
|
||||||
|
intervention=new_intervention,
|
||||||
|
project=project_link.project,
|
||||||
|
phase=project_link.phase,
|
||||||
|
row_number=project_link.row_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier les opérations (avec statut réinitialisé)
|
||||||
|
for op in intervention.operations.all():
|
||||||
|
Operation.objects.create(
|
||||||
|
intervention=new_intervention,
|
||||||
|
name_fr=op.name_fr,
|
||||||
|
name_nl=op.name_nl,
|
||||||
|
comment='',
|
||||||
|
status_set=op.status_set,
|
||||||
|
status=None,
|
||||||
|
measurement_definition=op.measurement_definition,
|
||||||
|
measured_value=None,
|
||||||
|
measured_duration=None,
|
||||||
|
measured_datetime=None,
|
||||||
|
is_not_applicable=False,
|
||||||
|
order=op.order,
|
||||||
|
template_group=op.template_group,
|
||||||
|
asset_content_type=op.asset_content_type,
|
||||||
|
asset_object_id=op.asset_object_id,
|
||||||
|
is_custom=op.is_custom,
|
||||||
|
is_generic=op.is_generic,
|
||||||
|
requires_photo=op.requires_photo,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier uniquement les notes de type 'origine'
|
||||||
|
origin_notes = intervention.notes.filter(note_type=InterventionNote.NoteType.ORIGIN)
|
||||||
|
for note in origin_notes:
|
||||||
|
InterventionNote.objects.create(
|
||||||
|
intervention=new_intervention,
|
||||||
|
content=note.content,
|
||||||
|
note_time=note.note_time,
|
||||||
|
note_type=note.note_type,
|
||||||
|
note_author=note.note_author or request.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier les documents
|
||||||
|
for doc in intervention.documents.all():
|
||||||
|
if doc.file:
|
||||||
|
try:
|
||||||
|
doc.file.open('rb')
|
||||||
|
InterventionDocument.objects.create(
|
||||||
|
intervention=new_intervention,
|
||||||
|
file=ContentFile(doc.file.read(), name=os.path.basename(doc.file.name)),
|
||||||
|
uploaded_by=doc.uploaded_by or request.user,
|
||||||
|
description=doc.description,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
doc.file.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Relier automatiquement à l'intervention précédente
|
||||||
|
InterventionLink.link(intervention, new_intervention, InterventionLink.LinkType.RELATED)
|
||||||
|
|
||||||
|
# Timeline de création
|
||||||
|
InterventionTimeLine.objects.create(
|
||||||
|
intervention=new_intervention,
|
||||||
|
event_user=request.user,
|
||||||
|
event_time=now(),
|
||||||
|
event_type='creation',
|
||||||
|
from_status=None,
|
||||||
|
to_status=new_intervention.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("L'intervention %(code)s a été dupliquée avec succès à partir de %(origin)s.") % {
|
||||||
|
'code': new_intervention.code,
|
||||||
|
'origin': intervention.code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if variant == 'mobile':
|
||||||
|
return redirect('mobile:intervention_detail_mobile', intervention_id=new_intervention.id)
|
||||||
|
|
||||||
|
return redirect('interventions:intervention_detail', intervention_id=new_intervention.id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def add_repair_intervention_modal(request):
|
def add_repair_intervention_modal(request):
|
||||||
"""
|
"""
|
||||||
Vue AJAX pour créer une intervention de réparation via modale desktop.
|
Vue AJAX pour créer une intervention de réparation via modale desktop.
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ from interventions.permissions import (intervention_permission_required, interve
|
||||||
can_view_intervention_summary,
|
can_view_intervention_summary,
|
||||||
get_accessible_interventions_for_technician, filter_viewable_interventions_for_user,
|
get_accessible_interventions_for_technician, filter_viewable_interventions_for_user,
|
||||||
can_edit_note, can_delete_document, can_manage_occupations,
|
can_edit_note, can_delete_document, can_manage_occupations,
|
||||||
can_add_intervention, can_add_repair_intervention,
|
can_add_intervention, can_add_repair_intervention, can_duplicate_intervention,
|
||||||
intervention_manage_occupations_required,
|
intervention_manage_occupations_required,
|
||||||
get_contracts_for_intervention_creation, is_external_manager_with_contract_creation_rights,
|
get_contracts_for_intervention_creation, is_external_manager_with_contract_creation_rights,
|
||||||
should_show_created_by_provider_badge, can_view_correction_messages)
|
should_show_created_by_provider_badge, can_view_correction_messages)
|
||||||
|
|
@ -1091,6 +1091,7 @@ def intervention_detail_mobile(request, intervention_id):
|
||||||
'pause_reason_choices': PAUSE_REASON_CHOICES,
|
'pause_reason_choices': PAUSE_REASON_CHOICES,
|
||||||
'linked_groups': linked_groups,
|
'linked_groups': linked_groups,
|
||||||
'can_create_repair': can_create_repair,
|
'can_create_repair': can_create_repair,
|
||||||
|
'can_duplicate_intervention': can_duplicate_intervention(request.user, intervention),
|
||||||
'mobile_assets_enabled': can_edit_mobile,
|
'mobile_assets_enabled': can_edit_mobile,
|
||||||
'mobile_assets_config': mobile_assets_config,
|
'mobile_assets_config': mobile_assets_config,
|
||||||
'mobile_asset_location_labels': mobile_asset_location_labels,
|
'mobile_asset_location_labels': mobile_asset_location_labels,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue