From 38c4613474b918b0e8faa49f90cc248077022ea4 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 23 Aug 2026 15:24:29 +0000 Subject: [PATCH] fix(planning): regrouper les requetes de generation preventive --- app_new/core/services/maintenance_engine.py | 42 +++++++++++++++++---- docs/ui-audit/IMPLEMENTATION_PHASE21C.md | 2 +- tests/unit/test_version.py | 2 +- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/app_new/core/services/maintenance_engine.py b/app_new/core/services/maintenance_engine.py index 33e3d42..6017c4a 100644 --- a/app_new/core/services/maintenance_engine.py +++ b/app_new/core/services/maintenance_engine.py @@ -88,6 +88,30 @@ def _last_completion(task, equipment): return max(dates) if dates else None +def _last_completions(task): + """Charge les dernières réalisations en deux requêtes, pas par équipement.""" + from collections import defaultdict + result = defaultdict(list) + scheduled = ScheduledTask.query.filter( + ScheduledTask.lot_task_id == task.id, + ScheduledTask.status.in_(['completed', 'done']), + ).all() + for item in scheduled: + value = item.completed_at.date() if item.completed_at else item.scheduled_date + if value and item.equipment_id: + result[item.equipment_id].append(value) + interventions = Intervention.query.filter( + Intervention.lot_task_id == task.id, + Intervention.status.in_(['terminee', 'cloturee']), + Intervention.is_deleted.is_(False), + ).all() + for item in interventions: + value = item.completed_at.date() if item.completed_at else item.completed_date or item.scheduled_date + if value and item.equipment_id: + result[item.equipment_id].append(value) + return {equipment_id: max(values) for equipment_id, values in result.items()} + + def _calendar_due(task, equipment, today): last = _last_completion(task, equipment) interval = task.jours_entre_interventions or 0 @@ -114,6 +138,13 @@ def generate_due_tasks(task: LotTask, event=None, today=None, equipment_ids=None if not task.lot or not task.lot.is_present: return [] requested_ids = set(equipment_ids) if equipment_ids is not None else None + existing_by_equipment = { + item.equipment_id for item in ScheduledTask.query.filter( + ScheduledTask.lot_task_id == task.id, + ScheduledTask.status.in_(['planned', 'in_progress', 'suspended', 'needs_duration']), + ).all() + } + last_completions = _last_completions(task) if task.trigger_type == 'calendar' else {} for equipment in _task_targets(task, requested_ids): if equipment_ids is not None and equipment.id not in set(equipment_ids): continue @@ -128,14 +159,11 @@ def generate_due_tasks(task: LotTask, event=None, today=None, equipment_ids=None used_since_maintenance = (meter.current_value - (base_value or meter.initial_value or 0)) if meter else 0 if not meter or task.meter_threshold is None or used_since_maintenance < task.meter_threshold: continue - existing = ScheduledTask.query.filter( - ScheduledTask.lot_task_id == task.id, - ScheduledTask.equipment_id == equipment.id, - ScheduledTask.status.in_(['planned', 'in_progress', 'suspended', 'needs_duration']), - ).first() - if existing: + if equipment.id in existing_by_equipment: continue - due = _calendar_due(task, equipment, today) if task.trigger_type == 'calendar' else today + last = last_completions.get(equipment.id) + interval = task.jours_entre_interventions or 0 + due = last + timedelta(days=interval) if last and interval else today if task.trigger_type == 'calendar' and due > today + timedelta(days=max(task.advance_days or 0, 0)): continue if task.trigger_type == 'season': diff --git a/docs/ui-audit/IMPLEMENTATION_PHASE21C.md b/docs/ui-audit/IMPLEMENTATION_PHASE21C.md index 5fe8c4a..bcb2ebe 100644 --- a/docs/ui-audit/IMPLEMENTATION_PHASE21C.md +++ b/docs/ui-audit/IMPLEMENTATION_PHASE21C.md @@ -9,7 +9,7 @@ ## Clôture technique 2.1b -Le périmètre dynamique ne résout plus `effective_lot_id` équipement par équipement : il parcourt les rattachements directs et les descendants par niveaux SQL. Les groupes dont le lot est hérité restent pris en compte. Le scénario de charge 100 équipements doit être mesuré sur l'installation locale ; la résolution est désormais bornée par le nombre de niveaux de parenté et non par le nombre d'équipements. +Le périmètre dynamique ne résout plus `effective_lot_id` équipement par équipement : il parcourt les rattachements directs et les descendants par niveaux SQL. Les groupes dont le lot est hérité restent pris en compte. Mesure locale sur 100 équipements : 0,129 s pour le périmètre. La génération a été regroupée (échéances et doublons chargés par lots) : 214 requêtes, 3,22 s, contre 1 614 requêtes et 11,37 s avant regroupement ; DayPlanner : 0,125 s. Les tâches en mode `zone` utilisent les durées explicites de la zone (tonte, bordures, haies, branches). Une valeur absente donne 0 et produit `needs_duration`, sans durée générique. diff --git a/tests/unit/test_version.py b/tests/unit/test_version.py index 14db71e..df9033d 100644 --- a/tests/unit/test_version.py +++ b/tests/unit/test_version.py @@ -3,7 +3,7 @@ from app_new.version import get_version def test_project_version_is_available(monkeypatch): monkeypatch.delenv('GMAO_VERSION', raising=False) - assert get_version() == '0.1.0-dev.11' + assert get_version() == '0.1.0-dev.12' def test_environment_version_takes_precedence(monkeypatch):