""" 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")) academic_year = db.Column(db.Integer, nullable=True, index=True) 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 template_id = db.Column(db.Integer, db.ForeignKey("work_schedule_templates.id"), nullable=True) is_active = db.Column(db.Boolean, default=True) user = db.relationship("User", backref=db.backref("work_schedules", lazy="dynamic")) template = db.relationship("WorkScheduleTemplate", back_populates="assignments") def __repr__(self): return f"" class WorkScheduleTemplate(db.Model): """Un des trois profils horaires paramétrables de l'agent.""" __tablename__ = "work_schedule_templates" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) name = db.Column(db.String(80), nullable=False) 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) is_active = db.Column(db.Boolean, nullable=False, default=True) user = db.relationship("User", backref=db.backref("work_schedule_templates", lazy="dynamic")) assignments = db.relationship("WorkSchedule", back_populates="template") @property def duration_minutes(self): total = (self.end_time.hour * 60 + self.end_time.minute) - (self.start_time.hour * 60 + self.start_time.minute) if self.lunch_start and self.lunch_end: total -= (self.lunch_end.hour * 60 + self.lunch_end.minute) - (self.lunch_start.hour * 60 + self.lunch_start.minute) return max(total, 0) class AnnualTimeConfig(db.Model): """Objectif annuel et déduction de pénibilité pour un agent.""" __tablename__ = "annual_time_configs" __table_args__ = (db.UniqueConstraint("user_id", "year", name="uq_annual_time_user_year"),) id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) year = db.Column(db.Integer, nullable=False) target_minutes = db.Column(db.Integer, nullable=False, default=1607 * 60) hardship_deduction_minutes = db.Column(db.Integer, nullable=False, default=0) notes = db.Column(db.Text, nullable=True) user = db.relationship("User", backref=db.backref("annual_time_configs", lazy="dynamic")) @property def minutes_to_work(self): return max((self.target_minutes or 0) - (self.hardship_deduction_minutes or 0), 0) class TimeEntry(db.Model): """Suivi réel d'une journée : retard, présence, permanence ou heures sup.""" __tablename__ = "work_time_entries" __table_args__ = (db.UniqueConstraint("user_id", "work_date", name="uq_work_time_user_date"),) id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) work_date = db.Column(db.Date, nullable=False, index=True) planned_minutes = db.Column(db.Integer, nullable=False, default=0) actual_minutes = db.Column(db.Integer, nullable=False, default=0) entry_type = db.Column(db.String(30), nullable=False, default="travail") note = db.Column(db.Text, nullable=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)) user = db.relationship("User", backref=db.backref("work_time_entries", lazy="dynamic")) @property def variance_minutes(self): return (self.actual_minutes or 0) - (self.planned_minutes or 0) 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"" 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"" 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, ) template_id = db.Column(db.Integer, db.ForeignKey("work_schedule_templates.id"), nullable=True, 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"), ) template = db.relationship("WorkScheduleTemplate") def __repr__(self): return f"" 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"" class Training(db.Model): """Formation.""" __tablename__ = "trainings" __table_args__ = (db.UniqueConstraint("source_type", "source_id", name="uq_training_source"),) 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) 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) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) participants = db.relationship("TrainingParticipant", back_populates="training", cascade="all, delete-orphan") def __repr__(self): return f"" 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"" 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"" 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) meter = db.relationship("Meter", back_populates="readings") read_by = db.relationship("User") def __repr__(self): return f"" 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"" 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"" 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"" 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")) contract_id = db.Column(db.Integer, db.ForeignKey("contracts.id"), nullable=True) 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") contract = db.relationship("Contract") consumables_needed = db.relationship("PreventiveTaskConsumable", back_populates="task", lazy="dynamic") def __repr__(self): return f"" 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"" 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) 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"" 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"" 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"" 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"" 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""