2026-08-14 18:02:25 +02:00
|
|
|
"""
|
|
|
|
|
Wizard de création d'équipements par lot - GMAO Collège
|
|
|
|
|
"""
|
|
|
|
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app
|
|
|
|
|
from flask_login import login_required
|
|
|
|
|
from app_new.extensions import db
|
|
|
|
|
from app_new.core.models.equipment import Equipment, EquipmentCategory
|
|
|
|
|
from app_new.core.models.maintenance import Lot
|
|
|
|
|
from app_new.core.models.college import Room, Building, Zone, RoomType
|
|
|
|
|
|
|
|
|
|
wizard_bp = Blueprint('wizard', __name__, url_prefix='/wizard', template_folder='templates')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_bp.route('/equipments')
|
|
|
|
|
@login_required
|
|
|
|
|
def equipment_wizard():
|
|
|
|
|
"""Page principale du wizard de création d'équipements."""
|
|
|
|
|
categories = EquipmentCategory.query.order_by(EquipmentCategory.name).all()
|
|
|
|
|
buildings = Building.query.order_by(Building.name).all()
|
|
|
|
|
room_types = RoomType.query.order_by(RoomType.name).all()
|
|
|
|
|
|
|
|
|
|
# Récupérer les lots groupés par catégorie (relation directe)
|
|
|
|
|
lots_by_category = {}
|
|
|
|
|
for cat in categories:
|
|
|
|
|
lots_by_category[cat.id] = [{'id': lot.id, 'name': lot.name} for lot in cat.lots]
|
|
|
|
|
|
|
|
|
|
# Tous les lots pour fallback
|
|
|
|
|
lots = Lot.query.order_by(Lot.name).all()
|
|
|
|
|
|
2026-08-14 23:24:42 +02:00
|
|
|
housing_id = request.args.get('housing_id', type=int)
|
|
|
|
|
selected_room_ids = []
|
|
|
|
|
if housing_id:
|
|
|
|
|
from app_new.core.models.college import HousingUnit
|
|
|
|
|
housing = HousingUnit.query.get_or_404(housing_id)
|
|
|
|
|
selected_room_ids = [room.id for room in housing.rooms]
|
2026-08-14 18:02:25 +02:00
|
|
|
return render_template('wizard/equipment_wizard.html',
|
|
|
|
|
lots=lots,
|
|
|
|
|
categories=categories,
|
|
|
|
|
buildings=buildings,
|
|
|
|
|
room_types=room_types,
|
2026-08-14 23:24:42 +02:00
|
|
|
lots_by_category=lots_by_category,
|
|
|
|
|
selected_room_ids=selected_room_ids)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_bp.route('/equipments/bulk-create', methods=['POST'])
|
|
|
|
|
@login_required
|
|
|
|
|
def bulk_create_equipments():
|
|
|
|
|
"""Crée plusieurs familles d'équipements en une transaction."""
|
|
|
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
|
room_ids = list(dict.fromkeys(data.get('room_ids') or []))
|
|
|
|
|
items = data.get('items') or []
|
|
|
|
|
rooms = Room.query.filter(Room.id.in_(room_ids)).all() if room_ids else []
|
|
|
|
|
if len(rooms) != len(room_ids):
|
|
|
|
|
return jsonify({'error': 'Une ou plusieurs pièces sont invalides.'}), 400
|
|
|
|
|
if not rooms or not items:
|
|
|
|
|
return jsonify({'error': 'Sélectionnez au moins une pièce et un équipement.'}), 400
|
|
|
|
|
|
|
|
|
|
created = []
|
|
|
|
|
try:
|
|
|
|
|
for raw in items:
|
|
|
|
|
name = (raw.get('name') or '').strip()
|
|
|
|
|
category = db.session.get(EquipmentCategory, int(raw.get('category_id') or 0))
|
|
|
|
|
lot = db.session.get(Lot, int(raw.get('lot_id') or 0))
|
|
|
|
|
quantity = int(raw.get('quantity') or 1)
|
|
|
|
|
mode = raw.get('mode', 'quantitatif')
|
|
|
|
|
mobility = raw.get('mobility', 'non_precise')
|
|
|
|
|
if not name or not category or not lot or quantity < 1 or quantity > 1000:
|
|
|
|
|
raise ValueError('Une ligne contient des données invalides.')
|
|
|
|
|
if lot.category_id and lot.category_id != category.id:
|
|
|
|
|
raise ValueError(f'Le lot « {lot.name} » ne correspond pas à la catégorie « {category.name} ».')
|
|
|
|
|
|
|
|
|
|
root = Equipment(
|
|
|
|
|
name=name, category_id=category.id, lot_id=lot.id,
|
|
|
|
|
is_group=True, quantity=quantity * len(rooms), mobility=mobility,
|
|
|
|
|
tracked_individually=(mode == 'individuel'), status='en_service',
|
|
|
|
|
)
|
|
|
|
|
db.session.add(root)
|
|
|
|
|
db.session.flush()
|
|
|
|
|
created.append(root)
|
|
|
|
|
for room in rooms:
|
|
|
|
|
group = Equipment(
|
|
|
|
|
name=name, parent_id=root.id, room_id=room.id, is_group=True,
|
|
|
|
|
quantity=quantity, mobility=mobility, status='en_service',
|
|
|
|
|
tracked_individually=(mode == 'individuel'),
|
|
|
|
|
)
|
|
|
|
|
db.session.add(group)
|
|
|
|
|
db.session.flush()
|
|
|
|
|
created.append(group)
|
|
|
|
|
if mode == 'individuel':
|
|
|
|
|
for number in range(1, quantity + 1):
|
|
|
|
|
unit = Equipment(
|
|
|
|
|
name=f'{name} #{number}', parent_id=group.id, room_id=room.id,
|
|
|
|
|
is_group=False, quantity=1, mobility=mobility,
|
|
|
|
|
tracked_individually=True, individual_number=number,
|
|
|
|
|
status='en_service', position=f'{name} n°{number}',
|
|
|
|
|
)
|
|
|
|
|
db.session.add(unit)
|
|
|
|
|
created.append(unit)
|
|
|
|
|
# Les tâches sont générées sur les unités individualisées, sinon sur
|
|
|
|
|
# les groupes de pièce, jamais sur le parent de synthèse.
|
|
|
|
|
task_targets = [eq for eq in created if (not eq.is_group or (eq.parent_id and not eq.tracked_individually))]
|
|
|
|
|
for equipment in task_targets:
|
|
|
|
|
equipment.create_scheduled_tasks_from_lot(commit=False)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return jsonify({'success': True, 'created_count': len(created), 'families': len(items), 'rooms': len(rooms)})
|
|
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
|
|
db.session.rollback()
|
|
|
|
|
return jsonify({'error': str(exc)}), 400
|
|
|
|
|
except Exception:
|
|
|
|
|
db.session.rollback()
|
|
|
|
|
current_app.logger.exception('Échec de la création massive du patrimoine')
|
|
|
|
|
return jsonify({'error': 'La création a été annulée intégralement.'}), 500
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_bp.route('/api/rooms/<int:building_id>')
|
|
|
|
|
@login_required
|
|
|
|
|
def get_rooms(building_id):
|
|
|
|
|
"""Récupère les salles d'un bâtiment."""
|
|
|
|
|
rooms = Room.query.filter_by(building_id=building_id).order_by(Room.name).all()
|
|
|
|
|
return jsonify([{
|
|
|
|
|
'id': r.id,
|
|
|
|
|
'name': r.name,
|
|
|
|
|
'zone': r.zone.name if r.zone else None
|
|
|
|
|
} for r in rooms])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_bp.route('/api/zones/<int:building_id>')
|
|
|
|
|
@login_required
|
|
|
|
|
def get_zones(building_id):
|
|
|
|
|
"""Récupère les zones d'un bâtiment."""
|
|
|
|
|
zones = Zone.query.filter_by(building_id=building_id).order_by(Zone.name).all()
|
|
|
|
|
return jsonify([{
|
|
|
|
|
'id': z.id,
|
|
|
|
|
'name': z.name
|
|
|
|
|
} for z in zones])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_bp.route('/api/lot_tasks/<int:lot_id>')
|
|
|
|
|
@login_required
|
|
|
|
|
def get_lot_tasks(lot_id):
|
|
|
|
|
"""Récupère les tâches préventives d'un lot."""
|
|
|
|
|
from app_new.core.models.maintenance import LotTask
|
|
|
|
|
|
|
|
|
|
tasks = LotTask.query.filter_by(lot_id=lot_id).order_by(LotTask.num_tache).all()
|
|
|
|
|
return jsonify([{
|
|
|
|
|
'id': t.id,
|
|
|
|
|
'num_tache': t.num_tache,
|
|
|
|
|
'tache': t.tache,
|
|
|
|
|
'periodicite': t.periodicite,
|
|
|
|
|
'contrat': t.contrat,
|
|
|
|
|
'gestionnaire': t.gestionnaire,
|
|
|
|
|
'type_maintenance': t.type_maintenance
|
|
|
|
|
} for t in tasks])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_bp.route('/equipments/create', methods=['POST'])
|
|
|
|
|
@login_required
|
|
|
|
|
def create_equipments():
|
|
|
|
|
"""Crée les équipements selon les paramètres du wizard."""
|
|
|
|
|
try:
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
lot_id = data.get('lot_id')
|
|
|
|
|
category_id = data.get('category_id')
|
|
|
|
|
equipment_name = data.get('name', '').strip() # Nom personnalisé
|
|
|
|
|
parent_name = data.get('parent_name')
|
|
|
|
|
create_parent = data.get('create_parent', False)
|
|
|
|
|
building_id = data.get('building_id')
|
|
|
|
|
zone_id = data.get('zone_id')
|
|
|
|
|
rooms_data = data.get('rooms', []) # [{room_id, count}, ...]
|
|
|
|
|
tracked_individually = data.get('tracked_individually', False)
|
|
|
|
|
|
|
|
|
|
# Debug
|
|
|
|
|
current_app.logger.info(f"Création équipements: tracked_individually={tracked_individually}, rooms={rooms_data}")
|
|
|
|
|
|
|
|
|
|
if not lot_id or not category_id:
|
|
|
|
|
return jsonify({'error': 'Lot et catégorie requis'}), 400
|
|
|
|
|
|
|
|
|
|
if not rooms_data:
|
|
|
|
|
return jsonify({'error': 'Au moins une salle requise'}), 400
|
|
|
|
|
|
|
|
|
|
lot = Lot.query.get(lot_id)
|
|
|
|
|
category = EquipmentCategory.query.get(category_id)
|
|
|
|
|
|
|
|
|
|
# Nom de base: personnalisé ou catégorie
|
|
|
|
|
base_name = equipment_name if equipment_name else category.name
|
|
|
|
|
|
|
|
|
|
created_equipments = []
|
|
|
|
|
|
|
|
|
|
# Étape 1: Créer le parent global (si demandé)
|
|
|
|
|
parent = None
|
|
|
|
|
if create_parent and parent_name:
|
|
|
|
|
parent = Equipment(
|
|
|
|
|
name=parent_name,
|
|
|
|
|
lot_id=lot_id,
|
|
|
|
|
category_id=category_id,
|
|
|
|
|
is_group=True,
|
|
|
|
|
parent_id=None
|
|
|
|
|
)
|
|
|
|
|
db.session.add(parent)
|
|
|
|
|
db.session.flush() # Pour avoir l'ID
|
|
|
|
|
|
|
|
|
|
# Étape 2: Pour chaque salle, créer le groupe et les individuels
|
|
|
|
|
for room_data in rooms_data:
|
|
|
|
|
room_id = room_data.get('room_id')
|
|
|
|
|
count = room_data.get('count', 1)
|
|
|
|
|
|
|
|
|
|
room = Room.query.get(room_id)
|
|
|
|
|
if not room:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if tracked_individually:
|
|
|
|
|
# Avec suivi individuel: créer un groupe + des individus
|
|
|
|
|
room_group = Equipment(
|
|
|
|
|
name=f"{base_name} {room.name}",
|
|
|
|
|
lot_id=lot_id if not parent else None,
|
|
|
|
|
category_id=category_id if not parent else None,
|
|
|
|
|
is_group=True,
|
|
|
|
|
parent_id=parent.id if parent else None,
|
|
|
|
|
room_id=room_id,
|
|
|
|
|
quantity=1
|
|
|
|
|
)
|
|
|
|
|
db.session.add(room_group)
|
|
|
|
|
db.session.flush()
|
|
|
|
|
|
|
|
|
|
created_equipments.append({
|
|
|
|
|
'type': 'group',
|
|
|
|
|
'name': room_group.name,
|
|
|
|
|
'room': room.name,
|
|
|
|
|
'quantity': 1
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Créer les équipements individuels
|
|
|
|
|
if count > 1:
|
|
|
|
|
for i in range(1, count + 1):
|
|
|
|
|
individual = Equipment(
|
|
|
|
|
name=f"{base_name} {i}",
|
|
|
|
|
lot_id=lot_id, # Hériter du lot
|
|
|
|
|
category_id=category_id, # Hériter de la catégorie
|
|
|
|
|
is_group=False,
|
|
|
|
|
tracked_individually=True,
|
|
|
|
|
parent_id=room_group.id,
|
|
|
|
|
room_id=room_id,
|
|
|
|
|
position=f"{base_name} n°{i}"
|
|
|
|
|
)
|
|
|
|
|
db.session.add(individual)
|
|
|
|
|
|
|
|
|
|
created_equipments.append({
|
|
|
|
|
'type': 'individual',
|
|
|
|
|
'name': individual.name,
|
|
|
|
|
'room': room.name
|
|
|
|
|
})
|
|
|
|
|
else:
|
|
|
|
|
# Sans suivi individuel: créer un seul équipement avec quantity
|
|
|
|
|
equipment = Equipment(
|
|
|
|
|
name=f"{base_name} {room.name}",
|
|
|
|
|
lot_id=lot_id if not parent else None,
|
|
|
|
|
category_id=category_id if not parent else None,
|
|
|
|
|
is_group=False,
|
|
|
|
|
parent_id=parent.id if parent else None,
|
|
|
|
|
room_id=room_id,
|
|
|
|
|
quantity=count
|
|
|
|
|
)
|
|
|
|
|
db.session.add(equipment)
|
|
|
|
|
db.session.flush()
|
|
|
|
|
|
|
|
|
|
created_equipments.append({
|
|
|
|
|
'type': 'equipment',
|
|
|
|
|
'name': equipment.name,
|
|
|
|
|
'room': room.name,
|
|
|
|
|
'quantity': count
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
# Créer les tâches planifiées pour tous les équipements créés
|
|
|
|
|
for eq in created_equipments:
|
|
|
|
|
# Récupérer l'équipement depuis la DB pour avoir son ID
|
|
|
|
|
equipment = Equipment.query.filter_by(name=eq['name']).first()
|
|
|
|
|
if equipment and equipment.effective_lot_id:
|
|
|
|
|
equipment.create_scheduled_tasks_from_lot()
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
'success': True,
|
|
|
|
|
'count': len(created_equipments),
|
|
|
|
|
'equipments': created_equipments
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
db.session.rollback()
|
|
|
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@wizard_bp.route('/equipments/preview', methods=['POST'])
|
|
|
|
|
@login_required
|
|
|
|
|
def preview_equipments():
|
|
|
|
|
"""Prévisualise les équipements qui seront créés."""
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
lot_id = data.get('lot_id')
|
|
|
|
|
category_id = data.get('category_id')
|
|
|
|
|
parent_name = data.get('parent_name')
|
|
|
|
|
create_parent = data.get('create_parent', False)
|
|
|
|
|
rooms_data = data.get('rooms', [])
|
|
|
|
|
tracked_individually = data.get('tracked_individually', False)
|
|
|
|
|
|
|
|
|
|
lot = Lot.query.get(lot_id) if lot_id else None
|
|
|
|
|
category = EquipmentCategory.query.get(category_id) if category_id else None
|
|
|
|
|
|
|
|
|
|
preview = []
|
|
|
|
|
|
|
|
|
|
# Parent global
|
|
|
|
|
if create_parent and parent_name:
|
|
|
|
|
preview.append({
|
|
|
|
|
'level': 0,
|
|
|
|
|
'name': parent_name,
|
|
|
|
|
'type': 'Groupe parent',
|
|
|
|
|
'lot': lot.name if lot else '-',
|
|
|
|
|
'category': category.name if category else '-',
|
|
|
|
|
'room': '-'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Par salle
|
|
|
|
|
for room_data in rooms_data:
|
|
|
|
|
room_id = room_data.get('room_id')
|
|
|
|
|
count = room_data.get('count', 1)
|
|
|
|
|
|
|
|
|
|
room = Room.query.get(room_id)
|
|
|
|
|
if not room:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Groupe salle
|
|
|
|
|
level = 1 if create_parent else 0
|
|
|
|
|
preview.append({
|
|
|
|
|
'level': level,
|
|
|
|
|
'name': f"{category.name} {room.name}",
|
|
|
|
|
'type': 'Groupe salle',
|
|
|
|
|
'lot': lot.name if lot else '-',
|
|
|
|
|
'category': category.name if category else '-',
|
|
|
|
|
'room': room.name
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Individuels
|
|
|
|
|
if tracked_individually:
|
|
|
|
|
for i in range(1, count + 1):
|
|
|
|
|
preview.append({
|
|
|
|
|
'level': level + 1,
|
|
|
|
|
'name': f"{category.name} {i}",
|
|
|
|
|
'type': 'Individuel',
|
|
|
|
|
'lot': '(hérité)',
|
|
|
|
|
'category': '(hérité)',
|
|
|
|
|
'room': room.name
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
'preview': preview,
|
|
|
|
|
'total': len(preview)
|
2026-08-14 23:24:42 +02:00
|
|
|
})
|