2026-08-14 18:02:25 +02:00
|
|
|
"""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
|
|
|
|
|
)
|
2026-08-15 00:47:55 +02:00
|
|
|
from ..core.models.maintenance import Lot, LotTask, Intervention
|
2026-08-14 18:02:25 +02:00
|
|
|
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
|
2026-08-20 22:25:52 +02:00
|
|
|
from ..core.models.planning import PlanningDay, PlanningItem, ClosureWorkDay
|
|
|
|
|
from ..core.models.settings import AppSettings
|
|
|
|
|
from ..core.services.planning_service import get_french_public_holidays
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
# Récupérer l'année demandée ou année courante
|
2026-08-20 22:25:52 +02:00
|
|
|
mode = request.args.get('mode')
|
|
|
|
|
saved_mode = AppSettings.get(f'planning_calendar_mode_{current_user.id}', 'calendar')
|
|
|
|
|
if mode not in ('calendar', 'school'):
|
|
|
|
|
mode = saved_mode if saved_mode in ('calendar', 'school') else 'calendar'
|
|
|
|
|
else:
|
|
|
|
|
AppSettings.set(f'planning_calendar_mode_{current_user.id}', mode)
|
2026-08-14 18:02:25 +02:00
|
|
|
year = request.args.get('year', date.today().year, type=int)
|
2026-08-20 22:25:52 +02:00
|
|
|
if mode == 'school' and 'year' not in request.args:
|
|
|
|
|
year = date.today().year if date.today().month >= 9 else date.today().year - 1
|
|
|
|
|
display_start = date(year, 9, 1) if mode == 'school' else date(year, 1, 1)
|
|
|
|
|
display_end = date(year + 1, 8, 31) if mode == 'school' else date(year, 12, 31)
|
2026-08-14 18:02:25 +02:00
|
|
|
|
2026-08-20 01:15:34 +02:00
|
|
|
# Préparer les données du calendrier. Le calendrier principal est la
|
|
|
|
|
# source unique : il rassemble les interventions et les échéances
|
|
|
|
|
# préventives qui n'ont pas encore été converties en intervention.
|
2026-08-14 18:02:25 +02:00
|
|
|
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(
|
2026-08-20 22:25:52 +02:00
|
|
|
CollegeClosure.start_date <= display_end,
|
|
|
|
|
CollegeClosure.end_date >= display_start
|
2026-08-14 18:02:25 +02:00
|
|
|
).all()
|
2026-08-20 22:25:52 +02:00
|
|
|
permanence_dates = {item.work_date for item in ClosureWorkDay.query.filter(ClosureWorkDay.work_date >= display_start, ClosureWorkDay.work_date <= display_end).all()}
|
|
|
|
|
holiday_dates = {item[0] for item in get_french_public_holidays(year)}
|
|
|
|
|
if mode == 'school':
|
|
|
|
|
holiday_dates |= {item[0] for item in get_french_public_holidays(year + 1)}
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
# 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(
|
2026-08-20 22:25:52 +02:00
|
|
|
TechnicianAvailability.date >= display_start,
|
|
|
|
|
TechnicianAvailability.date <= display_end
|
2026-08-14 18:02:25 +02:00
|
|
|
).all()
|
|
|
|
|
leave_dates = {l.date: l for l in leaves}
|
2026-08-20 01:15:34 +02:00
|
|
|
|
2026-08-20 22:25:52 +02:00
|
|
|
year_start = display_start
|
|
|
|
|
year_end = display_end
|
2026-08-20 01:15:34 +02:00
|
|
|
terminal_intervention_statuses = ('terminee', 'cloturee', 'annulee', 'ignoree')
|
|
|
|
|
interventions = Intervention.query.filter(
|
|
|
|
|
Intervention.scheduled_date >= year_start,
|
|
|
|
|
Intervention.scheduled_date <= year_end,
|
|
|
|
|
Intervention.is_deleted.is_(False),
|
|
|
|
|
~Intervention.status.in_(terminal_intervention_statuses),
|
|
|
|
|
).order_by(Intervention.scheduled_date, Intervention.scheduled_start).all()
|
|
|
|
|
scheduled_tasks = ScheduledTask.query.filter(
|
|
|
|
|
ScheduledTask.scheduled_date >= year_start,
|
|
|
|
|
ScheduledTask.scheduled_date <= year_end,
|
|
|
|
|
ScheduledTask.status.in_(('planned', 'in_progress', 'suspended', 'postponed')),
|
|
|
|
|
# Une échéance convertie est déjà représentée par son intervention.
|
|
|
|
|
ScheduledTask.intervention_id.is_(None),
|
|
|
|
|
).order_by(ScheduledTask.scheduled_date, ScheduledTask.scheduled_start).all()
|
|
|
|
|
|
|
|
|
|
events_by_date = {}
|
|
|
|
|
for intervention in interventions:
|
|
|
|
|
events_by_date.setdefault(intervention.scheduled_date, []).append({
|
|
|
|
|
'title': intervention.title or 'Intervention',
|
|
|
|
|
'type': intervention.type or 'curatif',
|
|
|
|
|
'status': intervention.status,
|
|
|
|
|
'start_time': intervention.scheduled_start,
|
|
|
|
|
'end_time': intervention.scheduled_end,
|
|
|
|
|
'duration': intervention.estimated_duration,
|
|
|
|
|
'room_name': intervention.room.name if intervention.room else None,
|
|
|
|
|
'url': url_for('interventions.detail', id=intervention.id),
|
|
|
|
|
'source': 'Intervention',
|
|
|
|
|
'color': 'danger' if intervention.type == 'curatif' else 'primary',
|
|
|
|
|
})
|
|
|
|
|
for task in scheduled_tasks:
|
|
|
|
|
title = 'Tâche préventive'
|
|
|
|
|
if task.lot_task and task.lot_task.tache:
|
|
|
|
|
title = task.lot_task.tache
|
|
|
|
|
elif task.preventive_task and task.preventive_task.name:
|
|
|
|
|
title = task.preventive_task.name
|
|
|
|
|
elif task.equipment:
|
|
|
|
|
title = f'Intervention sur {task.equipment.name}'
|
|
|
|
|
room = task.room or (task.equipment.effective_room if task.equipment else None)
|
|
|
|
|
events_by_date.setdefault(task.scheduled_date, []).append({
|
|
|
|
|
'title': title,
|
|
|
|
|
'type': 'preventive',
|
|
|
|
|
'status': task.status,
|
|
|
|
|
'start_time': task.scheduled_start,
|
|
|
|
|
'end_time': task.scheduled_end,
|
|
|
|
|
'duration': task.estimated_duration,
|
|
|
|
|
'room_name': room.name if room else None,
|
|
|
|
|
'url': url_for('planning.scheduled_detail', id=task.id),
|
|
|
|
|
'source': 'Échéance préventive',
|
|
|
|
|
'color': 'info',
|
|
|
|
|
})
|
2026-08-14 18:02:25 +02:00
|
|
|
|
2026-08-20 22:25:52 +02:00
|
|
|
month_order = list(range(9, 13)) + list(range(1, 9)) if mode == 'school' else list(range(1, 13))
|
|
|
|
|
for month in month_order:
|
|
|
|
|
actual_year = year if mode != 'school' or month >= 9 else year + 1
|
2026-08-14 18:02:25 +02:00
|
|
|
# Nombre de jours dans le mois
|
2026-08-20 22:25:52 +02:00
|
|
|
days_in_month = monthrange(actual_year, month)[1]
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
# Jour de la semaine du premier jour (0=lundi, 6=dimanche)
|
2026-08-20 22:25:52 +02:00
|
|
|
first_weekday = date(actual_year, month, 1).weekday()
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
weeks = []
|
|
|
|
|
week = [None] * first_weekday # Cases vides avant le 1er jour
|
|
|
|
|
|
|
|
|
|
for day in range(1, days_in_month + 1):
|
2026-08-20 22:25:52 +02:00
|
|
|
d = date(actual_year, month, day)
|
2026-08-14 18:02:25 +02:00
|
|
|
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,
|
2026-08-20 22:25:52 +02:00
|
|
|
'is_holiday': d in holiday_dates,
|
|
|
|
|
'is_permanence': d in permanence_dates,
|
2026-08-14 18:02:25 +02:00
|
|
|
'is_leave': leave is not None,
|
2026-08-20 01:15:34 +02:00
|
|
|
'leave_type': leave.availability_type if leave else None,
|
|
|
|
|
'events': events_by_date.get(d, []),
|
2026-08-14 18:02:25 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 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],
|
2026-08-20 22:25:52 +02:00
|
|
|
'year': actual_year,
|
2026-08-14 18:02:25 +02:00
|
|
|
'weeks': weeks
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Statistiques
|
|
|
|
|
stats = {
|
2026-08-20 01:15:34 +02:00
|
|
|
'preventive_tasks': len(scheduled_tasks),
|
|
|
|
|
'curative_interventions': sum(1 for i in interventions if i.type == 'curatif'),
|
2026-08-14 18:02:25 +02:00
|
|
|
'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,
|
2026-08-20 22:25:52 +02:00
|
|
|
mode=mode,
|
2026-08-14 18:02:25 +02:00
|
|
|
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
|
2026-08-20 01:15:34 +02:00
|
|
|
scheduled_tasks = ScheduledTask.query.filter(
|
|
|
|
|
ScheduledTask.scheduled_date == d,
|
|
|
|
|
ScheduledTask.status.in_(('planned', 'in_progress', 'suspended', 'postponed')),
|
|
|
|
|
ScheduledTask.intervention_id.is_(None),
|
2026-08-14 18:02:25 +02:00
|
|
|
).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
|
2026-08-14 23:39:55 +02:00
|
|
|
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'
|
2026-08-14 18:02:25 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 23:39:55 +02:00
|
|
|
@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'))
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:02:25 +02:00
|
|
|
@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':
|
2026-08-14 23:39:55 +02:00
|
|
|
task = LotTask(
|
2026-08-14 18:02:25 +02:00
|
|
|
lot_id=request.form.get('lot_id', type=int),
|
2026-08-14 23:39:55 +02:00
|
|
|
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,
|
2026-08-14 18:02:25 +02:00
|
|
|
meter_threshold=request.form.get('meter_threshold', type=float),
|
2026-08-14 23:39:55 +02:00
|
|
|
season_start_month=request.form.get('season_start_month', type=int),
|
|
|
|
|
season_end_month=request.form.get('season_end_month', type=int),
|
2026-08-14 18:02:25 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 00:56:36 +02:00
|
|
|
@planning_bp.route('/scheduled/reconcile', methods=['POST'])
|
|
|
|
|
@login_required
|
|
|
|
|
def reconcile_scheduled():
|
|
|
|
|
from app_new.core.services.maintenance_engine import reconcile_planned_tasks
|
|
|
|
|
changed = reconcile_planned_tasks()
|
|
|
|
|
flash(f'{len(changed)} échéance(s) déplacée(s) vers un jour travaillé.', 'success' if changed else 'info')
|
|
|
|
|
return redirect(url_for('planning.scheduled'))
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:02:25 +02:00
|
|
|
@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)
|
2026-08-15 00:47:55 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@planning_bp.route('/consumables/<int:id>/edit', methods=['GET', 'POST'])
|
|
|
|
|
@login_required
|
|
|
|
|
def edit_consumable(id):
|
|
|
|
|
consumable = Consumable.query.get_or_404(id)
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
|
consumable.name = (request.form.get('name') or '').strip()
|
|
|
|
|
consumable.reference = request.form.get('reference') or None
|
|
|
|
|
consumable.category = request.form.get('category') or None
|
|
|
|
|
consumable.quantity = max(0, request.form.get('quantity', type=int, default=0))
|
|
|
|
|
consumable.min_quantity = max(0, request.form.get('min_quantity', type=int, default=0))
|
|
|
|
|
consumable.unit = request.form.get('unit') or 'pièce'
|
|
|
|
|
consumable.unit_price = request.form.get('unit_price', type=float)
|
|
|
|
|
consumable.supplier_id = request.form.get('supplier_id', type=int) or None
|
|
|
|
|
consumable.storage_location = request.form.get('storage_location') or request.form.get('location') or None
|
|
|
|
|
db.session.commit()
|
|
|
|
|
flash('Consommable mis à jour', 'success')
|
|
|
|
|
return redirect(url_for('planning.consumables'))
|
|
|
|
|
companies = Company.query.order_by(Company.name).all()
|
|
|
|
|
return render_template('planning/consumable_form.html', title='Modifier le consommable',
|
|
|
|
|
consumable=consumable, companies=companies)
|