gmao/app_new/core/services/equipment_lifecycle.py

211 lines
8.3 KiB
Python
Raw Normal View History

"""Règles métier du cycle de vie des équipements et de leurs échéances."""
from datetime import date, datetime, timedelta, timezone
from app_new.extensions import db
from app_new.core.models.equipment import Equipment, EquipmentLifecycleEvent
from app_new.core.models.maintenance import Intervention
from app_new.core.models.planning import ScheduledTask
OPEN_TASK_STATUSES = ("planned", "in_progress", "suspended")
def record_event(equipment, event_type, user_id=None, previous_status=None,
new_status=None, quantity=1, reason=None, intervention_id=None):
"""Enregistre un événement avec la localisation et le lot tels qu'ils sont aujourd'hui."""
room = equipment.effective_room
lot = equipment.effective_lot
event = EquipmentLifecycleEvent(
equipment=equipment,
event_type=event_type,
previous_status=previous_status,
new_status=new_status,
quantity=max(quantity or 1, 1),
reason=reason,
room_id_snapshot=room.id if room else None,
room_name_snapshot=room.name if room else None,
lot_id_snapshot=lot.id if lot else None,
lot_name_snapshot=lot.name if lot else None,
intervention_id=intervention_id,
created_by_id=user_id,
)
db.session.add(event)
return event
def extract_units(group, quantity, user_id=None, reason=None):
"""Individualise des unités d'un groupe sans modifier son total physique."""
if not group.is_group:
if quantity != 1:
raise ValueError("Un équipement individuel représente une seule unité.")
return [group]
quantity = max(int(quantity or 0), 0)
if quantity < 1 or quantity > group.unindividualized_quantity:
raise ValueError(
f"Quantité invalide : {group.unindividualized_quantity} unité(s) seulement sont disponibles."
)
root = group
while root.parent_id:
root = root.parent
next_number = max([item.individual_number or 0 for item in root.all_children] or [0]) + 1
created = []
room = group.effective_room
for offset in range(quantity):
unit = Equipment(
name=f"{root.name} #{next_number + offset}",
description=root.description,
status="en_service",
lifecycle_status="en_service",
parent_id=group.id,
room_id=room.id if room else None,
is_group=False,
quantity=1,
tracked_individually=True,
individual_number=next_number + offset,
mobility=root.mobility,
)
db.session.add(unit)
db.session.flush()
record_event(unit, "individualisation", user_id, new_status="en_service", reason=reason)
created.append(unit)
return created
def _open_tasks(equipment):
return ScheduledTask.query.filter(
ScheduledTask.equipment_id == equipment.id,
ScheduledTask.status.in_(OPEN_TASK_STATUSES),
).all()
def _suspend_preventive(equipment, reason):
now = datetime.now(timezone.utc)
for task in _open_tasks(equipment):
rule = task.lot_task
if rule and (rule.is_safety_critical or rule.unavailable_policy == "keep"):
continue
task.status = "suspended"
task.suspension_reason = reason
task.suspended_at = now
def _cancel_preventive(equipment, reason, keep_safety=False):
for task in _open_tasks(equipment):
if keep_safety and task.lot_task and task.lot_task.is_safety_critical:
continue
task.status = "cancelled"
task.postpone_reason = reason
task.suspension_reason = None
def _resume_preventive(equipment):
"""Réactive les échéances suspendues en appliquant la règle du lot."""
from app_new.core.services.maintenance_engine import next_working_day
today = date.today()
for task in _open_tasks(equipment):
if task.status != "suspended":
continue
rule = task.lot_task
if rule and rule.after_repair_policy == "reset" and rule.jours_entre_interventions:
target = today + timedelta(days=rule.jours_entre_interventions)
else:
target = max(task.scheduled_date or today, today)
task.scheduled_date = next_working_day(target)
task.status = "planned"
task.suspension_reason = None
task.suspended_at = None
task.lifecycle_status_snapshot = "en_service"
def create_corrective_intervention(source, units, reason, user_id=None):
room = source.effective_room
lot = source.effective_lot
quantity = len(units)
intervention = Intervention(
title=f"Réparation — {quantity} × {source.name}"[:300],
description=reason or "Équipement retiré provisoirement pour diagnostic et réparation.",
type="curatif",
status="en_attente",
priority="normale",
equipment_id=source.id,
room_id=room.id if room else None,
lot_id=lot.id if lot else None,
quantity_affected=quantity,
individual_numbers=",".join(str(unit.individual_number) for unit in units if unit.individual_number),
# L'intervention reste non affectée jusqu'à sa prise en charge. L'auteur
# de l'action est conservé dans le journal de cycle de vie.
)
db.session.add(intervention)
db.session.flush()
return intervention
def start_repair(source, quantity=1, reason=None, user_id=None, create_intervention=True):
units = extract_units(source, quantity, user_id=user_id, reason=reason) if source.is_group else [source]
intervention = create_corrective_intervention(source, units, reason, user_id) if create_intervention else None
now = datetime.now(timezone.utc)
for unit in units:
previous = unit.lifecycle_status
unit.lifecycle_status = "en_reparation"
unit.status = "en_maintenance"
unit.unavailable_since = now
_suspend_preventive(unit, "Équipement indisponible pendant sa réparation")
record_event(unit, "retrait_reparation", user_id, previous, unit.lifecycle_status,
reason=reason, intervention_id=intervention.id if intervention else None)
return units, intervention
def complete_repair(equipment, reason=None, user_id=None):
previous = equipment.lifecycle_status
equipment.lifecycle_status = "en_service"
equipment.status = "en_service"
equipment.unavailable_since = None
equipment.create_scheduled_tasks_from_lot(commit=False)
_resume_preventive(equipment)
record_event(equipment, "reparation_terminee", user_id, previous, "en_service", reason=reason)
def propose_disposal(source, quantity=1, reason=None, planned_date=None, user_id=None):
units = extract_units(source, quantity, user_id=user_id, reason=reason) if source.is_group else [source]
for unit in units:
unit.create_scheduled_tasks_from_lot(commit=False)
previous = unit.lifecycle_status
unit.status = "a_jeter"
unit.lifecycle_status = "mise_au_rebut_planifiee"
unit.planned_disposal_date = planned_date
unit.disposal_reason = reason
_cancel_preventive(unit, "Mise au rebut proposée", keep_safety=True)
record_event(unit, "rebut_propose", user_id, previous, unit.lifecycle_status, reason=reason)
return units
def cancel_disposal(equipment, reason=None, user_id=None):
previous = equipment.lifecycle_status
equipment.status = "en_service"
equipment.lifecycle_status = "en_service"
equipment.planned_disposal_date = None
equipment.disposal_reason = None
_resume_preventive(equipment)
record_event(equipment, "rebut_annule", user_id, previous, "en_service", reason=reason)
def confirm_disposal(equipment, reason=None, user_id=None):
previous = equipment.lifecycle_status
equipment.status = "jete"
equipment.lifecycle_status = "jete"
equipment.disposal_date = date.today()
equipment.disposal_reason = reason or equipment.disposal_reason
_cancel_preventive(equipment, "Équipement sorti définitivement du patrimoine")
record_event(equipment, "rebut_confirme", user_id, previous, "jete", reason=equipment.disposal_reason)
equipment.room_id = None
def update_future_task_location(equipment):
room = equipment.effective_room
for task in _open_tasks(equipment):
if task.intervention_id:
continue
task.room_id = room.id if room else None
task.room_name_snapshot = room.name if room else None