fix(planning): regrouper les requetes de generation preventive
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run

This commit is contained in:
root 2026-08-23 15:24:29 +00:00
parent 5c0a442693
commit 38c4613474
3 changed files with 37 additions and 9 deletions

View file

@ -88,6 +88,30 @@ def _last_completion(task, equipment):
return max(dates) if dates else None 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): def _calendar_due(task, equipment, today):
last = _last_completion(task, equipment) last = _last_completion(task, equipment)
interval = task.jours_entre_interventions or 0 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: if not task.lot or not task.lot.is_present:
return [] return []
requested_ids = set(equipment_ids) if equipment_ids is not None else None 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): for equipment in _task_targets(task, requested_ids):
if equipment_ids is not None and equipment.id not in set(equipment_ids): if equipment_ids is not None and equipment.id not in set(equipment_ids):
continue 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 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: if not meter or task.meter_threshold is None or used_since_maintenance < task.meter_threshold:
continue continue
existing = ScheduledTask.query.filter( if equipment.id in existing_by_equipment:
ScheduledTask.lot_task_id == task.id,
ScheduledTask.equipment_id == equipment.id,
ScheduledTask.status.in_(['planned', 'in_progress', 'suspended', 'needs_duration']),
).first()
if existing:
continue 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)): if task.trigger_type == 'calendar' and due > today + timedelta(days=max(task.advance_days or 0, 0)):
continue continue
if task.trigger_type == 'season': if task.trigger_type == 'season':

View file

@ -9,7 +9,7 @@
## Clôture technique 2.1b ## 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. 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.

View file

@ -3,7 +3,7 @@ from app_new.version import get_version
def test_project_version_is_available(monkeypatch): def test_project_version_is_available(monkeypatch):
monkeypatch.delenv('GMAO_VERSION', raising=False) 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): def test_environment_version_takes_precedence(monkeypatch):