128 lines
5.7 KiB
Python
128 lines
5.7 KiB
Python
"""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
|