From 5bfde04f1a19409d742e05f54c8fe01b1628cabc Mon Sep 17 00:00:00 2001 From: kdeterme Date: Wed, 19 Aug 2026 16:02:02 +0200 Subject: [PATCH] feat: allow intervention association with contracts via orders and improve order selection UI in detail view --- .../detail/intervention_posts_editor.js | 13 +++++- .../interventions/detail/card_posts.html | 4 +- .../tests/test_preventive_templates.py | 45 +++++++++++++++++++ loko/interventions/views/detail.py | 26 ++++++++--- 4 files changed, 79 insertions(+), 9 deletions(-) diff --git a/loko/interventions/static/interventions/detail/intervention_posts_editor.js b/loko/interventions/static/interventions/detail/intervention_posts_editor.js index 007a008..c63b852 100644 --- a/loko/interventions/static/interventions/detail/intervention_posts_editor.js +++ b/loko/interventions/static/interventions/detail/intervention_posts_editor.js @@ -2654,6 +2654,8 @@ if (!orderSelect) return; + const initialVal = orderSelect.getAttribute('data-initial-order-id') || $(orderSelect).val() || orderSelect.value || ''; + // Initialiser Select2 avec recherche $(orderSelect).select2({ placeholder: gettext('-- Sélectionnez une commande --'), @@ -2670,6 +2672,10 @@ dropdownParent: $(CONFIG.selectors.modal) }); + if (initialVal) { + $(orderSelect).val(initialVal).trigger('change.select2'); + } + // Gérer le changement de commande $(orderSelect).on('change', async function() { const orderId = $(this).val(); @@ -2933,6 +2939,7 @@ fixTableColumnWidths(CONFIG.selectors.table); const orderSelect = document.getElementById('orderSelect'); if (orderSelect && $.fn.select2) { + const currentVal = $(orderSelect).val() || orderSelect.getAttribute('data-initial-order-id') || orderSelect.value || ''; $(orderSelect).select2('destroy'); $(orderSelect).select2({ placeholder: gettext('-- Sélectionnez une commande --'), @@ -2950,7 +2957,11 @@ }); // S'assurer que la valeur sélectionnée est correcte - $(orderSelect).trigger('change.select2'); + if (currentVal) { + $(orderSelect).val(currentVal).trigger('change.select2'); + } else { + $(orderSelect).trigger('change.select2'); + } } // Initialiser la visibilité du bouton d'ajout du devis diff --git a/loko/interventions/templates/interventions/detail/card_posts.html b/loko/interventions/templates/interventions/detail/card_posts.html index 0dd4819..6c0b0d3 100644 --- a/loko/interventions/templates/interventions/detail/card_posts.html +++ b/loko/interventions/templates/interventions/detail/card_posts.html @@ -355,7 +355,7 @@
- {% for order in orders %} {% endfor %} diff --git a/loko/interventions/tests/test_preventive_templates.py b/loko/interventions/tests/test_preventive_templates.py index 82aa75d..6fbbe1f 100644 --- a/loko/interventions/tests/test_preventive_templates.py +++ b/loko/interventions/tests/test_preventive_templates.py @@ -907,6 +907,11 @@ class ContractOperationTemplatePostTests(TestCase): ) self.contract.thematics.add(self.thematic) self.user_config.contracts.add(self.contract) + uca = UserContractAccess.objects.get(user_config=self.user_config, contract=self.contract) + uca.can_edit_interventions = True + uca.can_view_posts = True + uca.can_view_interventions = True + uca.save() UserThematics.objects.create(user_config=self.user_config, thematic=self.thematic, can_edit_interventions=True) self.contract_other = Contract.objects.create( @@ -1162,6 +1167,46 @@ class ContractOperationTemplatePostTests(TestCase): self.assertEqual(posts[0].quantity, Decimal('4.00')) self.assertEqual(posts[0].comment, 'Auto Post') + def test_detail_view_includes_associated_order_and_posts_modal(self): + """Test that the detail view context includes orders and correctly preselects the associated order.""" + admin_role, _ = Role.objects.get_or_create(name='admin') + self.user_config.roles.add(admin_role) + from contracts.models import ContractOrder + order = ContractOrder.objects.create( + contract=self.contract, + order_code='CMD-2026-001', + order_date=timezone.now().date(), + delivery_date=timezone.now().date() + timedelta(days=30), + order_status='pending', + total_amount=Decimal('500.00'), + ) + intervention = Intervention.objects.create( + title='Intervention With Order', + contract=self.contract, + order=order, + thematic=self.thematic, + status='in_preparation', + created_by=self.user, + ) + + self.client.login(username='test_user_posts', password='password123') + detail_url = reverse('interventions:intervention_detail', kwargs={'intervention_id': intervention.id}) + response = self.client.get(detail_url) + self.assertEqual(response.status_code, 200) + + # Check context + self.assertIn('orders', response.context) + orders = response.context['orders'] + self.assertIsNotNone(orders) + self.assertIn(order, orders) + self.assertEqual(response.context['contract_id'], self.contract.id) + + # Check rendered HTML for modal selector + content = response.content.decode('utf-8') + self.assertIn(f'data-initial-order-id="{order.id}"', content) + self.assertIn(f'value="{order.id}"', content) + self.assertIn('selected', content) + diff --git a/loko/interventions/views/detail.py b/loko/interventions/views/detail.py index df5f051..956a664 100644 --- a/loko/interventions/views/detail.py +++ b/loko/interventions/views/detail.py @@ -149,7 +149,7 @@ def intervention_detail(request, intervention_id): lang = get_language() - contract = intervention.contract + contract = intervention.contract or (intervention.order.contract if intervention.order else None) thematic = intervention.thematic allowed_update_fields = get_allowed_update_fields(request.user, intervention) @@ -223,11 +223,21 @@ def intervention_detail(request, intervention_id): else: has_coefficient_posts = False - orders = ContractOrder.objects.filter(contract=contract).exclude(order_status__in=['cancelled', 'paid']) + orders_qs = ContractOrder.objects.filter(contract=contract) + if intervention.order_id: + orders = orders_qs.filter( + Q(id=intervention.order_id) | ~Q(order_status__in=['cancelled', 'paid']) + ).distinct().select_related('contract').order_by('order_code') + else: + orders = orders_qs.exclude(order_status__in=['cancelled', 'paid']).select_related('contract').order_by('order_code') + elif intervention.order_id: + has_coefficient_posts = False + orders = ContractOrder.objects.filter(id=intervention.order_id).select_related('contract') else: has_coefficient_posts = False + orders = ContractOrder.objects.none() - contract_id = intervention.order.contract.id if intervention.order else None + contract_id = contract.id if contract else None # Limiter les providers et contracts selon les permissions de l'utilisateur if user_config.is_intern and not user_config.limit_interventions_to_contracts: @@ -2188,10 +2198,12 @@ def intervention_link_order(request, intervention_id): intervention = Intervention.objects.get(pk=intervention_id) allowed_update_fields = get_allowed_update_fields(request.user, intervention) - if data['order_id'] == '': + if not data.get('order_id'): intervention.order = None else: intervention.order = ContractOrder.objects.get(pk=data['order_id']) + if intervention.order and not intervention.contract: + intervention.contract = intervention.order.contract intervention.save() quote_items = [] @@ -2227,13 +2239,15 @@ def intervention_link_order(request, intervention_id): quote_total = format_decimal(intervention.order.get_quote_total_amount()) + active_contract = intervention.contract or (intervention.order.contract if intervention.order else None) return JsonResponse({ 'success': True, - 'contract_id': intervention.order.contract.id if intervention.order else None, - 'contract_posts_count': intervention.order.contract.posts.count() if intervention.order and intervention.order.contract else 0, + 'contract_id': active_contract.id if active_contract else None, + 'contract_posts_count': active_contract.posts.count() if active_contract else 0, 'quote_items': quote_items, 'quote_total': quote_total, 'can_manage_posts': 'posts' in allowed_update_fields, + 'order_display': f"{intervention.order.contract.contract_number} - {intervention.order.order_code}" if intervention.order else None, }) except (Intervention.DoesNotExist, ContractOrder.DoesNotExist) as e: return JsonResponse({'success': False, 'message': str(e)})