From 8c4556a34c37ca8b000f0bb3a3c858f38d8d84c7 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Tue, 11 Aug 2026 11:09:28 +0200 Subject: [PATCH] feat: prevent automatic intervention re-creation in Observation model using ref check --- streetup/observations/models.py | 24 ++++++++++- streetup/observations/tests.py | 70 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/streetup/observations/models.py b/streetup/observations/models.py index 2950b67..b00eca0 100644 --- a/streetup/observations/models.py +++ b/streetup/observations/models.py @@ -438,6 +438,28 @@ class Observation(models.Model): intervention = models.ForeignKey('interventions.Intervention', on_delete=models.SET_NULL, null=True, blank=True, related_name='observations') project = models.ForeignKey('projects.Project', on_delete=models.SET_NULL, null=True, blank=True, related_name='observations', verbose_name=_("Projet lié")) + def has_existing_intervention(self): + """ + Vérifie si une intervention est déjà liée à cette observation, + ou si une intervention a déjà été créée à partir de cette observation (ex. via source_ref). + Permet d'éviter de ré-instancier automatiquement une intervention en boucle + lorsqu'une intervention existante est annulée et que l'observation repasse au statut 'À traiter'. + """ + if self.intervention_id is not None: + return True + if not self.pk: + return False + + from interventions.models import Intervention + possible_refs = [str(self.pk)] + if self.code: + possible_refs.append(self.code) + possible_refs.append(f"OBS{self.pk:05d}") + + return Intervention.objects.filter( + models.Q(observations=self) | models.Q(source_ref__in=possible_refs) + ).exists() + def save(self, *args, **kwargs): is_new = self.pk is None old_status = None @@ -459,7 +481,7 @@ class Observation(models.Model): if (self.symptom_id and self.symptom.auto_create_intervention and self.status == 'to_process' and - not self.intervention_id): + not self.has_existing_intervention()): status_changed_to_to_process = (old_status != 'to_process' and self.status == 'to_process') symptom_changed_to_auto = (old_symptom_id != self.symptom_id and self.symptom.auto_create_intervention) diff --git a/streetup/observations/tests.py b/streetup/observations/tests.py index 9c77fff..95c8300 100644 --- a/streetup/observations/tests.py +++ b/streetup/observations/tests.py @@ -502,3 +502,73 @@ class ObservationPermissionTests(TestCase): self.assertContains(response, "Une intervention liée à cette observation a été annulée") self.assertContains(response, "INT-TEST-CANCEL") + def test_no_auto_intervention_loop_on_cancellation(self): + from contracts.models import Contract, Company + from interventions.models import Symptom, Intervention + from interventions.views.workflow import attempt_status_update + from django.utils import timezone + + company = Company.objects.create(name='Provider Co Loop') + contract = Contract.objects.create( + contract_number='C-LOOP', + company=company, + start_date=timezone.now().date(), + end_date=timezone.now().date() + timezone.timedelta(days=365) + ) + + symptom = Symptom.objects.create( + name_fr='Symptome Auto Loop Test', + name_nl='Symptoom Auto Loop Test', + thematic=self.thematic_a, + auto_create_intervention=True, + contract=contract, + provider=company, + ) + + # 1. Create observation in 'to_process' -> auto-creates intervention + obs = Observation.objects.create( + description='Loop test observation', + latitude=50.5, + longitude=4.5, + created_by=self.internal_user, + thematic=self.thematic_a, + symptom=symptom, + status='to_process', + ) + + obs.refresh_from_db() + self.assertIsNotNone(obs.intervention) + self.assertEqual(obs.status, 'in_progress') + first_intervention = obs.intervention + + # 2. Cancel the intervention + success, msg, _ = attempt_status_update( + request=None, + intervention=first_intervention, + user=self.admin_user, + new_status='canceled', + cancellation_type='other', + cancellation_reason='Test cancellation reason', + ) + self.assertTrue(success, msg) + + # 3. Observation status is reset to 'to_process' + obs.refresh_from_db() + self.assertEqual(obs.status, 'to_process') + + # 4. Save observation again (or unlink and save) - MUST NOT create a new intervention + obs.save() + obs.refresh_from_db() + self.assertEqual(obs.status, 'to_process') + + # Unlink intervention and save observation again + obs.intervention = None + obs.save() + obs.refresh_from_db() + + # Total interventions with source_ref equal to obs.code or obs.pk must still be 1 + possible_refs = [str(obs.pk), obs.code, f"OBS{obs.pk:05d}"] + total_interventions = Intervention.objects.filter(source_ref__in=possible_refs).count() + self.assertEqual(total_interventions, 1) + self.assertEqual(obs.status, 'to_process') +