diff --git a/app_new/core/models/maintenance.py b/app_new/core/models/maintenance.py index 11a63d9..076a5bf 100644 --- a/app_new/core/models/maintenance.py +++ b/app_new/core/models/maintenance.py @@ -339,10 +339,27 @@ class Lot(db.Model): name = db.Column(db.String(500), nullable=False) company_id = db.Column(db.Integer, db.ForeignKey("companies.id"), nullable=True) category_id = db.Column(db.Integer, db.ForeignKey("equipment_categories.id"), nullable=True) + is_present = db.Column(db.Boolean, nullable=False, default=True) + absence_reason = db.Column(db.String(500), nullable=True) # Relations category = db.relationship("EquipmentCategory", foreign_keys=[category_id]) tasks = db.relationship("LotTask", backref="lot") + + @property + def active_equipment_count(self): + """Equipements relies directement au lot et encore dans le patrimoine.""" + from .equipment import Equipment + return Equipment.query.filter( + Equipment.lot_id == self.id, + Equipment.is_deleted.is_(False), + db.or_(Equipment.status.is_(None), Equipment.status != 'jete'), + ).count() + + @property + def needs_equipment_attention(self): + """Un lot present sans equipement doit etre signale.""" + return self.is_present and self.active_equipment_count == 0 def __repr__(self): return f"" diff --git a/app_new/lots/routes.py b/app_new/lots/routes.py index 57d2a39..83efb45 100644 --- a/app_new/lots/routes.py +++ b/app_new/lots/routes.py @@ -7,6 +7,7 @@ from flask_login import login_required from app_new.extensions import db from ..core.models.maintenance import Lot, LotTask from ..core.models.company import Company +from ..core.models.equipment import Equipment lots_bp = Blueprint('lots', __name__) @@ -14,27 +15,78 @@ lots_bp = Blueprint('lots', __name__) @login_required def index(): """Liste des lots.""" - lots = Lot.query.order_by(Lot.name).all() - return render_template('lots/index.html', lots=lots) + page = request.args.get('page', 1, type=int) + search = (request.args.get('search') or '').strip() + company_id = request.args.get('company_id', type=int) + presence = request.args.get('presence', '') + + query = Lot.query + if search: + query = query.filter(Lot.name.ilike(f'%{search}%')) + if company_id: + query = query.filter(Lot.company_id == company_id) + if presence == 'present': + query = query.filter(Lot.is_present.is_(True)) + elif presence == 'absent': + query = query.filter(Lot.is_present.is_(False)) + + lots = query.order_by(Lot.name).paginate(page=page, per_page=50, error_out=False) + companies = Company.query.order_by(Company.name).all() + return render_template('lots/index.html', lots=lots, companies=companies) @lots_bp.route('/') @login_required def detail(id): """Détail d'un lot.""" - from app_new.core.models.equipment import Equipment lot = Lot.query.get_or_404(id) # Récupérer les équipements de ce lot equipments = Equipment.query.filter_by(lot_id=id).order_by(Equipment.name).all() return render_template('lots/detail.html', lot=lot, equipments=equipments) -@lots_bp.route('//edit') +@lots_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit(id): """Modifier un lot (formulaire).""" lot = Lot.query.get_or_404(id) - # TODO: formulaire d'édition - flash("Fonctionnalité en cours de développement", "info") - return redirect(url_for('lots.detail', id=id)) + if request.method == 'POST': + lot.company_id = request.form.get('company_id', type=int) + lot.absence_reason = (request.form.get('absence_reason') or '').strip() or None + db.session.commit() + flash('Lot modifié.', 'success') + return redirect(url_for('lots.detail', id=id)) + + companies = Company.query.order_by(Company.name).all() + return render_template('lots/edit.html', lot=lot, companies=companies) + + +@lots_bp.route('//presence', methods=['POST']) +@login_required +def set_presence(id): + """Marquer un lot comme présent ou non présent dans l'établissement.""" + lot = Lot.query.get_or_404(id) + action = request.form.get('action') + + if action == 'absent': + if lot.active_equipment_count: + flash( + f"Impossible de marquer ce lot non présent : " + f"{lot.active_equipment_count} équipement(s) y sont encore rattachés.", + 'danger', + ) + else: + lot.is_present = False + lot.absence_reason = (request.form.get('absence_reason') or '').strip() or None + db.session.commit() + flash(f"Le lot « {lot.name} » est marqué non présent.", 'success') + elif action == 'present': + lot.is_present = True + lot.absence_reason = None + db.session.commit() + flash(f"Le lot « {lot.name} » est marqué présent.", 'success') + else: + flash('Action de présence invalide.', 'danger') + + return redirect(request.referrer or url_for('lots.detail', id=id)) @lots_bp.route('//delete', methods=['POST']) @login_required @@ -43,4 +95,4 @@ def delete(id): lot = Lot.query.get_or_404(id) # TODO: suppression avec déplacement vers corbeille flash("Fonctionnalité en cours de développement", "info") - return redirect(url_for('lots.index')) \ No newline at end of file + return redirect(url_for('lots.index')) diff --git a/app_new/templates/lots/detail.html b/app_new/templates/lots/detail.html index fc3561f..66d4530 100644 --- a/app_new/templates/lots/detail.html +++ b/app_new/templates/lots/detail.html @@ -30,6 +30,18 @@
+ {% if lot.needs_equipment_attention %} +
+ + Lot présent sans équipement. Rattachez au moins un équipement ou marquez ce lot comme non présent. +
+ {% elif not lot.is_present %} +
+ + Ce lot n’est pas présent dans l’établissement. + {% if lot.absence_reason %}
{{ lot.absence_reason }}
{% endif %} +
+ {% endif %}
Informations
@@ -38,6 +50,12 @@
Nom
{{ lot.name }}
+ +
Présence
+
+ {% if lot.is_present %}Présent + {% else %}Non présent{% endif %} +
{% if lot.description %}
Description
@@ -66,6 +84,28 @@
+ + {% if not lot.active_equipment_count %} +
+
Statut du lot
+
+
+ + {% if lot.is_present %} + +
+ + +
+ + {% else %} + + + {% endif %} +
+
+
+ {% endif %} {# Équipements du lot #} {% if equipments %} @@ -176,4 +216,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/templates/lots/edit.html b/app_new/templates/lots/edit.html index a76a266..7553f01 100644 --- a/app_new/templates/lots/edit.html +++ b/app_new/templates/lots/edit.html @@ -19,6 +19,14 @@ + + {% if not lot.is_present %} +
+ + +
+ {% endif %}
@@ -40,4 +48,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/templates/lots/index.html b/app_new/templates/lots/index.html index 76b38ae..16b9a84 100644 --- a/app_new/templates/lots/index.html +++ b/app_new/templates/lots/index.html @@ -13,10 +13,10 @@
-
+
-
+
+
+ +
@@ -43,16 +50,34 @@ Lot - Catégories + Présence + Équipements + Tâches Entreprise Actions {% for lot in lots %} - + {{ lot.name }} - {{ lot.tasks|length }} tâche(s) + + {% if lot.is_present %} + Présent + {% else %} + Non présent + {% endif %} + + + {% if lot.active_equipment_count %} + {{ lot.active_equipment_count }} équipement(s) + {% elif lot.is_present %} + Aucun équipement + {% else %} + Sans objet + {% endif %} + + {{ lot.tasks|length }} tâche(s) {% if lot.company %} {{ lot.company.name }} @@ -67,6 +92,19 @@ Modifier + {% if not lot.is_present %} + + + + + + {% elif not lot.active_equipment_count %} +
+ + + +
+ {% endif %} {% endfor %} @@ -74,6 +112,19 @@
+ {% if lots.pages > 1 %} + + {% endif %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/migrations/versions/c8d2e3f4a5b6_add_lot_presence.py b/migrations/versions/c8d2e3f4a5b6_add_lot_presence.py new file mode 100644 index 0000000..0f8c43a --- /dev/null +++ b/migrations/versions/c8d2e3f4a5b6_add_lot_presence.py @@ -0,0 +1,29 @@ +"""Add explicit lot presence status. + +Revision ID: c8d2e3f4a5b6 +Revises: b7a1c2d3e4f5 +Create Date: 2026-08-14 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = "c8d2e3f4a5b6" +down_revision = "b7a1c2d3e4f5" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "lots", + sa.Column("is_present", sa.Boolean(), nullable=False, server_default=sa.true()), + ) + op.add_column("lots", sa.Column("absence_reason", sa.String(length=500), nullable=True)) + op.alter_column("lots", "is_present", server_default=None) + + +def downgrade(): + op.drop_column("lots", "absence_reason") + op.drop_column("lots", "is_present") diff --git a/tests/integration/test_lot_presence.py b/tests/integration/test_lot_presence.py new file mode 100644 index 0000000..d5e6c69 --- /dev/null +++ b/tests/integration/test_lot_presence.py @@ -0,0 +1,52 @@ +from app_new.extensions import db +from app_new.core.models.equipment import Equipment +from app_new.core.models.maintenance import Lot +from app_new.lots.routes import index, set_presence + + +def test_empty_lot_is_visible_and_can_be_marked_absent(app): + with app.app_context(): + lot = Lot(name="Ascenseurs") + db.session.add(lot) + db.session.commit() + lot_id = lot.id + with app.test_request_context('/lots/'): + rendered = index.__wrapped__() + assert 'Aucun équipement' in rendered + + with app.test_request_context( + f'/lots/{lot_id}/presence', + method='POST', + data={'action': 'absent', 'absence_reason': 'Aucun ascenseur dans le college'}, + ): + response = set_presence.__wrapped__(lot_id) + assert response.status_code == 302 + + lot = db.session.get(Lot, lot_id) + assert lot.is_present is False + assert lot.absence_reason == 'Aucun ascenseur dans le college' + assert lot.needs_equipment_attention is False + + +def test_lot_with_equipment_cannot_be_marked_absent(app): + with app.app_context(): + lot = Lot(name="Electricite") + equipment = Equipment( + name="Luminaires A01", + is_group=True, + quantity=10, + lot=lot, + status='en_service', + ) + db.session.add_all([lot, equipment]) + db.session.commit() + lot_id = lot.id + with app.test_request_context( + f'/lots/{lot_id}/presence', + method='POST', + data={'action': 'absent'}, + ): + response = set_presence.__wrapped__(lot_id) + assert response.status_code == 302 + + assert db.session.get(Lot, lot_id).is_present is True