feat(equipment): centraliser la creation des equipements

This commit is contained in:
root 2026-08-23 15:57:29 +00:00
parent 2fa13d47df
commit c4bfef10b8
7 changed files with 128 additions and 55 deletions

View file

@ -266,7 +266,7 @@ class Intervention(db.Model):
).scalar()
if avg_lot and avg_lot > 0:
return round(avg_lot, 1)
return 60.0 # valeur par defaut (1h)
return 0
@property
def is_overdue(self):

View file

@ -569,7 +569,7 @@ class ScheduledTask(db.Model):
equipment_id=self.equipment_id,
room_id=self.room_id,
scheduled_date=self.scheduled_date,
estimated_duration=float(self.estimated_duration or 60),
estimated_duration=float(self.estimated_duration or 0),
)
db.session.add(interv)
db.session.flush()

View file

@ -318,7 +318,8 @@ class DayPlanner:
description=intervention.description or "", task_type="emergency" if intervention.priority == "urgente" else "curative",
priority=1 if intervention.priority == "urgente" else 3,
constraint="urgent" if intervention.priority == "urgente" else ("fixed" if intervention.scheduled_start and intervention.scheduled_end else "flexible"),
target_date=day, duration_minutes=int(intervention.estimated_duration or 30),
target_date=day,
duration_minutes=int(intervention.estimated_duration or intervention.effective_estimated_duration or 0),
room_id=intervention.room_id, room_name=room.name if room else None,
zone_name=room.zone.name if room and room.zone else None,
building_name=room.building.name if room and room.building else None,

View file

@ -0,0 +1,52 @@
"""Service métier unique pour créer des équipements.
Les formulaires historiques (fiche avancée et wizard) délèguent ici la
construction de l'objet. Le lot reste facultatif : un inventaire incomplet est
valide et pourra être complété plus tard.
"""
from datetime import date
from ...extensions import db
from ..models.equipment import Equipment
def create_equipment(*, name, category_id=None, lot_id=None, room_id=None,
quantity=1, is_group=False, parent_id=None,
tracked_individually=False, mobility="non_precise",
status="en_service", code=None, description=None,
management_mode=None, serial_number=None,
manufacturer=None, model_reference=None,
supplier_id=None, supplier_reference=None,
purchase_price=None, purchase_date=None,
recurrence_monitoring=False, position=None,
individual_number=None, create_schedule=True):
"""Construit, ajoute et flush un équipement sans recherche par nom.
Retourne l'instance persistable et son identifiant est disponible dès le
retour. La génération préventive est déclenchée uniquement si un lot est
effectivement renseigné.
"""
name = (name or "").strip()
if not name:
raise ValueError("Le nom de l'équipement est obligatoire.")
quantity = int(quantity or 1)
if quantity < 1:
raise ValueError("La quantité doit être positive.")
equipment = Equipment(
name=name, category_id=category_id or None, lot_id=lot_id or None,
room_id=room_id or None, quantity=quantity, is_group=bool(is_group),
parent_id=parent_id or None,
tracked_individually=bool(tracked_individually), mobility=mobility or "non_precise",
status=status or "en_service", code=code or None,
description=description or None, management_mode=management_mode or None,
serial_number=serial_number or None, manufacturer=manufacturer or None,
model_reference=model_reference or None, supplier_id=supplier_id or None,
supplier_reference=supplier_reference or None, purchase_price=purchase_price or None,
purchase_date=purchase_date, recurrence_monitoring=bool(recurrence_monitoring),
position=position or None, individual_number=individual_number,
)
db.session.add(equipment)
db.session.flush()
if create_schedule and equipment.lot_id:
equipment.create_scheduled_tasks_from_lot(commit=False)
return equipment

View file

@ -16,6 +16,7 @@ from ..core.services.equipment_lifecycle import (
confirm_disposal, propose_disposal, record_event, start_repair as start_equipment_repair,
update_future_task_location,
)
from ..core.services.equipment_creation import create_equipment
from ..core.authorization import permission_required
def _empty_to_none(v):
@ -265,17 +266,14 @@ def create():
if not category_id or not lot or lot.category_id != category_id:
flash('Le lot sélectionné doit appartenir à la catégorie choisie.', 'danger')
return redirect(url_for('equipments.create', advanced=1)), 400
equipment = Equipment(
name=request.form.get('name'),
category_id=category_id,
room_id=request.form.get('room_id') or None,
lot_id=lot_id,
equipment = create_equipment(
name=request.form.get('name'), category_id=category_id,
room_id=request.form.get('room_id') or None, lot_id=lot_id,
code=_empty_to_none(request.form.get('code', '').strip()),
description=_empty_to_none(request.form.get('description', '').strip()),
status=request.form.get('status', 'en_service'),
is_group=request.form.get('is_group') == '1',
quantity=int(request.form.get('quantity', 1) or 1),
parent_id=parent_id,
quantity=int(request.form.get('quantity', 1) or 1), parent_id=parent_id,
mobility=request.form.get('mobility', 'non_precise'),
management_mode=request.form.get('management_mode') or None,
serial_number=_empty_to_none(request.form.get('serial_number', '').strip()),
@ -287,13 +285,8 @@ def create():
purchase_date=_date_or_none(request.form.get('purchase_date')),
recurrence_monitoring=request.form.get('recurrence_monitoring') == '1',
)
db.session.add(equipment)
db.session.commit()
# Créer les tâches planifiées si l'équipement a un lot
if equipment.lot_id:
equipment.create_scheduled_tasks_from_lot()
flash('Équipement créé avec succès.', 'success')
return redirect(url_for('equipments.detail', id=equipment.id))

View file

@ -7,6 +7,7 @@ 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
from app_new.core.services.equipment_creation import create_equipment
wizard_bp = Blueprint('wizard', __name__, url_prefix='/wizard', template_folder='templates')
@ -91,42 +92,43 @@ def bulk_create_equipments():
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))
category_raw = raw.get('category_id')
lot_raw = raw.get('lot_id')
category = db.session.get(EquipmentCategory, int(category_raw)) if category_raw not in (None, '') else None
lot = db.session.get(Lot, int(lot_raw)) if lot_raw not in (None, '') else None
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:
if not name or quantity < 1 or quantity > 1000 or (category_raw not in (None, '') and not category) or (lot_raw not in (None, '') and not lot):
raise ValueError('Une ligne contient des données invalides.')
if lot.category_id != category.id:
if lot and category 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,
root = create_equipment(
name=name, category_id=category.id if category else None,
lot_id=lot.id if lot else None, is_group=True,
quantity=quantity * len(rooms), mobility=mobility,
tracked_individually=(mode == 'individuel'), status='en_service',
create_schedule=False,
)
db.session.add(root)
db.session.flush()
created.append(root)
for room in rooms:
group = Equipment(
group = create_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'),
create_schedule=False,
)
db.session.add(group)
db.session.flush()
created.append(group)
if mode == 'individuel':
for number in range(1, quantity + 1):
unit = Equipment(
unit = create_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}{number}',
create_schedule=False,
)
db.session.add(unit)
created.append(unit)
# Valider d'abord le patrimoine. La génération des échéances est
# volontairement découplée : une anomalie dans un lot ne doit jamais
@ -219,32 +221,34 @@ def create_equipments():
# 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 category_id and not db.session.get(EquipmentCategory, category_id):
return jsonify({'error': 'Catégorie invalide'}), 400
if lot_id and not db.session.get(Lot, lot_id):
return jsonify({'error': 'Lot invalide'}), 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)
lot = db.session.get(Lot, lot_id) if lot_id else None
category = db.session.get(EquipmentCategory, category_id) if category_id else None
# Nom de base: personnalisé ou catégorie
base_name = equipment_name if equipment_name else category.name
base_name = equipment_name if equipment_name else (category.name if category else 'Équipement')
created_equipments = []
created_instances = []
# Étape 1: Créer le parent global (si demandé)
parent = None
if create_parent and parent_name:
parent = Equipment(
parent = create_equipment(
name=parent_name,
lot_id=lot_id,
category_id=category_id,
is_group=True,
parent_id=None
parent_id=None, create_schedule=False,
)
db.session.add(parent)
db.session.flush() # Pour avoir l'ID
created_instances.append(parent)
# Étape 2: Pour chaque salle, créer le groupe et les individuels
for room_data in rooms_data:
@ -257,17 +261,16 @@ def create_equipments():
if tracked_individually:
# Avec suivi individuel: créer un groupe + des individus
room_group = Equipment(
room_group = create_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
quantity=1, create_schedule=False,
)
db.session.add(room_group)
db.session.flush()
created_instances.append(room_group)
created_equipments.append({
'type': 'group',
@ -279,7 +282,7 @@ def create_equipments():
# Créer les équipements individuels
if count > 1:
for i in range(1, count + 1):
individual = Equipment(
individual = create_equipment(
name=f"{base_name} {i}",
lot_id=lot_id, # Hériter du lot
category_id=category_id, # Hériter de la catégorie
@ -287,9 +290,9 @@ def create_equipments():
tracked_individually=True,
parent_id=room_group.id,
room_id=room_id,
position=f"{base_name}{i}"
position=f"{base_name}{i}", create_schedule=False,
)
db.session.add(individual)
created_instances.append(individual)
created_equipments.append({
'type': 'individual',
@ -298,17 +301,16 @@ def create_equipments():
})
else:
# Sans suivi individuel: créer un seul équipement avec quantity
equipment = Equipment(
equipment = create_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
quantity=count, create_schedule=False,
)
db.session.add(equipment)
db.session.flush()
created_instances.append(equipment)
created_equipments.append({
'type': 'equipment',
@ -318,14 +320,13 @@ def create_equipments():
})
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:
for equipment in created_instances:
if equipment.effective_lot_id:
equipment.create_scheduled_tasks_from_lot()
# Les instances créées sont conservées en mémoire : aucune recherche
# fragile par nom (deux équipements peuvent porter le même libellé).
return jsonify({
'success': True,
'count': len(created_equipments),

View file

@ -0,0 +1,26 @@
from app_new import db
from app_new.core.models.equipment import Equipment
from app_new.core.services.equipment_creation import create_equipment
def test_equipment_creation_accepts_missing_lot_and_preserves_identity(app):
with app.app_context():
equipment = create_equipment(
name="TEST_UI_020_EQUIPMENT_NO_LOT",
quantity=12,
is_group=True,
create_schedule=False,
)
db.session.commit()
assert equipment.id is not None
assert equipment.lot_id is None
assert equipment.quantity == 12
assert db.session.get(Equipment, equipment.id) is equipment
def test_equipment_creation_returns_distinct_objects_for_duplicate_names(app):
with app.app_context():
first = create_equipment(name="TEST_UI_020_DUPLICATE", create_schedule=False)
second = create_equipment(name="TEST_UI_020_DUPLICATE", create_schedule=False)
db.session.commit()
assert first.id != second.id