gmao/app_new/core/models/planning.py

647 lines
24 KiB
Python
Raw Normal View History

2026-08-14 18:02:25 +02:00
"""
Modèles pour la planification et les maintenance préventive - GMAO Collège
"""
from datetime import datetime, timezone, date, time
from ...extensions import db
class WorkSchedule(db.Model):
"""Horaires de travail."""
__tablename__ = "work_schedules"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
day_of_week = db.Column(db.Integer)
start_time = db.Column(db.Time)
end_time = db.Column(db.Time)
lunch_start = db.Column(db.Time, nullable=True) # Début pause déjeuner
lunch_end = db.Column(db.Time, nullable=True) # Fin pause déjeuner
is_active = db.Column(db.Boolean, default=True)
user = db.relationship("User", backref=db.backref("work_schedules", lazy="dynamic"))
def __repr__(self):
return f"<WorkSchedule {self.day_of_week} {self.start_time}-{self.end_time}>"
class CollegeClosure(db.Model):
"""Fermetures du collège / Vacances scolaires."""
__tablename__ = "college_closures"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
start_date = db.Column(db.Date, nullable=False)
end_date = db.Column(db.Date, nullable=False)
closure_type = db.Column(db.String(50), default='vacances')
work_hours_type = db.Column(db.String(20), default='none') # none, reduced, custom
notes = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
# Relation avec les horaires spécifiques
schedules = db.relationship("ClosureSchedule", backref="closure", lazy="dynamic",
cascade="all, delete-orphan")
def __repr__(self):
return f"<CollegeClosure {self.name}>"
class ClosureSchedule(db.Model):
"""Horaires de travail pendant une période de vacances."""
__tablename__ = "closure_schedules"
id = db.Column(db.Integer, primary_key=True)
closure_id = db.Column(db.Integer, db.ForeignKey("college_closures.id"), nullable=False)
day_of_week = db.Column(db.Integer, nullable=False) # 0=Dimanche, 1=Lundi, etc.
start_time = db.Column(db.Time)
end_time = db.Column(db.Time)
lunch_start = db.Column(db.Time) # Début pause déjeuner
lunch_end = db.Column(db.Time) # Fin pause déjeuner
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
def __repr__(self):
days = {0: 'Dimanche', 1: 'Lundi', 2: 'Mardi', 3: 'Mercredi', 4: 'Jeudi', 5: 'Vendredi', 6: 'Samedi'}
return f"<ClosureSchedule {days.get(self.day_of_week)} {self.start_time}-{self.end_time}>"
class ClosureWorkDay(db.Model):
"""Journée exceptionnellement travaillée pendant des vacances scolaires."""
__tablename__ = "closure_work_days"
__table_args__ = (
db.UniqueConstraint("closure_id", "work_date", name="uq_closure_work_day"),
)
id = db.Column(db.Integer, primary_key=True)
closure_id = db.Column(
db.Integer,
db.ForeignKey("college_closures.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
work_date = db.Column(db.Date, nullable=False, index=True)
start_time = db.Column(db.Time, nullable=False)
end_time = db.Column(db.Time, nullable=False)
lunch_start = db.Column(db.Time, nullable=True)
lunch_end = db.Column(db.Time, nullable=True)
notes = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
closure = db.relationship(
"CollegeClosure",
backref=db.backref("work_days", cascade="all, delete-orphan", order_by="ClosureWorkDay.work_date"),
)
def __repr__(self):
return f"<ClosureWorkDay {self.work_date} {self.start_time}-{self.end_time}>"
2026-08-14 18:02:25 +02:00
class PersonalLeave(db.Model):
"""Congés personnels."""
__tablename__ = "personal_leaves"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
start_date = db.Column(db.Date, nullable=False)
end_date = db.Column(db.Date, nullable=False)
leave_type = db.Column(db.String(50))
notes = db.Column(db.Text)
is_approved = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
user = db.relationship("User", backref=db.backref("personal_leaves", lazy="dynamic"))
def __repr__(self):
return f"<PersonalLeave {self.user_id} {self.start_date}-{self.end_date}>"
class Training(db.Model):
"""Formation."""
__tablename__ = "trainings"
__table_args__ = (db.UniqueConstraint("source_type", "source_id", name="uq_training_source"),)
2026-08-14 18:02:25 +02:00
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
start_date = db.Column(db.Date)
end_date = db.Column(db.Date)
start_time = db.Column(db.Time, nullable=True)
end_time = db.Column(db.Time, nullable=True)
2026-08-14 18:02:25 +02:00
location = db.Column(db.String(255))
source_type = db.Column(db.String(30), nullable=True)
source_id = db.Column(db.String(191), nullable=True)
source_subject = db.Column(db.String(500), nullable=True)
source_sender = db.Column(db.String(255), nullable=True)
2026-08-14 18:02:25 +02:00
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
participants = db.relationship("TrainingParticipant", back_populates="training", cascade="all, delete-orphan")
2026-08-14 18:02:25 +02:00
def __repr__(self):
return f"<Training {self.name}>"
class TrainingParticipant(db.Model):
"""Participant à une formation."""
__tablename__ = "training_participants"
id = db.Column(db.Integer, primary_key=True)
training_id = db.Column(db.Integer, db.ForeignKey("trainings.id"))
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
is_confirmed = db.Column(db.Boolean, default=False)
notes = db.Column(db.Text)
training = db.relationship("Training", back_populates="participants")
user = db.relationship("User")
def __repr__(self):
return f"<TrainingParticipant {self.user_id}>"
class Meter(db.Model):
"""Compteur sur un équipement."""
__tablename__ = "meters"
id = db.Column(db.Integer, primary_key=True)
equipment_id = db.Column(db.Integer, db.ForeignKey("equipments.id"), nullable=False)
name = db.Column(db.String(100), nullable=False)
meter_type = db.Column(db.String(50), default="hours")
unit = db.Column(db.String(20), default="h")
current_value = db.Column(db.Float, default=0)
initial_value = db.Column(db.Float, default=0)
warning_threshold = db.Column(db.Float)
critical_threshold = db.Column(db.Float)
maintenance_interval = db.Column(db.Float)
last_maintenance_value = db.Column(db.Float)
# Intervalle de relevé en jours (pour planification)
reading_interval_days = db.Column(db.Integer)
last_reading_date = db.Column(db.Date)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
equipment = db.relationship("Equipment", backref=db.backref("meters", lazy="dynamic"))
readings = db.relationship("MeterReading", back_populates="meter", lazy="dynamic")
@property
def next_maintenance_at(self):
if self.last_maintenance_value and self.maintenance_interval:
return self.last_maintenance_value + self.maintenance_interval
return None
@property
def needs_maintenance(self):
if self.next_maintenance_at and self.current_value >= self.next_maintenance_at:
return True
if self.critical_threshold and self.current_value >= self.critical_threshold:
return True
return False
@property
def warning_level(self):
if self.critical_threshold and self.current_value >= self.critical_threshold:
return "critical"
if self.warning_threshold and self.current_value >= self.warning_threshold:
return "warning"
if self.needs_maintenance:
return "maintenance"
return "ok"
def __repr__(self):
return f"<Meter {self.name} ({self.current_value}{self.unit})>"
class MeterReading(db.Model):
"""Relevé de compteur."""
__tablename__ = "meter_readings"
id = db.Column(db.Integer, primary_key=True)
meter_id = db.Column(db.Integer, db.ForeignKey("meters.id"), nullable=False)
value = db.Column(db.Float, nullable=False)
reading_date = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
read_by_id = db.Column(db.Integer, db.ForeignKey("users.id"))
notes = db.Column(db.Text)
is_reset = db.Column(db.Boolean, nullable=False, default=False)
reset_reason = db.Column(db.String(255), nullable=True)
2026-08-14 18:02:25 +02:00
meter = db.relationship("Meter", back_populates="readings")
read_by = db.relationship("User")
def __repr__(self):
return f"<MeterReading {self.value}>"
class Consumable(db.Model):
"""Consommable pour maintenance."""
__tablename__ = "consumables"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
reference = db.Column(db.String(100))
category = db.Column(db.String(50))
quantity = db.Column(db.Integer, default=0)
min_quantity = db.Column(db.Integer, default=0)
unit = db.Column(db.String(20), default="pièce")
unit_price = db.Column(db.Float)
supplier_id = db.Column(db.Integer, db.ForeignKey("companies.id"))
storage_location = db.Column(db.String(100))
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
usages = db.relationship("ConsumableUsage", back_populates="consumable", lazy="dynamic")
supplier = db.relationship("Company")
@property
def is_low_stock(self):
return self.quantity <= self.min_quantity
def __repr__(self):
return f"<Consumable {self.name}>"
class ConsumableUsage(db.Model):
"""Utilisation de consommable."""
__tablename__ = "consumable_usages"
id = db.Column(db.Integer, primary_key=True)
consumable_id = db.Column(db.Integer, db.ForeignKey("consumables.id"))
equipment_id = db.Column(db.Integer, db.ForeignKey("equipments.id"))
intervention_id = db.Column(db.Integer, db.ForeignKey("interventions.id"))
quantity_used = db.Column(db.Integer, default=1)
used_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
used_by_id = db.Column(db.Integer, db.ForeignKey("users.id"))
notes = db.Column(db.Text)
consumable = db.relationship("Consumable", back_populates="usages")
equipment = db.relationship("Equipment")
intervention = db.relationship("Intervention")
used_by = db.relationship("User")
def __repr__(self):
return f"<ConsumableUsage {self.consumable_id}x{self.quantity_used}>"
class EquipmentConsumable(db.Model):
"""Consommables compatibles avec un équipement."""
__tablename__ = "equipment_consumables"
id = db.Column(db.Integer, primary_key=True)
equipment_category_id = db.Column(db.Integer, db.ForeignKey("equipment_categories.id"))
equipment_id = db.Column(db.Integer, db.ForeignKey("equipments.id"))
consumable_id = db.Column(db.Integer, db.ForeignKey("consumables.id"))
replacement_interval_days = db.Column(db.Integer)
replacement_interval_hours = db.Column(db.Integer)
replacement_interval_cycles = db.Column(db.Integer)
notes = db.Column(db.Text)
equipment_category = db.relationship("EquipmentCategory")
equipment = db.relationship("Equipment")
consumable = db.relationship("Consumable")
def __repr__(self):
return f"<EquipmentConsumable {self.equipment_id}-{self.consumable_id}>"
class PreventiveTask(db.Model):
"""Tâche préventive récurrente."""
__tablename__ = "preventive_tasks"
id = db.Column(db.Integer, primary_key=True)
lot_id = db.Column(db.Integer, db.ForeignKey("lots.id"))
category_id = db.Column(db.Integer, db.ForeignKey("equipment_categories.id"))
task_number = db.Column(db.String(20))
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
location_pattern = db.Column(db.String(255))
period_days = db.Column(db.Integer)
duration_minutes = db.Column(db.Integer, default=30)
has_contract = db.Column(db.Boolean, default=False)
manager = db.Column(db.String(50))
company_id = db.Column(db.Integer, db.ForeignKey("companies.id"))
trigger_type = db.Column(db.String(20), default="time")
meter_type_required = db.Column(db.String(50))
meter_threshold = db.Column(db.Float)
requires_room_access = db.Column(db.Boolean, default=True)
allowed_time_slots = db.Column(db.String(50), default="always")
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
lot = db.relationship("Lot", backref=db.backref("preventive_tasks", lazy="dynamic"))
category = db.relationship("EquipmentCategory", backref=db.backref("preventive_tasks", lazy="dynamic"))
company = db.relationship("Company")
consumables_needed = db.relationship("PreventiveTaskConsumable", back_populates="task", lazy="dynamic")
def __repr__(self):
return f"<PreventiveTask {self.task_number}: {self.name}>"
class PreventiveTaskConsumable(db.Model):
"""Consommables nécessaires pour une tâche préventive."""
__tablename__ = "preventive_task_consumables"
id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.Integer, db.ForeignKey("preventive_tasks.id"))
consumable_id = db.Column(db.Integer, db.ForeignKey("consumables.id"))
quantity = db.Column(db.Integer, default=1)
task = db.relationship("PreventiveTask", back_populates="consumables_needed")
consumable = db.relationship("Consumable")
def __repr__(self):
return f"<PreventiveTaskConsumable {self.task_id}-{self.consumable_id}>"
class ScheduledTask(db.Model):
"""Tâche planifiée pour un équipement."""
__tablename__ = "scheduled_tasks"
id = db.Column(db.Integer, primary_key=True)
preventive_task_id = db.Column(db.Integer, db.ForeignKey("preventive_tasks.id"))
lot_task_id = db.Column(db.Integer, db.ForeignKey("lot_tasks.id"))
equipment_id = db.Column(db.Integer, db.ForeignKey("equipments.id"))
room_id = db.Column(db.Integer, db.ForeignKey("rooms.id"))
company_id = db.Column(db.Integer, db.ForeignKey("companies.id"))
scheduled_date = db.Column(db.Date)
scheduled_start = db.Column(db.Time)
scheduled_end = db.Column(db.Time)
estimated_duration = db.Column(db.Integer)
status = db.Column(db.String(20), default="planned")
postpone_reason = db.Column(db.Text)
assigned_to_id = db.Column(db.Integer, db.ForeignKey("users.id"))
completed_at = db.Column(db.DateTime)
actual_duration = db.Column(db.Integer)
intervention_id = db.Column(db.Integer, db.ForeignKey("interventions.id"))
notes = db.Column(db.Text)
suspension_reason = db.Column(db.String(500), nullable=True)
suspended_at = db.Column(db.DateTime, nullable=True)
room_name_snapshot = db.Column(db.String(200), nullable=True)
lot_name_snapshot = db.Column(db.String(500), nullable=True)
equipment_name_snapshot = db.Column(db.String(200), nullable=True)
lifecycle_status_snapshot = db.Column(db.String(30), nullable=True)
2026-08-14 18:02:25 +02:00
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
preventive_task = db.relationship("PreventiveTask", backref=db.backref("scheduled_tasks", lazy="dynamic"))
lot_task = db.relationship("LotTask", backref=db.backref("scheduled_tasks", lazy="dynamic"))
equipment = db.relationship("Equipment", backref=db.backref("scheduled_tasks", lazy="dynamic"))
room = db.relationship("Room")
assigned_to = db.relationship("User")
company = db.relationship("Company")
intervention = db.relationship("Intervention")
@property
def is_overdue(self):
if self.status == "planned" and self.scheduled_date:
return self.scheduled_date < date.today()
return False
def can_be_ignored(self):
return self.status == 'planned'
def ignore(self, reason=''):
"""Marque la tache comme ignoree."""
self.status = 'ignored'
self.postpone_reason = reason
db.session.commit()
def convert_to_intervention(self, user_id=None):
"""Convertit la tache planifiee en intervention."""
from .maintenance import Intervention
if self.intervention_id:
return None # Deja convertie
title = ''
if self.preventive_task:
title = f"[{self.scheduled_date}] {self.preventive_task.name}"[:290]
elif self.lot_task:
title = f"[{self.scheduled_date}] {self.lot_task.tache or 'Tache lot'}"[:290]
elif self.equipment:
title = f"[{self.scheduled_date}] Tache {self.equipment.name}"[:290]
else:
title = f"Tache {self.id} du {self.scheduled_date}"
interv = Intervention(
title=title,
description='',
type='preventif',
status='en_attente',
priority='normale',
equipment_id=self.equipment_id,
room_id=self.room_id,
scheduled_date=self.scheduled_date,
estimated_duration=float(self.estimated_duration or 60),
)
db.session.add(interv)
db.session.flush()
self.intervention_id = interv.id
self.status = 'converted'
db.session.commit()
return interv
def __repr__(self):
return f"<ScheduledTask {self.id}: {self.scheduled_date}>"
class TechnicianAvailability(db.Model):
"""Disponibilité du technicien."""
__tablename__ = "technician_availability"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
date = db.Column(db.Date)
start_time = db.Column(db.Time)
end_time = db.Column(db.Time)
availability_type = db.Column(db.String(20), default="work")
notes = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
user = db.relationship("User", backref=db.backref("availabilities", lazy="dynamic"))
def __repr__(self):
return f"<TechnicianAvailability {self.date} {self.availability_type}>"
class AdminTask(db.Model):
"""Tâche administrative récurrente."""
__tablename__ = "admin_tasks"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
frequency = db.Column(db.String(20), default="daily")
day_of_week = db.Column(db.Integer)
day_of_month = db.Column(db.Integer)
start_time = db.Column(db.Time)
duration_minutes = db.Column(db.Integer, default=30)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
def __repr__(self):
return f"<AdminTask {self.name}>"
class ZoneAccessRule(db.Model):
"""Règle d'accès zone/salles."""
__tablename__ = "zone_access_rules"
id = db.Column(db.Integer, primary_key=True)
zone_id = db.Column(db.Integer, db.ForeignKey("zones.id"))
zone_name = db.Column(db.String(100))
blocking_room_ids = db.Column(db.Text)
blocked_hours_start = db.Column(db.Time)
blocked_hours_end = db.Column(db.Time)
notes = db.Column(db.Text)
zone = db.relationship("Zone")
class PlanningDay(db.Model):
"""Planification d'une journée."""
__tablename__ = "planning_days"
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.Date, nullable=False, unique=True)
# État de la journée
is_working_day = db.Column(db.Boolean, default=True)
is_holiday = db.Column(db.Boolean, default=False)
is_vacation = db.Column(db.Boolean, default=False)
# Notes
notes = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relations
items = db.relationship("PlanningItem", back_populates="planning_day", cascade="all, delete-orphan")
def __repr__(self):
return f"<PlanningDay {self.date}>"
class PlanningItem(db.Model):
"""Élément planifié (intervention, formation, administrative)."""
__tablename__ = "planning_items"
id = db.Column(db.Integer, primary_key=True)
planning_day_id = db.Column(db.Integer, db.ForeignKey("planning_days.id"))
# Type d'élément
item_type = db.Column(db.String(20), nullable=False) # 'preventive', 'curative', 'admin', 'formation'
# Références optionnelles
scheduled_task_id = db.Column(db.Integer, db.ForeignKey("scheduled_tasks.id"))
intervention_id = db.Column(db.Integer, db.ForeignKey("interventions.id"))
admin_task_id = db.Column(db.Integer, db.ForeignKey("admin_tasks.id"))
# Données de base (copiées pour affichage rapide)
title = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text)
duration_minutes = db.Column(db.Integer, default=30)
# Planification
start_time = db.Column(db.Time)
end_time = db.Column(db.Time)
room_id = db.Column(db.Integer, db.ForeignKey("rooms.id"))
# Priorité (1=haute, 5=basse)
priority = db.Column(db.Integer, default=3)
# Regroupement
group_id = db.Column(db.String(50)) # Pour grouper les tâches dans la même salle
# Présence requise (entreprise externe mais technicien présent)
requires_presence = db.Column(db.Boolean, default=False)
# État
status = db.Column(db.String(20), default="planned") # planned, in_progress, completed, postponed
postpone_reason = db.Column(db.Text)
postponed_to_date = db.Column(db.Date)
# Compteurs (pour relevés)
meter_reading_id = db.Column(db.Integer) # Référence sans FK (table créée plus tard)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relations
planning_day = db.relationship("PlanningDay", back_populates="items")
scheduled_task = db.relationship("ScheduledTask")
intervention = db.relationship("Intervention")
admin_task = db.relationship("AdminTask")
room = db.relationship("Room")
@property
def is_prioritary(self):
"""Une tâche est prioritaire si sa priorité est 1 ou 2."""
return self.priority in (1, 2)
def __repr__(self):
return f"<PlanningItem {self.item_type}: {self.title}>"