gmao/app_new/interventions/crud.py
root 256a0f43b7
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Distinguer le template des interventions
2026-08-21 10:40:58 +00:00

711 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Interventions Crud Routes - GMAO Collège"""
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify
from flask_login import login_required, current_user
from app_new.extensions import db
from ..core.models.maintenance import Intervention, InterventionComment
from ..core.models.equipment import Equipment, EquipmentCategory
from ..core.models.user import User
from datetime import datetime
interventions_bp = Blueprint("interventions", __name__, url_prefix="/interventions", template_folder="templates")
"""
Interventions Routes - GMAO Collège
Gestion des interventions de maintenance
"""
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_required, current_user
from datetime import datetime, timezone
from app_new.extensions import db
from ..core.models.maintenance import Intervention, StatusChange, InterventionComment, Lot
from ..core.models.planning import ScheduledTask
from ..core.models.equipment import Equipment
from ..core.models.college import Room
from ..core.models.company import Service, Company
from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES, WORKFLOW_TYPES, WORKFLOW_STATUS_ORDERS
interventions_bp = Blueprint('interventions', __name__, template_folder='templates')
@interventions_bp.route('/')
@login_required
def index():
"""Liste des interventions avec filtres avances et tri par colonne."""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', '')
priority = request.args.get('priority', '')
intervention_type = request.args.get('type', '')
workflow_type = request.args.get('workflow_type', '')
lot_id = request.args.get('lot', type=int)
assigned_to_id = request.args.get('assigned_to_id', type=int)
frequency = request.args.get('frequency', '')
date_from = request.args.get('date_from', '')
date_to = request.args.get('date_to', '')
sort_col = request.args.get('sort', 'created_at')
sort_dir = request.args.get('dir', 'desc')
show_trashed = request.args.get('trashed', '0') == '1'
query = Intervention.query
if show_trashed:
query = query.filter_by(is_deleted=True)
else:
query = query.filter_by(is_deleted=False)
if status:
query = query.filter_by(status=status)
if priority:
query = query.filter_by(priority=priority)
if intervention_type:
query = query.filter(Intervention.type == intervention_type)
if workflow_type in WORKFLOW_TYPES:
query = query.filter_by(workflow_type=workflow_type)
if lot_id:
query = query.filter_by(lot_id=lot_id)
if assigned_to_id:
query = query.filter_by(assigned_to_id=assigned_to_id)
if date_from:
try:
d = datetime.strptime(date_from, '%Y-%m-%d').date()
query = query.filter(Intervention.scheduled_date >= d)
except ValueError:
pass
if date_to:
try:
d = datetime.strptime(date_to, '%Y-%m-%d').date()
query = query.filter(Intervention.scheduled_date <= d)
except ValueError:
pass
if frequency:
if frequency == 'recurring':
query = query.filter(Intervention.is_recurring == True)
elif frequency == 'ponctuel':
query = query.filter(Intervention.is_recurring == False)
elif frequency.isdigit():
query = query.filter(Intervention.frequency_days == int(frequency))
# Tri par colonne (avec validation pour eviter injection)
valid_sort_cols = {'created_at', 'scheduled_date', 'priority', 'status', 'title'}
if sort_col not in valid_sort_cols:
sort_col = 'created_at'
sort_dir = 'asc' if sort_dir == 'asc' else 'desc'
order_clause = getattr(Intervention, sort_col).asc() if sort_dir == 'asc' else getattr(Intervention, sort_col).desc()
query = query.order_by(order_clause)
interventions = query.paginate(page=page, per_page=20)
lots = Lot.query.order_by(Lot.name).all()
users = User.query.filter_by(is_active=True).order_by(User.username).all()
# Compter les interventions par statut
status_counts = {}
for status_key in INTERVENTION_STATUSES.keys():
status_counts[status_key] = Intervention.query.filter_by(status=status_key, is_deleted=False).count()
# Compter les interventions à jeter
trashed_count = Intervention.query.filter_by(is_deleted=True).count()
return render_template('interventions/index.html',
interventions=interventions,
lots=lots,
users=users,
statuses=INTERVENTION_STATUSES,
workflow_types=WORKFLOW_TYPES,
priorities=PRIORITIES,
status_counts=status_counts,
trashed_count=trashed_count,
show_trashed=show_trashed,
sort_col=sort_col,
sort_dir=sort_dir)
@interventions_bp.route('/new', methods=['GET', 'POST'])
@login_required
def create():
"""Créer une intervention."""
from ..core.models.user import User
from ..outlook.models import OutlookMailInterpretation
if request.method == 'POST':
# Une intervention générale peut cibler une salle sans équipement.
equipment_value = request.form.get('equipment_id')
equipment_id = int(equipment_value) if equipment_value and equipment_value.isdigit() else None
# Déterminer le demandeur
requester_name = request.form.get('requester_name', '').strip()
if not requester_name:
# Si pas de demandeur fourni, utiliser le nom de l'utilisateur connecté
requester_name = current_user.username if hasattr(current_user, 'username') else 'Inconnu'
selected_type = request.form.get('intervention_type', 'curatif')
workflow_type = request.form.get('workflow_type') or {
'preventif': 'preventive', 'amelioratif': 'travaux',
'prevention': 'prevention', 'administratif': 'prevention',
}.get(selected_type, 'corrective')
intervention = Intervention(
title=request.form.get('title'),
description=request.form.get('description') or '',
lot_id=request.form.get('lot_id') or None,
equipment_id=equipment_id,
room_id=request.form.get('room_id') or None,
assigned_to_id=request.form.get('assigned_to_id') or None,
author_id=current_user.id,
status=request.form.get('status', 'brouillon'),
priority=request.form.get('priority', 'normale'),
type=selected_type,
workflow_type=workflow_type if workflow_type in WORKFLOW_TYPES else 'corrective',
notes=request.form.get('notes') or '',
requester_name=requester_name,
scheduled_date=datetime.strptime(request.form.get('intervention_date'), '%Y-%m-%d').date() if request.form.get('intervention_date') else None,
scheduled_start=datetime.strptime(request.form.get('scheduled_start'), '%H:%M').time() if request.form.get('scheduled_start') else None,
scheduled_end=datetime.strptime(request.form.get('scheduled_end'), '%H:%M').time() if request.form.get('scheduled_end') else None
)
db.session.add(intervention)
db.session.commit()
# Marquer l'interprétation comme acceptée si fournie
from_interpretation_id = request.form.get('from_interpretation')
source = request.form.get('source', 'outlook') # 'outlook' ou 'ent'
if from_interpretation_id:
if source == 'ent':
from app_new.ent.interpretation_models import EntMessageInterpretation
from app_new.lib_ext.ent_service import move_ent_message_to_processed
interpretation = EntMessageInterpretation.query.get(from_interpretation_id)
if interpretation:
message = interpretation.message
# Déplacer le message ENT vers "Traités"
if message:
move_result = move_ent_message_to_processed(interpretation.message_id)
if move_result.get('success'):
flash('Intervention créée. Message ENT déplacé vers Traités.', 'success')
else:
flash(f'Intervention créée. Erreur déplacement ENT: {move_result.get("error")}', 'warning')
interpretation.status = 'accepted'
if message:
message.intervention_id = intervention.id
db.session.commit()
else:
from app_new.outlook.models import OutlookMail
from app_new.outlook.routes import move_mail_to_processed
interpretation = OutlookMailInterpretation.query.get(from_interpretation_id)
if interpretation:
mail = interpretation.mail
# Déplacer le mail Outlook vers "Traités"
if mail:
move_result = move_mail_to_processed(mail)
if move_result.get('success'):
flash('Intervention créée. Mail déplacé vers Traités.', 'success')
else:
flash(f'Intervention créée. Erreur déplacement mail: {move_result.get("error")}', 'warning')
interpretation.status = 'accepted'
if mail:
mail.intervention_id = intervention.id
db.session.commit()
# Flash déjà envoyé ci-dessus
else:
flash('Intervention créée avec succès.', 'success')
return redirect(url_for('interventions.detail', id=intervention.id))
# GET - Pré-remplir depuis interprétation ou tâche planifiée
from_interpretation = request.args.get('from_interpretation')
scheduled_task_id = request.args.get('scheduled_task_id', type=int)
# Valeurs par défaut
prefilled = {
'title': '',
'description': '',
'priority': 'normale',
'type': 'curatif',
'location': '',
'equipment': '',
'assignee': '',
'date': None,
'requester_name': ''
, 'workflow_type': request.args.get('workflow', 'corrective')
}
interpretation = None
json_suggestions = None
if from_interpretation:
source = request.args.get('source', 'outlook')
if source == 'ent':
from app_new.ent.interpretation_models import EntMessageInterpretation
interpretation = EntMessageInterpretation.query.get(from_interpretation)
else:
interpretation = OutlookMailInterpretation.query.get(from_interpretation)
if interpretation:
# Adapter selon la source
if source == 'ent':
json_suggestions = {
'title': interpretation.message.subject if interpretation.message else '',
'description': interpretation.suggested_description,
'priority': interpretation.analysis_priority,
'type': interpretation.analysis_type,
'location': '',
'equipment': interpretation.suggested_equipment,
'assignee': '',
'date': None
}
prefilled['title'] = interpretation.message.subject if interpretation.message else ''
prefilled['description'] = interpretation.suggested_description or ''
prefilled['priority'] = interpretation.analysis_priority or 'normale'
# Demandeur = sender du message ENT
prefilled['requester_name'] = interpretation.message.sender_name if interpretation.message else ''
type_map = {
'curative': 'curatif',
'preventive': 'preventif',
'administrative': 'administratif',
'formation': 'formation',
'information': 'curatif'
}
prefilled['type'] = type_map.get(interpretation.analysis_type, 'curatif')
prefilled['workflow_type'] = 'preventive' if interpretation.analysis_type == 'preventive' else prefilled.get('workflow_type', 'corrective')
prefilled['equipment'] = interpretation.suggested_equipment or ''
else:
json_suggestions = {
'title': interpretation.suggested_title,
'description': interpretation.suggested_description,
'priority': interpretation.suggested_urgency,
'type': interpretation.analysis_type,
'location': interpretation.suggested_location,
'equipment': interpretation.suggested_equipment,
'assignee': interpretation.suggested_assignee,
'date': interpretation.suggested_date.strftime('%d/%m/%Y') if interpretation.suggested_date else None
}
# Pré-remplir avec les valeurs de l'interprétation
prefilled['title'] = interpretation.suggested_title or ''
prefilled['description'] = interpretation.suggested_description or ''
prefilled['priority'] = interpretation.suggested_urgency or 'normale'
# Demandeur = sender du mail Outlook
prefilled['requester_name'] = interpretation.mail.from_name if interpretation.mail else ''
# Mapper le type
type_map = {
'curative': 'curatif',
'preventive': 'preventif',
'administrative': 'administratif',
'formation': 'formation',
'information': 'curatif'
}
prefilled['type'] = type_map.get(interpretation.analysis_type, 'curatif')
prefilled['location'] = interpretation.suggested_location or ''
prefilled['equipment'] = interpretation.suggested_equipment or ''
prefilled['assignee'] = interpretation.suggested_assignee or ''
prefilled['date'] = interpretation.suggested_date
# Chercher l'équipement correspondant si spécifié
equipment_id = None
equipment_match = None
if prefilled['equipment']:
eq = Equipment.query.filter(Equipment.name.ilike(f"%{prefilled['equipment']}%")).first()
if eq:
equipment_id = eq.id
equipment_match = eq
# Pré-remplir depuis tâche planifiée si fourni
if scheduled_task_id and not from_interpretation:
task = ScheduledTask.query.get(scheduled_task_id)
if task:
prefilled['title'] = f"Intervention: {task.preventive_task.name if task.preventive_task else task.lot_task.name if task.lot_task else 'Tâche planifiée'}"
prefilled['description'] = task.notes or ''
prefilled['date'] = task.scheduled_date.strftime('%Y-%m-%d') if task.scheduled_date else None
prefilled['scheduled_start'] = task.scheduled_start.strftime('%H:%M') if task.scheduled_start else None
prefilled['scheduled_end'] = task.scheduled_end.strftime('%H:%M') if task.scheduled_end else None
# Chercher la salle correspondante si spécifiée
room_id = None
room_match = None
if prefilled['location']:
room = Room.query.filter(Room.name.ilike(f"%{prefilled['location']}%")).first()
if room:
room_id = room.id
room_match = room
lots = Lot.query.order_by(Lot.name).all()
equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all()
rooms = Room.query.order_by(Room.name).all()
users = User.query.filter_by(is_active=True).order_by(User.username).all()
return render_template('interventions/new.html',
lots=lots,
equipments=equipments,
rooms=rooms,
users=users,
statuses=INTERVENTION_STATUSES,
priorities=PRIORITIES,
workflow_types=WORKFLOW_TYPES,
prefilled=prefilled,
equipment_id=equipment_id,
equipment_match=equipment_match,
room_id=room_id,
room_match=room_match,
from_interpretation=from_interpretation,
interpretation=interpretation,
json_suggestions=json_suggestions,
source=source if from_interpretation else 'outlook')
@interventions_bp.route('/create-for-group', methods=['GET'])
@login_required
def create_for_group_select():
"""Sélection d'équipements pour création groupée."""
equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all()
return render_template('interventions/create_for_group_select.html', equipments=equipments)
@interventions_bp.route('/create-for-group', methods=['POST'])
@login_required
def create_for_group():
"""Créer des interventions pour plusieurs équipements."""
equipment_ids = request.form.getlist('equipment_ids')
title = request.form.get('title')
description = request.form.get('description', '')
priority = request.form.get('priority', 'normale')
lot_id = request.form.get('lot_id')
if not equipment_ids:
flash('Aucun équipement sélectionné.', 'error')
return redirect(url_for('interventions.create_for_group_select'))
created_count = 0
for eq_id in equipment_ids:
equipment = Equipment.query.get(eq_id)
if equipment:
intervention = Intervention(
title=title,
description=description,
priority=priority,
lot_id=lot_id if lot_id else None,
equipment_id=equipment.id,
room_id=equipment.room_id,
status='brouillon',
intervention_type='corrective'
)
db.session.add(intervention)
created_count += 1
db.session.commit()
flash(f'{created_count} intervention(s) créée(s) avec succès.', 'success')
return redirect(url_for('interventions.index'))
@interventions_bp.route('/<int:id>')
@login_required
def detail(id):
"""Détail d'une intervention."""
intervention = Intervention.query.get_or_404(id)
allowed_workflow_statuses = set(WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective'])) | {'refusee', 'annulee', 'reportee'}
transitions = [(key, value['label']) for key, value in INTERVENTION_STATUSES.items()
if key in INTERVENTION_TRANSITIONS.get(intervention.status, set()) and key in allowed_workflow_statuses]
history = intervention.status_history.order_by(StatusChange.created_at.desc()).all()
# Utiliser explicitement le template du module. Une ancienne copie globale
# de ``interventions/detail.html`` est conservée pour compatibilité.
return render_template('interventions_module/detail.html',
intervention=intervention,
statuses=INTERVENTION_STATUSES,
transitions=transitions,
history=history,
workflow_status_order=WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective']),
rooms=Room.query.order_by(Room.name).all(),
categories=EquipmentCategory.query.order_by(EquipmentCategory.name).all(),
equipment_choices=Equipment.query.filter_by(is_deleted=False).order_by(Equipment.name).all(),
services=Service.query.filter_by(is_active=True).order_by(Service.name).all(),
companies=Company.query.filter_by(is_active=True).order_by(Company.name).all())
@interventions_bp.route('/<int:id>/work-details', methods=['POST'])
@login_required
def save_work_details(id):
"""Enregistre les informations de travaux depuis la fiche intervention."""
intervention = Intervention.query.get_or_404(id)
if 'execution_mode' in request.form:
intervention.execution_mode = request.form.get('execution_mode') or 'interne'
if 'department_service_id' in request.form:
service_id = request.form.get('department_service_id')
intervention.department_service_id = int(service_id) if service_id and service_id.isdigit() else None
if 'company_id' in request.form:
company_id = request.form.get('company_id')
intervention.company_id = int(company_id) if company_id and company_id.isdigit() else None
if 'quote_status' in request.form:
intervention.quote_status = request.form.get('quote_status') or 'non_requis'
if 'quote_reference' in request.form:
intervention.quote_reference = request.form.get('quote_reference') or None
for field in ('quote_amount_ht', 'quote_amount_ttc'):
if field in request.form:
setattr(intervention, field, float(request.form[field]) if request.form.get(field) else None)
for field in ('department_request_date', 'chief_validation_date', 'department_validation_date', 'quote_date', 'quote_valid_until'):
if field in request.form:
value = request.form.get(field)
setattr(intervention, field, datetime.strptime(value, '%Y-%m-%d').date() if value else None)
if 'work_notes' in request.form:
intervention.work_notes = request.form.get('work_notes') or None
db.session.commit()
flash('Workflow travaux enregistré.', 'success')
return redirect(url_for('interventions.detail', id=id))
@interventions_bp.route('/<int:id>/location', methods=['POST'])
@login_required
def update_location(id):
"""Modifie la localisation en cascade salle -> catégorie -> équipement."""
intervention = Intervention.query.get_or_404(id)
room_id = request.form.get('room_id', type=int)
category_id = request.form.get('category_id', type=int)
equipment_id = request.form.get('equipment_id', type=int)
room = Room.query.get(room_id) if room_id else None
category = EquipmentCategory.query.get(category_id) if category_id else None
equipment = Equipment.query.get(equipment_id) if equipment_id else None
if room_id and not room:
flash('Salle invalide.', 'danger')
return redirect(url_for('interventions.detail', id=id))
if equipment:
if room_id and equipment.room_id != room_id:
flash("L'équipement choisi n'est pas dans la salle sélectionnée.", 'danger')
return redirect(url_for('interventions.detail', id=id))
if category_id and equipment.effective_category_id != category_id:
flash("L'équipement choisi ne correspond pas à la catégorie sélectionnée.", 'danger')
return redirect(url_for('interventions.detail', id=id))
intervention.room_id = room.id if room else None
intervention.equipment_id = equipment.id if equipment else None
db.session.commit()
flash('Salle et équipement concernés mis à jour.', 'success')
return redirect(url_for('interventions.detail', id=id))
@interventions_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
"""Modifier une intervention."""
from ..core.models.user import User
intervention = Intervention.query.get_or_404(id)
if request.method == 'POST':
old_status = intervention.status
intervention.title = request.form.get('title')
intervention.description = request.form.get('description')
intervention.lot_id = request.form.get('lot_id') or None
intervention.equipment_id = request.form.get('equipment_id') or None
intervention.room_id = request.form.get('room_id') or None
intervention.assigned_to_id = request.form.get('assigned_to_id') or None
# Ne pas changer le statut si le formulaire ne le contient pas
new_status = request.form.get('status')
if new_status:
intervention.status = new_status
intervention.priority = request.form.get('priority')
intervention.type = request.form.get('intervention_type', intervention.type)
workflow_type = request.form.get('workflow_type')
if workflow_type in WORKFLOW_TYPES:
intervention.workflow_type = workflow_type
intervention.scheduled_date = request.form.get('scheduled_date') or None
intervention.notes = request.form.get('notes')
intervention.requester_name = request.form.get('requester_name') or None
# Enregistrer le changement de statut seulement si le statut a changé
if new_status and old_status != intervention.status:
status_change = StatusChange(
intervention_id=intervention.id,
from_status=old_status,
to_status=intervention.status,
changed_by_id=current_user.id
)
db.session.add(status_change)
db.session.commit()
flash('Intervention modifiée avec succès.', 'success')
return redirect(url_for('interventions.detail', id=intervention.id))
lots = Lot.query.order_by(Lot.name).all()
equipments = Equipment.query.filter_by(status='en_service').order_by(Equipment.name).all()
rooms = Room.query.order_by(Room.name).all()
users = User.query.filter_by(is_active=True).order_by(User.username).all()
# Pré-remplir avec les valeurs existantes pour l'édition
prefilled = {
'title': intervention.title,
'description': intervention.description or '',
'priority': intervention.priority,
'type': intervention.type,
'location': intervention.equipment.room.name if intervention.equipment and intervention.equipment.room else '',
'equipment': intervention.equipment.name if intervention.equipment else '',
'assignee': intervention.assigned_to.username if intervention.assigned_to else '',
'date': intervention.scheduled_date,
'requester_name': intervention.requester_name or ''
, 'workflow_type': intervention.workflow_type
}
return render_template('interventions/new.html',
intervention=intervention,
lots=lots,
equipments=equipments,
rooms=rooms,
users=users,
statuses=INTERVENTION_STATUSES,
priorities=PRIORITIES,
workflow_types=WORKFLOW_TYPES,
prefilled=prefilled,
title="Modifier l'intervention")
@interventions_bp.route('/<int:id>/status', methods=['POST'])
@login_required
def change_status(id):
"""Changer le statut d'une intervention."""
intervention = Intervention.query.get_or_404(id)
old_status = intervention.status
# Les anciennes fiches utilisent ``to_status`` ; accepter aussi ``status``
# pour les formulaires d'édition et conserver une transition réellement modifiable.
new_status = request.form.get('to_status') or request.form.get('status')
comment = request.form.get('comment', '')
allowed = INTERVENTION_TRANSITIONS.get(old_status, set())
workflow_statuses = set(WORKFLOW_STATUS_ORDERS.get(intervention.workflow_type, WORKFLOW_STATUS_ORDERS['corrective'])) | {'refusee', 'annulee', 'reportee'}
if new_status not in workflow_statuses:
flash(f'Le statut « {new_status} » ne correspond pas au workflow {intervention.workflow_label}.', 'danger')
return redirect(url_for('interventions.detail', id=intervention.id))
if new_status != old_status and new_status not in allowed:
flash(f'Transition interdite : {old_status}{new_status}.', 'danger')
return redirect(url_for('interventions.detail', id=intervention.id))
if new_status == 'en_cours':
from ..core.services.planning_service import PlanningService
reason = PlanningService.maintenance_unavailability(
datetime.now(timezone.utc).date(), user_id=current_user.id
)
if reason:
flash(f'Impossible de démarrer une maintenance aujourdhui : {reason}.', 'danger')
return redirect(url_for('interventions.detail', id=intervention.id))
if new_status != old_status:
intervention.status = new_status
status_change = StatusChange(
intervention_id=intervention.id,
from_status=old_status,
to_status=new_status,
changed_by_id=current_user.id,
comment=comment
)
db.session.add(status_change)
if new_status == 'terminee':
now = datetime.now(timezone.utc)
intervention.completed_date = now.date()
intervention.completed_at = now
db.session.commit()
flash('Statut modifié.', 'success')
return redirect(url_for('interventions.detail', id=intervention.id))
@interventions_bp.route('/<int:id>/refuse', methods=['POST'])
@login_required
def refuse(id):
"""Refuse une demande avec un motif obligatoire."""
intervention = Intervention.query.get_or_404(id)
reason = (request.form.get('refusal_reason') or '').strip()
if not reason:
flash('Le motif du refus est obligatoire.', 'danger')
return redirect(url_for('interventions.detail', id=id))
if intervention.status in {'terminee', 'cloturee', 'refusee'}:
flash('Cette intervention ne peut plus être refusée depuis son statut actuel.', 'danger')
return redirect(url_for('interventions.detail', id=id))
old_status = intervention.status
intervention.status = 'refusee'
intervention.refusal_reason = reason
intervention.refused_at = datetime.now(timezone.utc)
intervention.refused_by_id = current_user.id
db.session.add(StatusChange(intervention_id=id, from_status=old_status, to_status='refusee', changed_by_id=current_user.id, comment=reason))
db.session.commit()
flash('Intervention refusée et motif enregistré.', 'success')
return redirect(url_for('interventions.detail', id=id))
@interventions_bp.route('/<int:id>/comment', methods=['POST'])
@login_required
def add_comment(id):
"""Ajouter un commentaire à une intervention."""
intervention = Intervention.query.get_or_404(id)
content = request.form.get('content')
if content:
comment = InterventionComment(
intervention_id=intervention.id,
author_id=current_user.id,
content=content
)
db.session.add(comment)
db.session.commit()
flash('Commentaire ajouté.', 'success')
return redirect(url_for('interventions.detail', id=intervention.id))
@interventions_bp.route('/<int:id>/soft-delete', methods=['POST'])
@login_required
def soft_delete(id):
"""Mettre une intervention en corbeille (soft delete)."""
intervention = Intervention.query.get_or_404(id)
intervention.is_deleted = True
intervention.deleted_at = datetime.now(timezone.utc)
intervention.deleted_by_id = current_user.id
db.session.commit()
flash('Intervention mise en corbeille.', 'success')
return redirect(url_for('interventions.index'))
@interventions_bp.route('/<int:id>/postpone', methods=['POST'])
@login_required
def postpone(id):
"""Reporter une intervention : motif, compteur, conservation date originale."""
intervention = Intervention.query.get_or_404(id)
new_date_str = request.form.get('new_scheduled_date')
reason = request.form.get('reason', '').strip()
if not new_date_str:
flash('Nouvelle date requise.', 'danger')
return redirect(url_for('interventions.detail', id=id))
try:
new_date = datetime.strptime(new_date_str, '%Y-%m-%d').date()
except ValueError:
flash('Format de date invalide.', 'danger')
return redirect(url_for('interventions.detail', id=id))
# Conserver la date originale à la première reprogrammation
if intervention.postpone_count == 0 and intervention.scheduled_date:
intervention.original_scheduled_date = intervention.scheduled_date
intervention.scheduled_date = new_date
intervention.postpone_count += 1
intervention.last_postpone_reason = reason
intervention.last_postpone_at = datetime.now(timezone.utc)
# Commentaire d'historique
comment_text = f"Report #{intervention.postpone_count}"
if intervention.original_scheduled_date:
comment_text += f" (date originale {intervention.original_scheduled_date.strftime('%d/%m/%Y')})"
comment_text += f"{new_date.strftime('%d/%m/%Y')}"
if reason:
comment_text += f"{reason}"
status_change = StatusChange(
intervention_id=intervention.id,
from_status=intervention.status,
to_status='reportee',
changed_by_id=current_user.id,
comment=comment_text
)
db.session.add(status_change)
db.session.commit()
flash(f"Intervention reportée au {new_date.strftime('%d/%m/%Y')}.", 'success')
return redirect(url_for('interventions.detail', id=id))