"""Moteur unique de génération des échéances depuis les tâches de lot.""" from datetime import date, timedelta from app_new.extensions import db from app_new.core.models.planning import Meter, ScheduledTask from app_new.core.models.maintenance import Intervention, LotTask from app_new.core.services.planning_service import PlanningService def _targets(lot): result = [] for root in lot.equipments.filter_by(is_deleted=False): if root.tracked_individually: result.extend([x for x in root.all_children if not x.is_group and not x.is_deleted]) elif root.is_group and root.children.count(): result.extend([x for x in root.children if not x.is_deleted]) else: result.append(root) return list({item.id: item for item in result}.values()) def _working_day(candidate): for _ in range(370): if PlanningService.get_working_hours(candidate): return candidate candidate += timedelta(days=1) raise ValueError("Aucune journée travaillée disponible dans les 12 prochains mois.") def _last_completion(task, equipment): dates = [] scheduled = ScheduledTask.query.filter( ScheduledTask.lot_task_id == task.id, ScheduledTask.equipment_id == equipment.id, ScheduledTask.status.in_(['completed', 'done']), ).order_by(ScheduledTask.completed_at.desc(), ScheduledTask.scheduled_date.desc()).first() if scheduled: if scheduled.completed_at: dates.append(scheduled.completed_at.date()) elif scheduled.scheduled_date: dates.append(scheduled.scheduled_date) intervention = Intervention.query.filter( Intervention.lot_task_id == task.id, Intervention.equipment_id == equipment.id, Intervention.status.in_(['terminee', 'cloturee']), Intervention.is_deleted.is_(False), ).order_by(Intervention.completed_at.desc(), Intervention.completed_date.desc()).first() if intervention: if intervention.completed_at: dates.append(intervention.completed_at.date()) elif intervention.completed_date: dates.append(intervention.completed_date) elif intervention.scheduled_date: dates.append(intervention.scheduled_date) return max(dates) if dates else None def _calendar_due(task, equipment, today): last = _last_completion(task, equipment) interval = task.jours_entre_interventions or 0 return last + timedelta(days=interval) if last and interval else today def generate_due_tasks(task: LotTask, event=None, today=None, equipment_ids=None, commit=True): """Génère les échéances sans doublon et retourne les objets créés.""" today = today or date.today() if not task.is_active: return [] if task.trigger_type in ("event", "weather") and task.trigger_event != event: return [] if task.trigger_type == "season": start, end = task.season_start_month or 1, task.season_end_month or 12 inside = start <= today.month <= end if start <= end else today.month >= start or today.month <= end if not inside: return [] created = [] for equipment in _targets(task.lot): if equipment_ids is not None and equipment.id not in set(equipment_ids): continue if task.trigger_type == "meter": meter = Meter.query.filter_by(equipment_id=equipment.id, is_active=True).order_by(Meter.current_value.desc()).first() base_value = meter.last_maintenance_value if meter else 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']), ).first() if existing: continue due = _calendar_due(task, equipment, today) if task.trigger_type == 'calendar' 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': completed_this_season = ScheduledTask.query.filter( ScheduledTask.lot_task_id == task.id, ScheduledTask.equipment_id == equipment.id, ScheduledTask.status.in_(['completed', 'done']), db.extract('year', ScheduledTask.scheduled_date) == today.year, ).first() if completed_this_season: continue due = _working_day(max(due, today)) item = ScheduledTask(lot_task_id=task.id, equipment_id=equipment.id, room_id=equipment.effective_room.id if equipment.effective_room else None, scheduled_date=due, estimated_duration=task.effective_duration(), status="planned") db.session.add(item) created.append(item) if commit: db.session.commit() elif created: db.session.flush() return created def reconcile_planned_tasks(): """Déplace les échéances ouvertes placées sur un jour non travaillé.""" changed = [] tasks = ScheduledTask.query.filter(ScheduledTask.status.in_(['planned', 'in_progress'])).all() for item in tasks: if item.scheduled_date and not PlanningService.get_working_hours(item.scheduled_date, user_id=item.assigned_to_id): item.scheduled_date = _working_day(item.scheduled_date + timedelta(days=1)) changed.append(item) if changed: db.session.commit() return changed