diff --git a/app_new/core/models/equipment.py b/app_new/core/models/equipment.py index 3de4020..055c96e 100644 --- a/app_new/core/models/equipment.py +++ b/app_new/core/models/equipment.py @@ -167,17 +167,26 @@ class Equipment(db.Model): @property def total_quantity(self): - """Quantité totale: si groupe, compte tous les descendants; sinon retourne quantity.""" - if self.is_group: - # Compter récursivement tous les descendants - total = 0 - for child in Equipment.query.filter_by(parent_id=self.id).all(): - if child.is_group: - total += child.total_quantity - else: - total += child.quantity - return total if total > 0 else self.quantity + """Quantite physique declaree, individualisation comprise.""" return self.quantity + + @property + def individualized_quantity(self): + """Nombre d'unites du groupe disposant deja de leur propre fiche.""" + if not self.is_group: + return 0 + return sum( + max(child.quantity or 1, 1) + for child in self.children + if not child.is_group + ) + + @property + def unindividualized_quantity(self): + """Nombre d'unites encore gerees uniquement sous forme quantitative.""" + if not self.is_group: + return 0 + return max((self.quantity or 0) - self.individualized_quantity, 0) @property def total_children(self): @@ -196,9 +205,11 @@ class Equipment(db.Model): @property def effective_room(self): - """Salle effective (celle du parent si individuel, sinon la salle).""" + """Salle propre, ou salle heritee du parent si elle n'est pas definie.""" + if self.room_id and self.room: + return self.room if self.is_individual and self.parent: - return self.parent.room + return self.parent.effective_room return self.room def create_scheduled_tasks_from_lot(self): @@ -413,4 +424,4 @@ class EquipmentRestriction(db.Model): return self.restriction_type == 'room_occupancy' def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/app_new/equipments/main.py b/app_new/equipments/main.py index d7fa095..11467a1 100644 --- a/app_new/equipments/main.py +++ b/app_new/equipments/main.py @@ -518,18 +518,27 @@ def add_elements(id): if request.method == 'POST': count = int(request.form.get('count', 1)) + if count < 1: + flash('La quantite ajoutee doit etre positive.', 'danger') + return redirect(url_for('equipments.add_elements', id=equipment.id)) + + first_number = equipment.individualized_quantity + 1 for i in range(count): new_element = Equipment( - name=f"{equipment.name} #{equipment.children.count() + i + 1}", - code=f"{equipment.code}-{equipment.children.count() + i + 1}" if equipment.code else None, + name=f"{equipment.name} #{first_number + i}", + code=f"{equipment.code}-{first_number + i}" if equipment.code else None, parent_id=equipment.id, + individual_number=first_number + i, status='en_service', room_id=equipment.room_id, - lot_id=equipment.lot_id, # Hériter du lot - category_id=equipment.category_id, # Hériter de la catégorie - is_group=False # Un enfant n'est pas un groupe + quantity=1, + is_group=False, + tracked_individually=True, ) db.session.add(new_element) + + # L'ajout augmente le patrimoine physique du groupe. + equipment.quantity = (equipment.quantity or 0) + count db.session.commit() @@ -549,22 +558,62 @@ def add_elements(id): @main_bp.route('//individualize', methods=['GET', 'POST']) @login_required def individualize(id): - """Individualiser un élément d'un groupe.""" + """Extraire une unite quantitative d'un groupe vers une fiche individuelle.""" equipment = Equipment.query.get_or_404(id) - if not equipment.parent_id: - flash("Cet équipement n'est pas un élément de groupe.", 'warning') + if not equipment.is_group: + flash("Seul un groupe d'équipements peut fournir une unité à individualiser.", 'warning') + return redirect(url_for('equipments.detail', id=equipment.id)) + + if equipment.unindividualized_quantity < 1: + flash("Toutes les unités de ce groupe sont déjà individualisées.", 'warning') return redirect(url_for('equipments.detail', id=equipment.id)) if request.method == 'POST': - # Rendre l'équipement indépendant - equipment.parent_id = None - equipment.is_group = False + next_number = max( + [child.individual_number or 0 for child in equipment.children] or [0] + ) + 1 + name = (request.form.get('name') or '').strip() or f"{equipment.name} #{next_number}" + code = (request.form.get('code') or '').strip() or None + position = (request.form.get('position') or '').strip() or None + + individual = Equipment( + name=name, + code=code, + description=equipment.description, + status='en_service', + parent_id=equipment.id, + is_group=False, + quantity=1, + tracked_individually=True, + individual_number=next_number, + position=position, + room_id=equipment.room_id, + # Le lot et la categorie restent herites du groupe. + lot_id=None, + category_id=None, + ) + db.session.add(individual) db.session.commit() - flash(f"Équipement '{equipment.name}' individualisé.", 'success') - return redirect(url_for('equipments.detail', id=equipment.id)) - - return render_template('equipments/individualize.html', equipment=equipment) + + if individual.effective_lot_id: + individual.create_scheduled_tasks_from_lot() + + flash( + f"Unité #{next_number} individualisée. Le groupe conserve un total de " + f"{equipment.quantity} unité(s).", + 'success', + ) + return redirect(url_for('equipments.detail', id=individual.id)) + + next_number = max( + [child.individual_number or 0 for child in equipment.children] or [0] + ) + 1 + return render_template( + 'equipments/individualize.html', + equipment=equipment, + next_number=next_number, + ) @main_bp.route('//children') @@ -640,4 +689,3 @@ def move_to_room(id): # === Gestion des Zones === - diff --git a/app_new/equipments/templates/individualize.html b/app_new/equipments/templates/individualize.html deleted file mode 100644 index ea2cb4a..0000000 --- a/app_new/equipments/templates/individualize.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "base.html" %} -{% block title %}Individualiser un élément — GMAO Collège{% endblock %} - -{% block content %} -
-
- Individualiser un élément -
-
-

- Créer un élément individuel à partir du groupe {{ equipment.name }} - ({{ equipment.quantity }} unités). -

- -
- - L'élément sera numéroté n°{{ next_number }} et pourra être suivi indépendamment. -
- -
- {{ csrf_token() if csrf_token is defined }} - -
- - - Attribué automatiquement -
- -
- - - Localisation précise dans la salle -
- -
- - - Annuler - -
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/app_new/templates/equipments/detail.html b/app_new/templates/equipments/detail.html index 6599aaf..715ebce 100644 --- a/app_new/templates/equipments/detail.html +++ b/app_new/templates/equipments/detail.html @@ -105,6 +105,10 @@ Quantité déclarée {{ equipment.quantity }} unités +
+ {{ equipment.individualized_quantity }} individualisée(s), + {{ equipment.unindividualized_quantity }} encore gérée(s) en quantité +
@@ -450,6 +454,13 @@
{% if equipment.is_group %}
+ {% if equipment.unindividualized_quantity > 0 %} + + {% endif %} {% if not equipment.parent %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/templates/equipments/individualize.html b/app_new/templates/equipments/individualize.html index d6f385d..9ea9a5e 100644 --- a/app_new/templates/equipments/individualize.html +++ b/app_new/templates/equipments/individualize.html @@ -1,55 +1,53 @@ {% extends "base.html" %} - -{% block title %}Individualiser — GMAO{% endblock %} +{% block title %}Individualiser une unité — GMAO{% endblock %} {% block content %}
-
-
- -

- - Individualiser l'équipement -

+ +
+
+
Groupe d’origine
+
+

{{ equipment.name }}

+
+
Total physique
{{ equipment.quantity }}
+
Déjà individualisées
{{ equipment.individualized_quantity }}
+
Restantes après l’action
{{ equipment.unindividualized_quantity - 1 }}
+
+ {% if equipment.effective_lot %} +

Lot hérité : {{ equipment.effective_lot.name }}

+ {% endif %} +
-
- -
-
-
-
-
{{ equipment.name }}
-

- Cet élément appartient au groupe: {{ equipment.parent.name }}
- Individualiser cet élément le rendra indépendant du groupe. -

+
+ +
Nouvelle fiche individuelle #{{ next_number }}
+
+
+ + +
+
+ + +
+
+ + +
Facultatif pour le mobilier, recommandé pour un luminaire, une prise ou une fenêtre.
- - - - -
- - Cette action est irréversible. L'équipement sera dissocié du groupe parent. -
- -
- - - Annuler - -
-
-
-
+ + +
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tests/integration/test_equipment_individualization.py b/tests/integration/test_equipment_individualization.py new file mode 100644 index 0000000..55ca90b --- /dev/null +++ b/tests/integration/test_equipment_individualization.py @@ -0,0 +1,61 @@ +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.equipments.main import individualize + + +def test_group_unit_can_be_individualized_without_changing_total(app): + with app.app_context(): + category = EquipmentCategory(name="Luminaires") + lot = Lot(name="Electricite") + group = Equipment( + name="Luminaires A01", + is_group=True, + quantity=10, + category=category, + lot=lot, + status="en_service", + ) + db.session.add_all([category, lot, group]) + db.session.commit() + group_id = group.id + lot_id = lot.id + category_id = category.id + with app.test_request_context( + f"/equipments/{group_id}/individualize", + method="POST", + data={ + "name": "Luminaire du milieu", + "code": "A01-LUM-01", + "position": "milieu, cote fenetres", + }, + ): + response = individualize.__wrapped__(group_id) + assert response.status_code == 302 + + group = db.session.get(Equipment, group_id) + individual = Equipment.query.filter_by(parent_id=group_id).one() + assert group.quantity == 10 + assert group.individualized_quantity == 1 + assert group.unindividualized_quantity == 9 + assert individual.quantity == 1 + assert individual.individual_number == 1 + assert individual.position == "milieu, cote fenetres" + assert individual.lot_id is None + assert individual.category_id is None + assert individual.effective_lot_id == lot_id + assert individual.effective_category_id == category_id + + +def test_group_cannot_individualize_more_than_declared_quantity(app): + with app.app_context(): + group = Equipment(name="Luminaire unique", is_group=True, quantity=1) + db.session.add(group) + db.session.commit() + group_id = group.id + with app.test_request_context(method="POST"): + assert individualize.__wrapped__(group_id).status_code == 302 + with app.test_request_context(method="POST"): + assert individualize.__wrapped__(group_id).status_code == 302 + + assert Equipment.query.filter_by(parent_id=group_id).count() == 1