diff --git a/loko/interventions/permissions.py b/loko/interventions/permissions.py index b390206..eed1708 100644 --- a/loko/interventions/permissions.py +++ b/loko/interventions/permissions.py @@ -1023,11 +1023,36 @@ def can_edit_intervention(user, intervention, user_config=None): thematic=thematic, ).first() if ut: - return UserThematicStatusPermission.objects.filter( + if UserThematicStatusPermission.objects.filter( user_thematic=ut, status=status, can_edit=True, - ).exists() + ).exists(): + return True + # Si pas de contrat et pas de permission de statut thématique explicite : + if contract is None: + # 1. Le créateur de l'intervention peut toujours la modifier s'il n'y a pas de contrat + if intervention.created_by_id == user.id: + return True + # 2. Utilisateur ayant des droits d'édition sur au moins un contrat de la thématique + if UserContractStatusPermission.objects.filter( + user_contract__user_config=user_config, + user_contract__contract__thematics=thematic, + status=status, + can_edit=True, + ).exists(): + return True + # 3. Utilisateur ayant des droits d'édition sur au moins un contrat du prestataire assigné + if intervention.assigned_provider_id and UserContractStatusPermission.objects.filter( + user_contract__user_config=user_config, + user_contract__contract__company_id=intervention.assigned_provider_id, + status=status, + can_edit=True, + ).exists(): + return True + # 4. Si l'utilisateur a can_edit_interventions sur la thématique pour les statuts initiaux + if ut.can_edit_interventions and status in ('in_preparation', 'to_be_approved', 'to_be_planned'): + return True return False diff --git a/loko/interventions/static/interventions/detail/layout.js b/loko/interventions/static/interventions/detail/layout.js index 4bee3c3..8677267 100644 --- a/loko/interventions/static/interventions/detail/layout.js +++ b/loko/interventions/static/interventions/detail/layout.js @@ -144,19 +144,20 @@ } } - async function updateField(interventionId, field, value) { + async function updateFieldsBatch(interventionId, payload) { const response = await fetch(`/interventions/${interventionId}/update/`, { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') }, - body: new URLSearchParams({ field, value }) + body: JSON.stringify(payload) }); const data = await response.json(); if (!response.ok || !data.success) { - throw new Error(data.error || gettext('Une erreur est survenue.')); + const errMsg = data.error || (data.errors && data.errors.join(', ')) || gettext('Une erreur est survenue.'); + throw new Error(errMsg); } return data; } @@ -227,11 +228,16 @@ if (spinnerEl) spinnerEl.classList.remove('d-none'); try { + const payload = {}; if (providerChanged) { - await updateField(interventionId, 'assigned_provider', providerValue); + payload.assigned_provider = providerValue; } if (contractChanged) { - await updateField(interventionId, 'contract', contractValue); + payload.contract = contractValue; + } + + if (Object.keys(payload).length > 0) { + await updateFieldsBatch(interventionId, payload); } if (providerDisplay && providerSelect) { diff --git a/loko/interventions/tests/test_visibility_permissions.py b/loko/interventions/tests/test_visibility_permissions.py index d3eae01..d9d4f69 100644 --- a/loko/interventions/tests/test_visibility_permissions.py +++ b/loko/interventions/tests/test_visibility_permissions.py @@ -600,6 +600,94 @@ class GetContractsByProviderTests(TestCase): self.assertIn("C-002", contract_numbers) +class QuickProviderContractUpdateTests(TestCase): + def setUp(self): + from common.models import UserContractAccess, UserContractStatusPermission + self.User = get_user_model() + self.manager_role, _ = Role.objects.get_or_create(name='manager') + self.thematic = Thematic.objects.create(name_fr='Voirie Test', allow_planning_without_contract=True) + + self.company1 = Company.objects.create(name='Company 1') + self.company2 = Company.objects.create(name='Company 2') + + self.contract1 = Contract.objects.create( + contract_number="C-001", + company=self.company1, + start_date=timezone.now().date(), + end_date=timezone.now().date() + timedelta(days=365) + ) + self.contract1.thematics.add(self.thematic) + + self.contract2 = Contract.objects.create( + contract_number="C-002", + company=self.company2, + start_date=timezone.now().date(), + end_date=timezone.now().date() + timedelta(days=365) + ) + self.contract2.thematics.add(self.thematic) + + # User with limited contract access + self.user = self.User.objects.create_user(username='contract-manager', password='pwd') + self.user_config = UserConfig.objects.create( + user=self.user, + is_intern=True, + limit_interventions_to_contracts=True, + ) + self.user_config.roles.add(self.manager_role) + + # User thematic without explicit UserThematicStatusPermission + UserThematics.objects.create( + user_config=self.user_config, + thematic=self.thematic, + can_view_interventions=True, + can_edit_interventions=True, + ) + + # Access and edit permissions on both contracts for to_be_planned status + uca1 = UserContractAccess.objects.create(user_config=self.user_config, contract=self.contract1, can_view_interventions=True) + UserContractStatusPermission.objects.create(user_contract=uca1, status='to_be_planned', can_edit=True, can_view=True) + + uca2 = UserContractAccess.objects.create(user_config=self.user_config, contract=self.contract2, can_view_interventions=True) + UserContractStatusPermission.objects.create(user_contract=uca2, status='to_be_planned', can_edit=True, can_view=True) + + self.intervention = Intervention.objects.create( + title='Test Quick Edit Intervention', + thematic=self.thematic, + status='to_be_planned', + assigned_provider=self.company1, + contract=self.contract1, + ) + + def test_batch_update_provider_and_contract(self): + from interventions.permissions import can_edit_intervention + self.assertTrue(can_edit_intervention(self.user, self.intervention)) + + self.client.login(username='contract-manager', password='pwd') + url = reverse('interventions:update_intervention', args=[self.intervention.id]) + payload = { + 'assigned_provider': str(self.company2.id), + 'contract': str(self.contract2.id), + } + response = self.client.post(url, data=json.dumps(payload), content_type='application/json') + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertTrue(data['success']) + + self.intervention.refresh_from_db() + self.assertEqual(self.intervention.assigned_provider, self.company2) + self.assertEqual(self.intervention.contract, self.contract2) + + def test_can_edit_intervention_when_contract_is_none(self): + from interventions.permissions import can_edit_intervention + self.intervention.contract = None + self.intervention.assigned_provider = self.company2 + self.intervention.save() + + # User should still be allowed to edit because they have edit rights on contracts of this thematic/provider + self.assertTrue(can_edit_intervention(self.user, self.intervention)) + + + diff --git a/loko/interventions/views/helpers.py b/loko/interventions/views/helpers.py index 0ac8ed2..618cd9d 100644 --- a/loko/interventions/views/helpers.py +++ b/loko/interventions/views/helpers.py @@ -2495,6 +2495,8 @@ def _update_intervention_field(intervention, field, new_value, user_config=None) if not has_access: return False, _('You do not have access to this contract') intervention.contract = contract + if contract.company and intervention.assigned_provider != contract.company: + intervention.assigned_provider = contract.company return True, None return False, _('Contract not found')