gmao/app_new/lib_ext/utils.py

267 lines
10 KiB
Python
Raw Permalink Normal View History

2026-08-14 18:02:25 +02:00
from flask import current_app, session
from app_new.extensions import db
from app_new.core.models import Alert, Company, Equipment, Part, Intervention, INTERVENTION_STATUSES, User
import json
from datetime import datetime, timezone, timedelta
def format_datetime(value):
"""Formate une date/heure pour l'affichage"""
if not value:
return ""
try:
return value.strftime('%d/%m/%Y %H:%M')
except:
return str(value)
def format_date(value):
"""Formate une date pour l'affichage"""
if not value:
return ""
try:
return value.strftime('%d/%m/%Y')
except:
return str(value)
def get_user_session():
"""Récupérer l'utilisateur de la session"""
if 'user_id' in session:
return User.query.get(session['user_id'])
return None
def requires_permission(user, permission):
"""Vérifier si l'utilisateur a une permission"""
if not user:
return False
# Compatibilité pour les anciens appelants : la décision est déléguée au
# RBAC SQL centralisé et ne lit plus User.role.
from app_new.core.authorization import has_permission
normalized = permission.replace(':', '.').replace('_', '.')
return has_permission(normalized, user)
2026-08-14 18:02:25 +02:00
def log_action(user, action, data=None):
"""Loguer une action utilisateur"""
try:
# Log simple pour l'instant
current_app.logger.info(f"User action: {user.username if user else 'anonymous'} - {action} - {data}")
except Exception as e:
current_app.logger.error(f"Error logging action: {str(e)}")
def log_user_action(user, action, data=None):
"""Loguer une action utilisateur avec plus de détails"""
try:
log_data = {
'user_id': user.id if user else None,
'username': user.username if user else 'anonymous',
'action': action,
'data': data,
'timestamp': datetime.now(timezone.utc).isoformat()
}
current_app.logger.info(f"User action: {json.dumps(log_data)}")
except Exception as e:
current_app.logger.error(f"Error logging user action: {str(e)}")
def generate_alerts():
"""Génère les alertes automatiques : récurrence, stock bas, échéances préventif, workflow."""
new_alerts = []
# 1) Alertes récurrence : 3+ interventions curatives sur un même équipement
equipments = Equipment.query.all()
for eq in equipments:
curative_count = eq.interventions.filter_by(type="curatif").count()
existing = Alert.query.filter_by(
equipment_id=eq.id, type="recurrence_equipement"
).first()
if curative_count >= 3:
msg = (f"⚠️ {eq.name} : {curative_count} interventions curatives. "
f"Travaux profonds recommandés.")
if existing:
existing.message = msg
existing.severity = "critical"
else:
alert = Alert(type="recurrence_equipement", severity="critical",
message=msg, equipment_id=eq.id)
new_alerts.append(alert)
elif existing:
db.session.delete(existing)
# 2) Alertes stock bas
parts = Part.query.all()
for part in parts:
if part.is_low_stock:
existing = Alert.query.filter_by(
part_id=part.id, type="stock_bas"
).first()
msg = f"📦 Stock bas : {part.name}{part.quantity}/{part.min_quantity} {part.unit}"
if existing:
existing.message = msg
existing.severity = "warning"
else:
alert = Alert(type="stock_bas", severity="warning",
message=msg, part_id=part.id)
new_alerts.append(alert)
# 3) Alertes échéances préventif (uniquement si échéance dans les 7 prochains jours)
now = datetime.now(timezone.utc)
week_later = now + timedelta(days=7)
upcoming = Intervention.query.filter(
Intervention.type == "preventif",
Intervention.is_recurring == True,
Intervention.next_due_date != None,
Intervention.next_due_date <= week_later,
Intervention.status.in_(["brouillon", "en_cours", "attente_chef"]),
).all()
for interv in upcoming:
existing = Alert.query.filter_by(
intervention_id=interv.id, type="preventive_echeance"
).first()
if not existing:
msg = (f"📅 Maintenance préventive à échéance : {interv.title} "
f"(équipement : {interv.equipment.name})")
alert = Alert(type="preventive_echeance", severity="info",
message=msg, intervention_id=interv.id,
equipment_id=interv.equipment_id)
new_alerts.append(alert)
# 4) Alertes interventions en attente chef depuis +3 jours
three_days_ago = datetime.now(timezone.utc) - timedelta(days=3)
pending_chef = Intervention.query.filter(
Intervention.status == "attente_chef",
Intervention.requested_at <= three_days_ago
).all()
for interv in pending_chef:
existing = Alert.query.filter_by(
intervention_id=interv.id, type="attente_chef"
).first()
if not existing:
msg = (f"⏳ Intervention #{interv.id} en attente du chef depuis +3 jours : {interv.title}")
alert = Alert(type="attente_chef", severity="warning",
message=msg, intervention_id=interv.id)
new_alerts.append(alert)
# 5) Alertes interventions en demande GIMA depuis +7 jours
seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7)
pending_gima = Intervention.query.filter(
Intervention.status == "demande_gima",
Intervention.updated_at <= seven_days_ago
).all()
for interv in pending_gima:
existing = Alert.query.filter_by(
intervention_id=interv.id, type="gima_relance"
).first()
if not existing:
msg = (f"🏛️ Relance GIMA : Intervention #{interv.id} en demande GIMA depuis +7 jours : {interv.title}")
alert = Alert(type="gima_relance", severity="info",
message=msg, intervention_id=interv.id)
new_alerts.append(alert)
db.session.add_all(new_alerts)
db.session.commit()
return new_alerts
def get_dashboard_stats():
"""Statistiques pour le tableau de bord."""
stats = {}
stats["interventions_total"] = Intervention.query.count()
# Compter par statut du nouveau workflow
for status_key in INTERVENTION_STATUSES:
stats[f"interventions_{status_key}"] = Intervention.query.filter_by(status=status_key).count()
# Stats rapides pour les cartes
stats["interventions_en_attente_chef"] = Intervention.query.filter_by(status="attente_chef").count()
stats["interventions_en_cours"] = Intervention.query.filter_by(status="en_cours").count()
stats["interventions_demande_gima"] = Intervention.query.filter_by(status="demande_gima").count()
stats["interventions_curatif"] = Intervention.query.filter_by(type="curatif").count()
stats["interventions_preventif"] = Intervention.query.filter_by(type="preventif").count()
stats["interventions_recurrentes"] = Intervention.query.filter_by(is_recurring=True).count()
# Échéances préventif
now = datetime.now(timezone.utc)
week_later = now + timedelta(days=7)
month_later = now + timedelta(days=30)
stats["preventif_overdue"] = Intervention.query.filter(
Intervention.is_recurring == True,
Intervention.next_due_date < now
).count()
stats["preventif_this_week"] = Intervention.query.filter(
Intervention.is_recurring == True,
Intervention.next_due_date >= now,
Intervention.next_due_date <= week_later
).count()
stats["preventif_this_month"] = Intervention.query.filter(
Intervention.is_recurring == True,
Intervention.next_due_date > week_later,
Intervention.next_due_date <= month_later
).count()
stats["equipments_total"] = Equipment.query.count()
stats["equipments_hors_service"] = Equipment.query.filter(
Equipment.status.in_(["hors_service", "out_of_service"])
).count()
stats["parts_low_stock"] = Part.query.filter(Part.quantity <= Part.min_quantity).count()
stats["alerts_unread"] = Alert.query.filter_by(is_read=False).count()
stats["alerts_total"] = Alert.query.count()
stats["entreprises_actives"] = Company.query.filter_by(is_active=True).count()
return stats
def format_status(status):
"""Convertit le statut technique en label lisible."""
from app_new.core.models import INTERVENTION_STATUSES, EQUIPMENT_STATUSES
if status in INTERVENTION_STATUSES:
return INTERVENTION_STATUSES[status]["label"]
if status in EQUIPMENT_STATUSES:
return EQUIPMENT_STATUSES[status]["label"]
# Fallback pour statuts inconnus
mapping = {
"en_service": "En service",
"hors_service": "Hors service",
"en_reparation": "En réparation",
"remplace": "Remplacé",
}
return mapping.get(status, status)
def status_badge_class(status):
"""Retourne la classe CSS Bootstrap pour un statut."""
from app_new.core.models import INTERVENTION_STATUSES, EQUIPMENT_STATUSES
if status in INTERVENTION_STATUSES:
color = INTERVENTION_STATUSES[status].get("color", "secondary")
if color == "purple":
return "bg-purple"
return f"bg-{color}"
if status in EQUIPMENT_STATUSES:
color = EQUIPMENT_STATUSES[status].get("color", "secondary")
return f"bg-{color}"
mapping = {
"en_service": "bg-success",
"hors_service": "bg-danger",
"en_reparation": "bg-warning text-dark",
"remplace": "bg-secondary",
}
return mapping.get(status, "bg-secondary")
def priority_badge_class(priority):
"""Retourne la classe CSS Bootstrap pour une priorité."""
from app_new.core.models import PRIORITIES
if priority in PRIORITIES:
return f"bg-{PRIORITIES[priority]['color']}"
mapping = {
"basse": "bg-light text-dark",
"normale": "bg-info",
"haute": "bg-warning text-dark",
"urgente": "bg-danger",
}
return mapping.get(priority, "bg-secondary")