From 927c049d02d2c05052ac6a4c1de34e40b414b4ff Mon Sep 17 00:00:00 2001 From: kdeterme Date: Wed, 30 Sep 2026 19:20:59 +0200 Subject: [PATCH] git add loko/contracts/ loko/interventions/ git commit -m "feat(interventions): configure team maintain types for control auto-assignment and remove structures limitation" --- loko/contracts/admin.py | 10 +++- .../contracts/locale/en/LC_MESSAGES/django.mo | Bin 34732 -> 35240 bytes .../contracts/locale/en/LC_MESSAGES/django.po | 8 +++ .../contracts/locale/fr/LC_MESSAGES/django.mo | Bin 41809 -> 42413 bytes .../contracts/locale/fr/LC_MESSAGES/django.po | 8 +++ .../contracts/locale/nl/LC_MESSAGES/django.mo | Bin 36091 -> 36630 bytes .../contracts/locale/nl/LC_MESSAGES/django.po | 8 +++ ...013_companyteam_maintain_types_and_more.py | 38 ++++++++++++ loko/contracts/models.py | 26 ++++++++- loko/interventions/models.py | 6 +- .../services_control_assignment.py | 30 +++++++--- .../tests/test_control_assignment.py | 55 +++++++++++++++++- 12 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 loko/contracts/migrations/0013_companyteam_maintain_types_and_more.py diff --git a/loko/contracts/admin.py b/loko/contracts/admin.py index ae7f56e..ea73720 100644 --- a/loko/contracts/admin.py +++ b/loko/contracts/admin.py @@ -314,23 +314,27 @@ class CompanyTeamAdminForm(forms.ModelForm): class CompanyTeamAdmin(gis_admin.GISModelAdmin): form = CompanyTeamAdminForm list_display = ( - 'name', 'company', 'thematics_list', 'operation_types_list', 'has_geom', + 'name', 'company', 'thematics_list', 'maintain_types_list', 'operation_types_list', 'has_geom', 'members_count', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'visibility_start_hour', 'restrict_to_assigned_interventions' ) search_fields = ('name', 'company__name') list_filter = ( - 'company', 'can_generate_daily_report', 'photos_required', + 'company', 'maintain_types', 'can_generate_daily_report', 'photos_required', 'restrict_today_interventions_visibility', 'restrict_to_assigned_interventions' ) - filter_horizontal = ('thematics', 'asset_categories') + filter_horizontal = ('thematics', 'asset_categories', 'maintain_types') inlines = [CompanyTeamMemberInline] def thematics_list(self, obj): return ", ".join([thematic.name for thematic in obj.thematics.all()]) thematics_list.short_description = 'Thématiques' + def maintain_types_list(self, obj): + return obj.get_maintain_types_display() + maintain_types_list.short_description = _("Types de maintenance") + def operation_types_list(self, obj): return obj.get_operation_types_display() or "—" operation_types_list.short_description = _("Opérations gérées") diff --git a/loko/contracts/locale/en/LC_MESSAGES/django.mo b/loko/contracts/locale/en/LC_MESSAGES/django.mo index a0c431bc4ba35eed84518b12de60935cf699d639..d464408540a90581728fb0f1446593a06c5165a2 100644 GIT binary patch delta 9013 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 delta 8762 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 EA4GFfG5`Po diff --git a/loko/contracts/locale/en/LC_MESSAGES/django.po b/loko/contracts/locale/en/LC_MESSAGES/django.po index 23cf9b3..32e5bae 100644 --- a/loko/contracts/locale/en/LC_MESSAGES/django.po +++ b/loko/contracts/locale/en/LC_MESSAGES/django.po @@ -2375,3 +2375,11 @@ msgstr "If enabled, a control team is automatically assigned to corrective inter msgid "Contrôleur assigné" msgstr "Assigned controller" +#: contracts/models.py:450 +msgid "Si activé, une équipe de contrôle est automatiquement assignée aux interventions de ce contrat." +msgstr "If enabled, a control team is automatically assigned to interventions for this contract." + +#: contracts/models.py:93 +msgid "Types de maintenance pris en charge par cette équipe (corrective, préventive, améliorative). Si aucun n'est sélectionné, tous les types sont pris en charge." +msgstr "Maintenance types handled by this team (corrective, preventive, ameliorative). If none is selected, all types are supported." + diff --git a/loko/contracts/locale/fr/LC_MESSAGES/django.mo b/loko/contracts/locale/fr/LC_MESSAGES/django.mo index fe4478f69f169baa5af57a836ca34b375a8b246e..271a5bae0b4cd2eb28b2f20337cb140fecc8e7f7 100644 GIT binary patch delta 11023 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 delta 8762 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 bool: + """Indique si l'équipe prend en charge le type de maintenance donné. + Si aucun type n'est configuré, l'équipe prend en charge tous les types.""" + if not maintain_type_code: + return True + types = list(self.maintain_types.all()) + if not types: + return True + return any(t.code == maintain_type_code for t in types) + + def get_maintain_types_display(self) -> str: + """Retourne la liste lisible des types de maintenance gérés.""" + types = list(self.maintain_types.all()) + if not types: + return str(_("Tous")) + return ", ".join(str(t) for t in types) + def __str__(self): return f"{self.name} ({self.company.name})" @@ -447,7 +471,7 @@ class Contract(models.Model): auto_assign_control_team = models.BooleanField( default=False, verbose_name=_("Assigner automatiquement une équipe de contrôle"), - help_text=_("Si activé, une équipe de contrôle est automatiquement assignée aux interventions correctives de ce contrat."), + help_text=_("Si activé, une équipe de contrôle est automatiquement assignée aux interventions de ce contrat."), ) is_blocked = models.BooleanField( default=False, diff --git a/loko/interventions/models.py b/loko/interventions/models.py index 61ae7ca..19c7992 100644 --- a/loko/interventions/models.py +++ b/loko/interventions/models.py @@ -658,11 +658,9 @@ class Intervention(models.Model): if user_config and not user_config.is_intern: self.created_by_provider = True - # Attribution automatique de l'équipe de contrôle pour les interventions correctives Structures + # Attribution automatique de l'équipe de contrôle if ( - self.maintain_type == 'corrective' - and self.thematic and getattr(self.thematic, 'code', None) == 'structures' - and self.contract and getattr(self.contract, 'auto_assign_control_team', False) + self.contract and getattr(self.contract, 'auto_assign_control_team', False) and not self.assigned_control_team_id ): try: diff --git a/loko/interventions/services_control_assignment.py b/loko/interventions/services_control_assignment.py index e81392f..0189409 100644 --- a/loko/interventions/services_control_assignment.py +++ b/loko/interventions/services_control_assignment.py @@ -10,16 +10,18 @@ def resolve_control_team_and_member( point=None, scheduled_date=None, thematic=None, + maintain_type=None, locations=None, ): """ Détermine l'équipe de contrôle (CompanyTeam) et le contrôleur (CompanyMember) : 1. Équipes candidates gérant l'opération 'controls' (OPERATION_CONTROLS). 2. Filtrage thématique si l'équipe restreint ses thématiques. - 3. Résolution spatiale si une géométrie est définie (cible et équipes) : + 3. Filtrage par type de maintenance (corrective, preventive, ameliorative) si l'équipe restreint ses types. + 4. Résolution spatiale si une géométrie est définie (cible et équipes) : - team.geom.intersects(target_geom) ou team.geom.contains(point). - Fallback sur toutes les équipes candidates si aucune géométrie n'est définie. - 4. Calendrier d'assignation à la date scheduled_date : + 5. Calendrier d'assignation à la date scheduled_date : - Évalue le nombre d'agents planifiés et non absents. - Si aucun agent n'est disponible ou aucune date : retourne (None, None). - Sélectionne l'équipe ayant le plus d'agents disponibles. @@ -28,20 +30,31 @@ def resolve_control_team_and_member( from contracts.models import CompanyTeam, CompanyMember from contracts.services_teams import get_teams_composition_for_period, get_member_absences_for_period - teams_qs = CompanyTeam.objects.all().select_related('company') + teams_qs = CompanyTeam.objects.all().select_related('company').prefetch_related('thematics', 'maintain_types') candidate_teams = [t for t in teams_qs if t.handles_operation(CompanyTeam.OPERATION_CONTROLS)] if not candidate_teams: return None, None - # 1. Filtrage thématique + # 1. Filtrage thématique si l'équipe restreint ses thématiques if thematic: - filtered_thematic = [ + candidate_teams = [ t for t in candidate_teams - if not t.thematics.exists() or t.thematics.filter(id=thematic.id).exists() + if not t.thematics.all() or any(thm.id == thematic.id for thm in t.thematics.all()) ] - if filtered_thematic: - candidate_teams = filtered_thematic + + if not candidate_teams: + return None, None + + # 2. Filtrage par type de maintenance si l'équipe restreint ses types de maintenance + if maintain_type: + candidate_teams = [ + t for t in candidate_teams + if t.handles_maintain_type(maintain_type) + ] + + if not candidate_teams: + return None, None # 2. Résolution spatiale si géométrie définie has_target_geom = bool(target_geom or point or locations) @@ -150,5 +163,6 @@ def resolve_control_team_and_member_for_intervention(intervention): point=point, scheduled_date=target_date, thematic=intervention.thematic, + maintain_type=intervention.maintain_type, locations=locs, ) diff --git a/loko/interventions/tests/test_control_assignment.py b/loko/interventions/tests/test_control_assignment.py index 6ccab79..d874793 100644 --- a/loko/interventions/tests/test_control_assignment.py +++ b/loko/interventions/tests/test_control_assignment.py @@ -6,7 +6,7 @@ from django.contrib.gis.geos import MultiPolygon, Polygon, Point from common.models import Role, UserConfig, Thematic, UserContractAccess from contracts.models import ( - Company, CompanyTeam, CompanyMember, CompanyMemberHistory, CompanyMemberAbsence, Contract + Company, CompanyTeam, CompanyMember, CompanyMemberHistory, CompanyMemberAbsence, Contract, MaintainType ) from interventions.models import Intervention, InterventionContractPost, ContractPost from interventions.permissions import can_view_intervention, can_control_intervention @@ -30,6 +30,17 @@ class ControlAssignmentTests(TestCase): code='roads', defaults={'name_fr': 'Voirie', 'name_nl': 'Wegen'} ) + # Types de maintenance + self.mt_corrective, _ = MaintainType.objects.get_or_create( + code='corrective', defaults={'name_fr': 'Maintenance corrective', 'name_nl': 'Correctief onderhoud'} + ) + self.mt_preventive, _ = MaintainType.objects.get_or_create( + code='preventive', defaults={'name_fr': 'Maintenance préventive', 'name_nl': 'Preventief onderhoud'} + ) + self.mt_ameliorative, _ = MaintainType.objects.get_or_create( + code='ameliorative', defaults={'name_fr': 'Maintenance améliorative', 'name_nl': 'Verbeterend onderhoud'} + ) + # Société & Prestataire self.company = Company.objects.create(name='Prestataire Structures') self.org_intern = Company.objects.create(name='Organisme Interne Contrôle') @@ -51,6 +62,8 @@ class ControlAssignmentTests(TestCase): operation_types=[CompanyTeam.OPERATION_CONTROLS], geom=self.ctrl_geom ) + self.team_ctrl.thematics.add(self.thematic_structures) + self.team_ctrl.maintain_types.add(self.mt_corrective) # Membre contrôleur self.user_ctrl = self.User.objects.create_user(username='ctrl_agent', password='pwd') @@ -108,7 +121,7 @@ class ControlAssignmentTests(TestCase): self.assertEqual(itv.assigned_controller, self.member_ctrl) def test_no_auto_assignment_if_not_corrective(self): - """Une intervention préventive ne doit pas recevoir d'attribution automatique de contrôle.""" + """Une intervention préventive ne doit pas être assignée si l'équipe ne gère que le correctif.""" itv = Intervention.objects.create( maintain_type='preventive', thematic=self.thematic_structures, @@ -118,8 +131,32 @@ class ControlAssignmentTests(TestCase): self.assertIsNone(itv.assigned_control_team) self.assertIsNone(itv.assigned_controller) + def test_auto_assignment_preventive_when_team_handles_preventive(self): + """Une intervention préventive est assignée dès lors que l'équipe prend en charge le préventif.""" + self.team_ctrl.maintain_types.add(self.mt_preventive) + itv = Intervention.objects.create( + maintain_type='preventive', + thematic=self.thematic_structures, + contract=self.contract_auto, + planned_begin_time=timezone.now(), + ) + self.assertEqual(itv.assigned_control_team, self.team_ctrl) + self.assertEqual(itv.assigned_controller, self.member_ctrl) + + def test_auto_assignment_when_team_has_no_maintain_types_restriction(self): + """Une équipe sans restriction de types de maintenance prend en charge tous les types (y compris amélioratif).""" + self.team_ctrl.maintain_types.clear() + itv = Intervention.objects.create( + maintain_type='ameliorative', + thematic=self.thematic_structures, + contract=self.contract_auto, + planned_begin_time=timezone.now(), + ) + self.assertEqual(itv.assigned_control_team, self.team_ctrl) + self.assertEqual(itv.assigned_controller, self.member_ctrl) + def test_no_auto_assignment_if_not_structures(self): - """Une intervention d'une autre thématique ne doit pas recevoir d'attribution automatique de contrôle.""" + """Une intervention d'une autre thématique ne doit pas être assignée à une équipe restreinte à structures.""" itv = Intervention.objects.create( maintain_type='corrective', thematic=self.thematic_roads, @@ -129,6 +166,18 @@ class ControlAssignmentTests(TestCase): self.assertIsNone(itv.assigned_control_team) self.assertIsNone(itv.assigned_controller) + def test_auto_assignment_other_thematic_when_team_matches(self): + """Une intervention d'une autre thématique (ex: voirie) est assignée si une équipe de contrôle correspondante existe.""" + self.team_ctrl.thematics.add(self.thematic_roads) + itv = Intervention.objects.create( + maintain_type='corrective', + thematic=self.thematic_roads, + contract=self.contract_auto, + planned_begin_time=timezone.now(), + ) + self.assertEqual(itv.assigned_control_team, self.team_ctrl) + self.assertEqual(itv.assigned_controller, self.member_ctrl) + def test_no_auto_assignment_if_contract_not_enabled(self): """Un contrat sans auto_assign_control_team ne doit pas déclencher l'attribution.""" itv = Intervention.objects.create(