335 lines
13 KiB
Python
335 lines
13 KiB
Python
"""Planning Schedules Routes - GMAO Collège"""
|
|
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
|
from flask_login import login_required, current_user
|
|
from ..extensions import db
|
|
from ..core.models.planning import (
|
|
PreventiveTask, PreventiveTaskConsumable, ScheduledTask,
|
|
Meter, MeterReading, Consumable, ConsumableUsage,
|
|
TechnicianAvailability, AdminTask, ZoneAccessRule,
|
|
WorkSchedule, CollegeClosure, PlanningDay, PlanningItem
|
|
)
|
|
from ..core.models.maintenance import Lot, Intervention
|
|
from ..core.models.equipment import EquipmentCategory
|
|
from ..core.models.company import Company
|
|
from datetime import datetime, time as dt_time
|
|
|
|
planning_bp = Blueprint('planning', __name__, url_prefix='/planning', template_folder='templates')
|
|
|
|
|
|
@planning_bp.route('/')
|
|
@login_required
|
|
def index():
|
|
"""Page principale de planification - calendrier sur 1 an."""
|
|
from datetime import date, timedelta
|
|
from calendar import monthrange
|
|
from ..core.services.planning_service import PlanningService
|
|
from ..core.models.planning import PlanningDay, PlanningItem
|
|
|
|
# Récupérer l'année demandée ou année courante
|
|
year = request.args.get('year', date.today().year, type=int)
|
|
|
|
# Préparer les données du calendrier
|
|
months_data = []
|
|
months_names = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
|
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
|
|
|
|
# Récupérer les vacances
|
|
closures = CollegeClosure.query.filter(
|
|
CollegeClosure.start_date <= date(year, 12, 31),
|
|
CollegeClosure.end_date >= date(year, 1, 1)
|
|
).all()
|
|
|
|
# Fonction pour vérifier si une date est en vacances
|
|
def is_vacation(d):
|
|
for c in closures:
|
|
if c.start_date <= d <= c.end_date:
|
|
return c.name
|
|
return None
|
|
|
|
# Récupérer les congés personnels
|
|
leaves = TechnicianAvailability.query.filter(
|
|
TechnicianAvailability.date >= date(year, 1, 1),
|
|
TechnicianAvailability.date <= date(year, 12, 31)
|
|
).all()
|
|
leave_dates = {l.date: l for l in leaves}
|
|
|
|
for month in range(1, 13):
|
|
# Nombre de jours dans le mois
|
|
days_in_month = monthrange(year, month)[1]
|
|
|
|
# Jour de la semaine du premier jour (0=lundi, 6=dimanche)
|
|
first_weekday = date(year, month, 1).weekday()
|
|
|
|
weeks = []
|
|
week = [None] * first_weekday # Cases vides avant le 1er jour
|
|
|
|
for day in range(1, days_in_month + 1):
|
|
d = date(year, month, day)
|
|
vacation = is_vacation(d)
|
|
leave = leave_dates.get(d)
|
|
weekday = d.weekday()
|
|
|
|
week.append({
|
|
'day': day,
|
|
'date': d,
|
|
'is_weekend': weekday >= 5,
|
|
'is_vacation': vacation is not None,
|
|
'vacation_name': vacation,
|
|
'is_leave': leave is not None,
|
|
'leave_type': leave.availability_type if leave else None
|
|
})
|
|
|
|
# Si on atteint dimanche (fin de semaine), nouvelle semaine
|
|
if weekday == 6 or day == days_in_month:
|
|
# Compléter la semaine si nécessaire
|
|
while len(week) < 7:
|
|
week.append(None)
|
|
weeks.append(week)
|
|
week = []
|
|
|
|
months_data.append({
|
|
'name': months_names[month - 1],
|
|
'weeks': weeks
|
|
})
|
|
|
|
# Statistiques
|
|
stats = {
|
|
'preventive_tasks': ScheduledTask.query.filter_by(status='planned').count(),
|
|
'curative_interventions': Intervention.query.filter_by(type='curatif', status='en_cours').count(),
|
|
'admin_tasks': AdminTask.query.filter_by(is_active=True).count(),
|
|
'pending_items': PlanningItem.query.filter_by(status='planned').count(),
|
|
}
|
|
|
|
return render_template('planning/calendar.html',
|
|
year=year,
|
|
months=months_data,
|
|
stats=stats
|
|
)
|
|
|
|
|
|
@planning_bp.route('/day/<date_str>')
|
|
@login_required
|
|
def day(date_str):
|
|
"""Afficher le détail d'une journée."""
|
|
from datetime import datetime
|
|
from ..core.services.planning_service import PlanningService
|
|
|
|
try:
|
|
d = datetime.strptime(date_str, '%Y-%m-%d').date()
|
|
except ValueError:
|
|
flash('Date invalide', 'error')
|
|
return redirect(url_for('planning.index'))
|
|
|
|
# Récupérer le planning du jour
|
|
schedule = PlanningService.get_day_schedule(d)
|
|
|
|
# Récupérer les tâches planifiées pour ce jour
|
|
scheduled_tasks = ScheduledTask.query.filter_by(
|
|
scheduled_date=d,
|
|
status='planned'
|
|
).all()
|
|
|
|
return render_template('planning/day.html',
|
|
date=d,
|
|
schedule=schedule,
|
|
tasks=scheduled_tasks
|
|
)
|
|
|
|
|
|
@planning_bp.route('/tasks')
|
|
@login_required
|
|
def tasks():
|
|
"""Liste des tâches préventives."""
|
|
from app_new.core.models.maintenance import LotTask
|
|
tasks = LotTask.query.join(Lot).order_by(Lot.name, LotTask.num_tache).all()
|
|
lots = Lot.query.order_by(Lot.name).all()
|
|
categories = EquipmentCategory.query.order_by(EquipmentCategory.name).all()
|
|
|
|
return render_template('planning/tasks.html',
|
|
tasks=tasks,
|
|
lots=lots,
|
|
categories=categories)
|
|
|
|
|
|
@planning_bp.route('/tasks/<int:id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
def edit_task(id):
|
|
"""Modifier une tâche préventive."""
|
|
from app_new.core.models.maintenance import LotTask
|
|
task = LotTask.query.get_or_404(id)
|
|
|
|
if request.method == 'POST':
|
|
duree = request.form.get('duree_minutes')
|
|
task.duree_minutes = int(duree) if duree and duree.isdigit() else None
|
|
task.gestionnaire = request.form.get('gestionnaire') or None
|
|
task.periodicite = request.form.get('periodicite') or None
|
|
task.jours_entre_interventions = int(request.form.get('jours_entre_interventions')) if request.form.get('jours_entre_interventions') and request.form.get('jours_entre_interventions').isdigit() else None
|
|
task.type_maintenance = request.form.get('type_maintenance') or None
|
|
task.trigger_type = request.form.get('trigger_type', 'calendar')
|
|
task.trigger_event = request.form.get('trigger_event') or None
|
|
task.meter_threshold = request.form.get('meter_threshold', type=float)
|
|
task.season_start_month = request.form.get('season_start_month', type=int)
|
|
task.season_end_month = request.form.get('season_end_month', type=int)
|
|
task.advance_days = request.form.get('advance_days', type=int, default=0)
|
|
task.is_active = request.form.get('is_active') == 'on'
|
|
task.contrat = request.form.get('contrat') == 'on'
|
|
|
|
db.session.commit()
|
|
flash('Tâche mise à jour', 'success')
|
|
return redirect(url_for('planning.tasks'))
|
|
|
|
return render_template('planning/edit_task.html', task=task)
|
|
|
|
|
|
@planning_bp.route('/tasks/<int:id>/generate', methods=['POST'])
|
|
@login_required
|
|
def generate_task(id):
|
|
from app_new.core.models.maintenance import LotTask
|
|
from app_new.core.services.maintenance_engine import generate_due_tasks
|
|
task = LotTask.query.get_or_404(id)
|
|
created = generate_due_tasks(task, event=(request.form.get('event') or '').strip() or None)
|
|
flash(f'{len(created)} échéance(s) créée(s), sans doublon.', 'success' if created else 'info')
|
|
return redirect(url_for('planning.tasks'))
|
|
|
|
|
|
@planning_bp.route('/tasks/new', methods=['GET', 'POST'])
|
|
@login_required
|
|
def new_task():
|
|
"""Créer une nouvelle tâche préventive."""
|
|
if request.method == 'POST':
|
|
task = LotTask(
|
|
lot_id=request.form.get('lot_id', type=int),
|
|
num_tache=request.form.get('task_number'),
|
|
tache=request.form.get('name'),
|
|
periodicite=request.form.get('periodicite'),
|
|
jours_entre_interventions=request.form.get('period_days', type=int),
|
|
duree_minutes=request.form.get('duration_minutes', type=int, default=30),
|
|
contrat=request.form.get('has_contract') == 'on',
|
|
gestionnaire=request.form.get('manager'),
|
|
trigger_type=request.form.get('trigger_type', 'calendar'),
|
|
trigger_event=request.form.get('trigger_event') or None,
|
|
meter_threshold=request.form.get('meter_threshold', type=float),
|
|
season_start_month=request.form.get('season_start_month', type=int),
|
|
season_end_month=request.form.get('season_end_month', type=int),
|
|
is_active=True
|
|
)
|
|
db.session.add(task)
|
|
db.session.commit()
|
|
flash('Tâche préventive créée', 'success')
|
|
return redirect(url_for('planning.tasks'))
|
|
|
|
lots = Lot.query.order_by(Lot.name).all()
|
|
categories = EquipmentCategory.query.order_by(EquipmentCategory.name).all()
|
|
companies = Company.query.order_by(Company.name).all()
|
|
|
|
return render_template('planning/task_form.html',
|
|
title='Nouvelle tâche préventive',
|
|
task=None,
|
|
lots=lots,
|
|
categories=categories,
|
|
companies=companies)
|
|
|
|
|
|
@planning_bp.route('/scheduled')
|
|
@login_required
|
|
def scheduled():
|
|
"""Liste des tâches planifiées."""
|
|
status = request.args.get('status', 'all')
|
|
date_filter = request.args.get('date')
|
|
|
|
query = ScheduledTask.query.filter(ScheduledTask.equipment_id.isnot(None))
|
|
|
|
if status != 'all':
|
|
query = query.filter_by(status=status)
|
|
|
|
if date_filter:
|
|
query = query.filter_by(scheduled_date=datetime.strptime(date_filter, '%Y-%m-%d').date())
|
|
|
|
tasks = query.order_by(ScheduledTask.scheduled_date, ScheduledTask.scheduled_start).all()
|
|
|
|
return render_template('planning/scheduled.html', tasks=tasks, status=status)
|
|
|
|
|
|
@planning_bp.route('/scheduled/<int:id>')
|
|
@login_required
|
|
def scheduled_detail(id):
|
|
"""Détail d'une tâche planifiée."""
|
|
task = ScheduledTask.query.get_or_404(id)
|
|
return render_template('planning/scheduled_detail.html', task=task)
|
|
|
|
|
|
@planning_bp.route('/meters')
|
|
@login_required
|
|
def meters():
|
|
"""Liste des compteurs."""
|
|
meters = Meter.query.order_by(Meter.equipment_id).all()
|
|
return render_template('planning/meters.html', meters=meters)
|
|
|
|
|
|
@planning_bp.route('/meters/new', methods=['GET', 'POST'])
|
|
@login_required
|
|
def new_meter():
|
|
"""Créer un nouveau compteur."""
|
|
if request.method == 'POST':
|
|
meter = Meter(
|
|
equipment_id=request.form.get('equipment_id', type=int),
|
|
name=request.form.get('name'),
|
|
meter_type=request.form.get('meter_type', 'hours'),
|
|
unit=request.form.get('unit', 'h'),
|
|
current_value=request.form.get('current_value', type=float, default=0),
|
|
initial_value=request.form.get('initial_value', type=float, default=0),
|
|
warning_threshold=request.form.get('warning_threshold', type=float),
|
|
critical_threshold=request.form.get('critical_threshold', type=float),
|
|
maintenance_interval=request.form.get('maintenance_interval', type=float),
|
|
is_active=True
|
|
)
|
|
db.session.add(meter)
|
|
db.session.commit()
|
|
flash('Compteur créé', 'success')
|
|
return redirect(url_for('planning.meters'))
|
|
|
|
from ..core.models.equipment import Equipment
|
|
equipments = Equipment.query.filter_by(is_group=False).order_by(Equipment.name).all()
|
|
|
|
return render_template('planning/meter_form.html',
|
|
title='Nouveau compteur',
|
|
meter=None,
|
|
equipments=equipments)
|
|
|
|
|
|
@planning_bp.route('/consumables')
|
|
@login_required
|
|
def consumables():
|
|
"""Liste des consommables."""
|
|
consumables = Consumable.query.order_by(Consumable.category, Consumable.name).all()
|
|
return render_template('planning/consumables.html', consumables=consumables)
|
|
|
|
|
|
@planning_bp.route('/consumables/new', methods=['GET', 'POST'])
|
|
@login_required
|
|
def new_consumable():
|
|
"""Créer un nouveau consommable."""
|
|
if request.method == 'POST':
|
|
consumable = Consumable(
|
|
name=request.form.get('name'),
|
|
reference=request.form.get('reference'),
|
|
category=request.form.get('category'),
|
|
quantity=request.form.get('quantity', type=int, default=0),
|
|
min_quantity=request.form.get('min_quantity', type=int, default=0),
|
|
unit=request.form.get('unit', 'pièce'),
|
|
unit_price=request.form.get('unit_price', type=float),
|
|
supplier_id=request.form.get('supplier_id', type=int) or None,
|
|
storage_location=request.form.get('storage_location'),
|
|
is_active=True
|
|
)
|
|
db.session.add(consumable)
|
|
db.session.commit()
|
|
flash('Consommable créé', 'success')
|
|
return redirect(url_for('planning.consumables'))
|
|
|
|
companies = Company.query.order_by(Company.name).all()
|
|
|
|
return render_template('planning/consumable_form.html',
|
|
title='Nouveau consommable',
|
|
consumable=None,
|
|
companies=companies)
|