gmao/app_new/core/services/maintenance_engine.py

129 lines
5.7 KiB
Python
Raw Normal View History

"""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