feat(equipment): centraliser la creation des equipements
This commit is contained in:
parent
2fa13d47df
commit
c4bfef10b8
7 changed files with 128 additions and 55 deletions
|
|
@ -266,7 +266,7 @@ class Intervention(db.Model):
|
||||||
).scalar()
|
).scalar()
|
||||||
if avg_lot and avg_lot > 0:
|
if avg_lot and avg_lot > 0:
|
||||||
return round(avg_lot, 1)
|
return round(avg_lot, 1)
|
||||||
return 60.0 # valeur par defaut (1h)
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_overdue(self):
|
def is_overdue(self):
|
||||||
|
|
|
||||||
|
|
@ -569,7 +569,7 @@ class ScheduledTask(db.Model):
|
||||||
equipment_id=self.equipment_id,
|
equipment_id=self.equipment_id,
|
||||||
room_id=self.room_id,
|
room_id=self.room_id,
|
||||||
scheduled_date=self.scheduled_date,
|
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.add(interv)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,8 @@ class DayPlanner:
|
||||||
description=intervention.description or "", task_type="emergency" if intervention.priority == "urgente" else "curative",
|
description=intervention.description or "", task_type="emergency" if intervention.priority == "urgente" else "curative",
|
||||||
priority=1 if intervention.priority == "urgente" else 3,
|
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"),
|
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,
|
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,
|
zone_name=room.zone.name if room and room.zone else None,
|
||||||
building_name=room.building.name if room and room.building else None,
|
building_name=room.building.name if room and room.building else None,
|
||||||
|
|
|
||||||
52
app_new/core/services/equipment_creation.py
Normal file
52
app_new/core/services/equipment_creation.py
Normal 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
|
||||||
|
|
@ -16,6 +16,7 @@ from ..core.services.equipment_lifecycle import (
|
||||||
confirm_disposal, propose_disposal, record_event, start_repair as start_equipment_repair,
|
confirm_disposal, propose_disposal, record_event, start_repair as start_equipment_repair,
|
||||||
update_future_task_location,
|
update_future_task_location,
|
||||||
)
|
)
|
||||||
|
from ..core.services.equipment_creation import create_equipment
|
||||||
from ..core.authorization import permission_required
|
from ..core.authorization import permission_required
|
||||||
|
|
||||||
def _empty_to_none(v):
|
def _empty_to_none(v):
|
||||||
|
|
@ -265,17 +266,14 @@ def create():
|
||||||
if not category_id or not lot or lot.category_id != category_id:
|
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')
|
flash('Le lot sélectionné doit appartenir à la catégorie choisie.', 'danger')
|
||||||
return redirect(url_for('equipments.create', advanced=1)), 400
|
return redirect(url_for('equipments.create', advanced=1)), 400
|
||||||
equipment = Equipment(
|
equipment = create_equipment(
|
||||||
name=request.form.get('name'),
|
name=request.form.get('name'), category_id=category_id,
|
||||||
category_id=category_id,
|
room_id=request.form.get('room_id') or None, lot_id=lot_id,
|
||||||
room_id=request.form.get('room_id') or None,
|
|
||||||
lot_id=lot_id,
|
|
||||||
code=_empty_to_none(request.form.get('code', '').strip()),
|
code=_empty_to_none(request.form.get('code', '').strip()),
|
||||||
description=_empty_to_none(request.form.get('description', '').strip()),
|
description=_empty_to_none(request.form.get('description', '').strip()),
|
||||||
status=request.form.get('status', 'en_service'),
|
status=request.form.get('status', 'en_service'),
|
||||||
is_group=request.form.get('is_group') == '1',
|
is_group=request.form.get('is_group') == '1',
|
||||||
quantity=int(request.form.get('quantity', 1) or 1),
|
quantity=int(request.form.get('quantity', 1) or 1), parent_id=parent_id,
|
||||||
parent_id=parent_id,
|
|
||||||
mobility=request.form.get('mobility', 'non_precise'),
|
mobility=request.form.get('mobility', 'non_precise'),
|
||||||
management_mode=request.form.get('management_mode') or None,
|
management_mode=request.form.get('management_mode') or None,
|
||||||
serial_number=_empty_to_none(request.form.get('serial_number', '').strip()),
|
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')),
|
purchase_date=_date_or_none(request.form.get('purchase_date')),
|
||||||
recurrence_monitoring=request.form.get('recurrence_monitoring') == '1',
|
recurrence_monitoring=request.form.get('recurrence_monitoring') == '1',
|
||||||
)
|
)
|
||||||
db.session.add(equipment)
|
|
||||||
db.session.commit()
|
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')
|
flash('Équipement créé avec succès.', 'success')
|
||||||
return redirect(url_for('equipments.detail', id=equipment.id))
|
return redirect(url_for('equipments.detail', id=equipment.id))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from app_new.extensions import db
|
||||||
from app_new.core.models.equipment import Equipment, EquipmentCategory
|
from app_new.core.models.equipment import Equipment, EquipmentCategory
|
||||||
from app_new.core.models.maintenance import Lot
|
from app_new.core.models.maintenance import Lot
|
||||||
from app_new.core.models.college import Room, Building, Zone, RoomType
|
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')
|
wizard_bp = Blueprint('wizard', __name__, url_prefix='/wizard', template_folder='templates')
|
||||||
|
|
||||||
|
|
@ -91,42 +92,43 @@ def bulk_create_equipments():
|
||||||
try:
|
try:
|
||||||
for raw in items:
|
for raw in items:
|
||||||
name = (raw.get('name') or '').strip()
|
name = (raw.get('name') or '').strip()
|
||||||
category = db.session.get(EquipmentCategory, int(raw.get('category_id') or 0))
|
category_raw = raw.get('category_id')
|
||||||
lot = db.session.get(Lot, int(raw.get('lot_id') or 0))
|
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)
|
quantity = int(raw.get('quantity') or 1)
|
||||||
mode = raw.get('mode', 'quantitatif')
|
mode = raw.get('mode', 'quantitatif')
|
||||||
mobility = raw.get('mobility', 'non_precise')
|
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.')
|
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} ».')
|
raise ValueError(f'Le lot « {lot.name} » ne correspond pas à la catégorie « {category.name} ».')
|
||||||
|
|
||||||
root = Equipment(
|
root = create_equipment(
|
||||||
name=name, category_id=category.id, lot_id=lot.id,
|
name=name, category_id=category.id if category else None,
|
||||||
is_group=True, quantity=quantity * len(rooms), mobility=mobility,
|
lot_id=lot.id if lot else None, is_group=True,
|
||||||
|
quantity=quantity * len(rooms), mobility=mobility,
|
||||||
tracked_individually=(mode == 'individuel'), status='en_service',
|
tracked_individually=(mode == 'individuel'), status='en_service',
|
||||||
|
create_schedule=False,
|
||||||
)
|
)
|
||||||
db.session.add(root)
|
|
||||||
db.session.flush()
|
|
||||||
created.append(root)
|
created.append(root)
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
group = Equipment(
|
group = create_equipment(
|
||||||
name=name, parent_id=root.id, room_id=room.id, is_group=True,
|
name=name, parent_id=root.id, room_id=room.id, is_group=True,
|
||||||
quantity=quantity, mobility=mobility, status='en_service',
|
quantity=quantity, mobility=mobility, status='en_service',
|
||||||
tracked_individually=(mode == 'individuel'),
|
tracked_individually=(mode == 'individuel'),
|
||||||
|
create_schedule=False,
|
||||||
)
|
)
|
||||||
db.session.add(group)
|
|
||||||
db.session.flush()
|
|
||||||
created.append(group)
|
created.append(group)
|
||||||
if mode == 'individuel':
|
if mode == 'individuel':
|
||||||
for number in range(1, quantity + 1):
|
for number in range(1, quantity + 1):
|
||||||
unit = Equipment(
|
unit = create_equipment(
|
||||||
name=f'{name} #{number}', parent_id=group.id, room_id=room.id,
|
name=f'{name} #{number}', parent_id=group.id, room_id=room.id,
|
||||||
is_group=False, quantity=1, mobility=mobility,
|
is_group=False, quantity=1, mobility=mobility,
|
||||||
tracked_individually=True, individual_number=number,
|
tracked_individually=True, individual_number=number,
|
||||||
status='en_service', position=f'{name} n°{number}',
|
status='en_service', position=f'{name} n°{number}',
|
||||||
|
create_schedule=False,
|
||||||
)
|
)
|
||||||
db.session.add(unit)
|
|
||||||
created.append(unit)
|
created.append(unit)
|
||||||
# Valider d'abord le patrimoine. La génération des échéances est
|
# 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
|
# volontairement découplée : une anomalie dans un lot ne doit jamais
|
||||||
|
|
@ -219,32 +221,34 @@ def create_equipments():
|
||||||
# Debug
|
# Debug
|
||||||
current_app.logger.info(f"Création équipements: tracked_individually={tracked_individually}, rooms={rooms_data}")
|
current_app.logger.info(f"Création équipements: tracked_individually={tracked_individually}, rooms={rooms_data}")
|
||||||
|
|
||||||
if not lot_id or not category_id:
|
if category_id and not db.session.get(EquipmentCategory, category_id):
|
||||||
return jsonify({'error': 'Lot et catégorie requis'}), 400
|
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:
|
if not rooms_data:
|
||||||
return jsonify({'error': 'Au moins une salle requise'}), 400
|
return jsonify({'error': 'Au moins une salle requise'}), 400
|
||||||
|
|
||||||
lot = Lot.query.get(lot_id)
|
lot = db.session.get(Lot, lot_id) if lot_id else None
|
||||||
category = EquipmentCategory.query.get(category_id)
|
category = db.session.get(EquipmentCategory, category_id) if category_id else None
|
||||||
|
|
||||||
# Nom de base: personnalisé ou catégorie
|
# 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_equipments = []
|
||||||
|
created_instances = []
|
||||||
|
|
||||||
# Étape 1: Créer le parent global (si demandé)
|
# Étape 1: Créer le parent global (si demandé)
|
||||||
parent = None
|
parent = None
|
||||||
if create_parent and parent_name:
|
if create_parent and parent_name:
|
||||||
parent = Equipment(
|
parent = create_equipment(
|
||||||
name=parent_name,
|
name=parent_name,
|
||||||
lot_id=lot_id,
|
lot_id=lot_id,
|
||||||
category_id=category_id,
|
category_id=category_id,
|
||||||
is_group=True,
|
is_group=True,
|
||||||
parent_id=None
|
parent_id=None, create_schedule=False,
|
||||||
)
|
)
|
||||||
db.session.add(parent)
|
created_instances.append(parent)
|
||||||
db.session.flush() # Pour avoir l'ID
|
|
||||||
|
|
||||||
# Étape 2: Pour chaque salle, créer le groupe et les individuels
|
# Étape 2: Pour chaque salle, créer le groupe et les individuels
|
||||||
for room_data in rooms_data:
|
for room_data in rooms_data:
|
||||||
|
|
@ -257,17 +261,16 @@ def create_equipments():
|
||||||
|
|
||||||
if tracked_individually:
|
if tracked_individually:
|
||||||
# Avec suivi individuel: créer un groupe + des individus
|
# Avec suivi individuel: créer un groupe + des individus
|
||||||
room_group = Equipment(
|
room_group = create_equipment(
|
||||||
name=f"{base_name} {room.name}",
|
name=f"{base_name} {room.name}",
|
||||||
lot_id=lot_id if not parent else None,
|
lot_id=lot_id if not parent else None,
|
||||||
category_id=category_id if not parent else None,
|
category_id=category_id if not parent else None,
|
||||||
is_group=True,
|
is_group=True,
|
||||||
parent_id=parent.id if parent else None,
|
parent_id=parent.id if parent else None,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
quantity=1
|
quantity=1, create_schedule=False,
|
||||||
)
|
)
|
||||||
db.session.add(room_group)
|
created_instances.append(room_group)
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
created_equipments.append({
|
created_equipments.append({
|
||||||
'type': 'group',
|
'type': 'group',
|
||||||
|
|
@ -279,7 +282,7 @@ def create_equipments():
|
||||||
# Créer les équipements individuels
|
# Créer les équipements individuels
|
||||||
if count > 1:
|
if count > 1:
|
||||||
for i in range(1, count + 1):
|
for i in range(1, count + 1):
|
||||||
individual = Equipment(
|
individual = create_equipment(
|
||||||
name=f"{base_name} {i}",
|
name=f"{base_name} {i}",
|
||||||
lot_id=lot_id, # Hériter du lot
|
lot_id=lot_id, # Hériter du lot
|
||||||
category_id=category_id, # Hériter de la catégorie
|
category_id=category_id, # Hériter de la catégorie
|
||||||
|
|
@ -287,9 +290,9 @@ def create_equipments():
|
||||||
tracked_individually=True,
|
tracked_individually=True,
|
||||||
parent_id=room_group.id,
|
parent_id=room_group.id,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
position=f"{base_name} n°{i}"
|
position=f"{base_name} n°{i}", create_schedule=False,
|
||||||
)
|
)
|
||||||
db.session.add(individual)
|
created_instances.append(individual)
|
||||||
|
|
||||||
created_equipments.append({
|
created_equipments.append({
|
||||||
'type': 'individual',
|
'type': 'individual',
|
||||||
|
|
@ -298,17 +301,16 @@ def create_equipments():
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
# Sans suivi individuel: créer un seul équipement avec quantity
|
# Sans suivi individuel: créer un seul équipement avec quantity
|
||||||
equipment = Equipment(
|
equipment = create_equipment(
|
||||||
name=f"{base_name} {room.name}",
|
name=f"{base_name} {room.name}",
|
||||||
lot_id=lot_id if not parent else None,
|
lot_id=lot_id if not parent else None,
|
||||||
category_id=category_id if not parent else None,
|
category_id=category_id if not parent else None,
|
||||||
is_group=False,
|
is_group=False,
|
||||||
parent_id=parent.id if parent else None,
|
parent_id=parent.id if parent else None,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
quantity=count
|
quantity=count, create_schedule=False,
|
||||||
)
|
)
|
||||||
db.session.add(equipment)
|
created_instances.append(equipment)
|
||||||
db.session.flush()
|
|
||||||
|
|
||||||
created_equipments.append({
|
created_equipments.append({
|
||||||
'type': 'equipment',
|
'type': 'equipment',
|
||||||
|
|
@ -318,14 +320,13 @@ def create_equipments():
|
||||||
})
|
})
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
for equipment in created_instances:
|
||||||
# Créer les tâches planifiées pour tous les équipements créés
|
if equipment.effective_lot_id:
|
||||||
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()
|
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({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'count': len(created_equipments),
|
'count': len(created_equipments),
|
||||||
|
|
|
||||||
26
tests/unit/test_equipment_creation.py
Normal file
26
tests/unit/test_equipment_creation.py
Normal 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
|
||||||
Loading…
Reference in a new issue