From a296b9e01333876228385b20cc45355cd976c174 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 14 Aug 2026 21:23:33 +0000 Subject: [PATCH] =?UTF-8?q?G=C3=A8re=20les=20logements=20de=20fonction=20e?= =?UTF-8?q?t=20leurs=20pi=C3=A8ces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_new/__init__.py | 3 + app_new/core/authorization.py | 2 +- app_new/core/models/__init__.py | 4 +- app_new/core/models/college.py | 25 +++++ app_new/housing/__init__.py | 3 + app_new/housing/routes.py | 100 ++++++++++++++++++ app_new/templates/base.html | 1 + app_new/templates/housing/detail.html | 5 + app_new/templates/housing/form.html | 7 ++ app_new/templates/housing/index.html | 6 ++ .../d5e9f0a1b2c3_add_housing_units.py | 45 ++++++++ 11 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 app_new/housing/__init__.py create mode 100644 app_new/housing/routes.py create mode 100644 app_new/templates/housing/detail.html create mode 100644 app_new/templates/housing/form.html create mode 100644 app_new/templates/housing/index.html create mode 100644 migrations/versions/d5e9f0a1b2c3_add_housing_units.py diff --git a/app_new/__init__.py b/app_new/__init__.py index 43ae0ea..802ca9f 100644 --- a/app_new/__init__.py +++ b/app_new/__init__.py @@ -133,6 +133,9 @@ def create_app(config_name='default'): from .wizard.routes import wizard_bp app.register_blueprint(wizard_bp) + + from .housing import housing_bp + app.register_blueprint(housing_bp, url_prefix='/housing') # Blueprints des intégrations from .pronote.routes import pronote_bp diff --git a/app_new/core/authorization.py b/app_new/core/authorization.py index f2a650c..06d113a 100644 --- a/app_new/core/authorization.py +++ b/app_new/core/authorization.py @@ -68,7 +68,7 @@ ADMIN_BLUEPRINTS = { "outlook_auth", "outlook_dashboard", "outlook_pages", "outlook_sync", "ent", "pronote", } PATRIMOINE_BLUEPRINTS = { - "equipments", "equipments_meters", "equipments_documents", "equipments_restrictions", + "equipments", "equipments_meters", "equipments_documents", "equipments_restrictions", "housing", "equipments_scheduled", "buildings", "zones", "rooms", "room_types", "wizard", "lots", } PLANNING_BLUEPRINTS = {"planning", "scheduler", "interventions_planning"} diff --git a/app_new/core/models/__init__.py b/app_new/core/models/__init__.py index 6453193..caacf77 100644 --- a/app_new/core/models/__init__.py +++ b/app_new/core/models/__init__.py @@ -3,7 +3,7 @@ Core Models - GMAO Collège Import tous les modèles du core """ from .user import User, Staff -from .college import College, Site, Building, Room, RoomType, RoomSchedule, Zone +from .college import College, Site, Building, HousingUnit, Room, RoomType, RoomSchedule, Zone from .equipment import EquipmentCategory, Equipment, EquipmentDocument, EquipmentRoomHistory, EquipmentQuantityMovement from .maintenance import ( Intervention, StatusChange, InterventionComment, InterventionDocument, @@ -21,7 +21,7 @@ from .audit import AuditLog __all__ = [ 'User', 'Staff', - 'College', 'Site', 'Building', 'Room', 'RoomType', 'RoomSchedule', 'Zone', + 'College', 'Site', 'Building', 'HousingUnit', 'Room', 'RoomType', 'RoomSchedule', 'Zone', 'EquipmentCategory', 'Equipment', 'EquipmentDocument', 'EquipmentRoomHistory', 'EquipmentQuantityMovement', 'Intervention', 'StatusChange', 'InterventionComment', 'InterventionDocument', 'Lot', 'LotTask', 'LotService', diff --git a/app_new/core/models/college.py b/app_new/core/models/college.py index 575ac50..4bdef6f 100644 --- a/app_new/core/models/college.py +++ b/app_new/core/models/college.py @@ -66,6 +66,29 @@ class Building(db.Model): return f"" +class HousingUnit(db.Model): + """Logement de fonction et historique courant de son occupation.""" + __tablename__ = "housing_units" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), nullable=False) + code = db.Column(db.String(40), nullable=True, unique=True) + building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), nullable=False, index=True) + housing_type = db.Column(db.String(40), nullable=False, default="fonction") + occupancy_status = db.Column(db.String(30), nullable=False, default="vacant") + occupant_name = db.Column(db.String(150), nullable=True) + occupant_contact = db.Column(db.String(150), nullable=True) + occupancy_start = db.Column(db.Date, nullable=True) + occupancy_end = db.Column(db.Date, nullable=True) + notes = db.Column(db.Text, nullable=True) + is_active = db.Column(db.Boolean, nullable=False, default=True) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) + + building = db.relationship("Building", backref=db.backref("housing_units", lazy="dynamic")) + rooms = db.relationship("Room", back_populates="housing_unit", lazy="dynamic") + + class Zone(db.Model): """Zone dans un bâtiment.""" __tablename__ = "zones" @@ -100,6 +123,7 @@ class Room(db.Model): floor = db.Column(db.Integer, default=0) building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), nullable=False) room_type_id = db.Column(db.Integer, db.ForeignKey("room_types.id"), nullable=True) + housing_unit_id = db.Column(db.Integer, db.ForeignKey("housing_units.id"), nullable=True, index=True) # Libelle historique conserve pour les imports anterieurs a zone_id. legacy_zone_name = db.Column("zone", db.String(100), nullable=True) @@ -108,6 +132,7 @@ class Room(db.Model): zone = db.relationship("Zone", back_populates="rooms") room_type = db.relationship("RoomType", back_populates="rooms") equipments = db.relationship("Equipment", back_populates="room") + housing_unit = db.relationship("HousingUnit", back_populates="rooms") @property def full_name(self): diff --git a/app_new/housing/__init__.py b/app_new/housing/__init__.py new file mode 100644 index 0000000..032bc7a --- /dev/null +++ b/app_new/housing/__init__.py @@ -0,0 +1,3 @@ +from .routes import housing_bp + +__all__ = ["housing_bp"] diff --git a/app_new/housing/routes.py b/app_new/housing/routes.py new file mode 100644 index 0000000..e3fb161 --- /dev/null +++ b/app_new/housing/routes.py @@ -0,0 +1,100 @@ +from datetime import datetime + +from flask import Blueprint, flash, redirect, render_template, request, url_for +from flask_login import login_required + +from app_new.extensions import db +from app_new.core.models.college import Building, HousingUnit, Room, RoomType, Site, Zone + + +housing_bp = Blueprint("housing", __name__, template_folder="templates") + + +def _date(value): + return datetime.strptime(value, "%Y-%m-%d").date() if value else None + + +@housing_bp.route("/") +@login_required +def index(): + units = HousingUnit.query.join(Building).order_by(Building.name, HousingUnit.name).all() + return render_template("housing/index.html", units=units) + + +@housing_bp.route("/new", methods=["GET", "POST"]) +@login_required +def create(): + housing_sites = Site.query.filter_by(site_type="logements", is_active=True).order_by(Site.name).all() + buildings = Building.query.order_by(Building.name).all() + if request.method == "POST": + name = (request.form.get("name") or "").strip() + building_id = request.form.get("building_id", type=int) + if not name or not building_id: + flash("Le nom et le bâtiment sont obligatoires.", "danger") + return render_template("housing/form.html", unit=None, buildings=buildings, housing_sites=housing_sites), 400 + unit = HousingUnit(name=name, building_id=building_id) + _apply_form(unit) + db.session.add(unit) + db.session.commit() + flash(f"Logement « {unit.name} » créé.", "success") + return redirect(url_for("housing.detail", unit_id=unit.id)) + return render_template("housing/form.html", unit=None, buildings=buildings, housing_sites=housing_sites) + + +@housing_bp.route("/") +@login_required +def detail(unit_id): + unit = HousingUnit.query.get_or_404(unit_id) + equipments = [equipment for room in unit.rooms for equipment in room.equipments if not equipment.is_deleted] + return render_template("housing/detail.html", unit=unit, equipments=equipments) + + +@housing_bp.route("//edit", methods=["GET", "POST"]) +@login_required +def edit(unit_id): + unit = HousingUnit.query.get_or_404(unit_id) + buildings = Building.query.order_by(Building.name).all() + if request.method == "POST": + _apply_form(unit) + unit.name = (request.form.get("name") or "").strip() + unit.building_id = request.form.get("building_id", type=int) + db.session.commit() + flash("Logement mis à jour.", "success") + return redirect(url_for("housing.detail", unit_id=unit.id)) + return render_template("housing/form.html", unit=unit, buildings=buildings, housing_sites=[]) + + +@housing_bp.route("//rooms", methods=["POST"]) +@login_required +def add_room(unit_id): + unit = HousingUnit.query.get_or_404(unit_id) + name = (request.form.get("name") or "").strip() + if not name: + flash("Le nom de la pièce est obligatoire.", "danger") + return redirect(url_for("housing.detail", unit_id=unit.id)) + zone = Zone.query.filter_by(building_id=unit.building_id, name="Logements").first() + if zone is None: + zone = Zone(name="Logements", building_id=unit.building_id) + db.session.add(zone) + db.session.flush() + room_type = RoomType.query.filter_by(name=request.form.get("room_type_name", "Logement")).first() + room = Room( + name=name, code=(request.form.get("code") or "").strip() or None, + building_id=unit.building_id, zone_id=zone.id, + room_type_id=room_type.id if room_type else None, housing_unit_id=unit.id, + ) + db.session.add(room) + db.session.commit() + flash(f"Pièce « {room.name} » ajoutée.", "success") + return redirect(url_for("housing.detail", unit_id=unit.id)) + + +def _apply_form(unit): + unit.code = (request.form.get("code") or "").strip() or None + unit.housing_type = request.form.get("housing_type", "fonction") + unit.occupancy_status = request.form.get("occupancy_status", "vacant") + unit.occupant_name = (request.form.get("occupant_name") or "").strip() or None + unit.occupant_contact = (request.form.get("occupant_contact") or "").strip() or None + unit.occupancy_start = _date(request.form.get("occupancy_start")) + unit.occupancy_end = _date(request.form.get("occupancy_end")) + unit.notes = (request.form.get("notes") or "").strip() or None diff --git a/app_new/templates/base.html b/app_new/templates/base.html index 5db7eac..e7010bb 100644 --- a/app_new/templates/base.html +++ b/app_new/templates/base.html @@ -211,6 +211,7 @@
  • Bâtiments
  • Salles
  • +
  • Logements de fonction
  • Types de salles
  • Entreprises
  • diff --git a/app_new/templates/housing/detail.html b/app_new/templates/housing/detail.html new file mode 100644 index 0000000..d132a63 --- /dev/null +++ b/app_new/templates/housing/detail.html @@ -0,0 +1,5 @@ +{% extends "base.html" %}{% block title %}{{ unit.name }} — GMAO{% endblock %}{% block content %} +

    {{ unit.name }}

    {{ unit.building.site.name if unit.building.site else '' }} · {{ unit.building.name }}
    +
    Occupation

    État : {{ unit.occupancy_status|replace('_',' ')|title }}

    Occupant : {{ unit.occupant_name or 'Aucun' }}

    Contact : {{ unit.occupant_contact or '—' }}

    Période : {{ unit.occupancy_start|date_fmt if unit.occupancy_start else '—' }} → {{ unit.occupancy_end|date_fmt if unit.occupancy_end else '—' }}

    +
    Pièces
    {% for room in unit.rooms %}{{ room.name }}{{ room.equipments|length }} équipement(s){% else %}
    Aucune pièce.
    {% endfor %}
    Patrimoine du logement
    {% for equipment in equipments %}{{ equipment.name }} · {{ equipment.room.name }}{{ equipment.quantity }}{% else %}
    Aucun équipement.
    {% endfor %}
    +{% endblock %} diff --git a/app_new/templates/housing/form.html b/app_new/templates/housing/form.html new file mode 100644 index 0000000..c9b275e --- /dev/null +++ b/app_new/templates/housing/form.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% block title %}{{ 'Modifier' if unit else 'Nouveau' }} logement — GMAO{% endblock %} +{% block content %}

    {{ 'Modifier le' if unit else 'Nouveau' }} logement

    +
    +
    +
    +
    {% endblock %} diff --git a/app_new/templates/housing/index.html b/app_new/templates/housing/index.html new file mode 100644 index 0000000..3234089 --- /dev/null +++ b/app_new/templates/housing/index.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block title %}Logements de fonction — GMAO{% endblock %} +{% block content %} +

    Logements de fonction

    Occupation, pièces et patrimoine technique des logements.

    Nouveau logement
    +
    {% for unit in units %}

    {{ unit.name }}

    {{ unit.occupancy_status|replace('_',' ') }}
    {{ unit.building.site.name if unit.building.site else 'Site non renseigné' }} · {{ unit.building.name }}

    Occupant : {{ unit.occupant_name or 'Aucun' }}
    Pièces : {{ unit.rooms.count() }}
    {% else %}
    Aucun logement enregistré.
    {% endfor %}
    +{% endblock %} diff --git a/migrations/versions/d5e9f0a1b2c3_add_housing_units.py b/migrations/versions/d5e9f0a1b2c3_add_housing_units.py new file mode 100644 index 0000000..5659650 --- /dev/null +++ b/migrations/versions/d5e9f0a1b2c3_add_housing_units.py @@ -0,0 +1,45 @@ +"""Ajoute la gestion métier des logements de fonction. + +Revision ID: d5e9f0a1b2c3 +Revises: c4d8e9f0a1b2 +""" +from alembic import op +import sqlalchemy as sa + +revision = "d5e9f0a1b2c3" +down_revision = "c4d8e9f0a1b2" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "housing_units", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=120), nullable=False), + sa.Column("code", sa.String(length=40), nullable=True), + sa.Column("building_id", sa.Integer(), nullable=False), + sa.Column("housing_type", sa.String(length=40), nullable=False, server_default="fonction"), + sa.Column("occupancy_status", sa.String(length=30), nullable=False, server_default="vacant"), + sa.Column("occupant_name", sa.String(length=150), nullable=True), + sa.Column("occupant_contact", sa.String(length=150), nullable=True), + sa.Column("occupancy_start", sa.Date(), nullable=True), + sa.Column("occupancy_end", sa.Date(), nullable=True), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["building_id"], ["buildings.id"]), + sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("code"), + ) + op.create_index("ix_housing_units_building_id", "housing_units", ["building_id"]) + op.add_column("rooms", sa.Column("housing_unit_id", sa.Integer(), nullable=True)) + op.create_foreign_key("fk_rooms_housing_unit", "rooms", "housing_units", ["housing_unit_id"], ["id"]) + op.create_index("ix_rooms_housing_unit_id", "rooms", ["housing_unit_id"]) + + +def downgrade(): + op.drop_index("ix_rooms_housing_unit_id", table_name="rooms") + op.drop_constraint("fk_rooms_housing_unit", "rooms", type_="foreignkey") + op.drop_column("rooms", "housing_unit_id") + op.drop_table("housing_units")