Gère les logements de fonction et leurs pièces
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
This commit is contained in:
parent
358f946c76
commit
a296b9e013
11 changed files with 198 additions and 3 deletions
|
|
@ -134,6 +134,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
|
||||
app.register_blueprint(pronote_bp, url_prefix='/pronote')
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -66,6 +66,29 @@ class Building(db.Model):
|
|||
return f"<Building {self.name}>"
|
||||
|
||||
|
||||
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):
|
||||
|
|
|
|||
3
app_new/housing/__init__.py
Normal file
3
app_new/housing/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .routes import housing_bp
|
||||
|
||||
__all__ = ["housing_bp"]
|
||||
100
app_new/housing/routes.py
Normal file
100
app_new/housing/routes.py
Normal file
|
|
@ -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("/<int:unit_id>")
|
||||
@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("/<int:unit_id>/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("/<int:unit_id>/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
|
||||
|
|
@ -211,6 +211,7 @@
|
|||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('buildings.index') }}"><i class="bi bi-buildings"></i> Bâtiments</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('rooms.index') }}"><i class="bi bi-door-open"></i> Salles</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('housing.index') }}"><i class="bi bi-house-door"></i> Logements de fonction</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('room_types.index') }}"><i class="bi bi-tags"></i> Types de salles</a></li>
|
||||
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('companies.index') }}"><i class="bi bi-building"></i> Entreprises</a></li>
|
||||
|
|
|
|||
5
app_new/templates/housing/detail.html
Normal file
5
app_new/templates/housing/detail.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "base.html" %}{% block title %}{{ unit.name }} — GMAO{% endblock %}{% block content %}
|
||||
<div class="d-flex justify-content-between mb-4"><div><h1 class="h3"><i class="bi bi-house-door"></i> {{ unit.name }}</h1><div class="text-muted">{{ unit.building.site.name if unit.building.site else '' }} · {{ unit.building.name }}</div></div><div><a href="{{ url_for('housing.edit', unit_id=unit.id) }}" class="btn btn-outline-primary">Modifier</a> <a href="{{ url_for('wizard.equipment_wizard', housing_id=unit.id) }}" class="btn btn-success"><i class="bi bi-magic"></i> Inventorier</a></div></div>
|
||||
<div class="row g-3"><div class="col-lg-4"><div class="card"><div class="card-header">Occupation</div><div class="card-body"><p><strong>État :</strong> {{ unit.occupancy_status|replace('_',' ')|title }}</p><p><strong>Occupant :</strong> {{ unit.occupant_name or 'Aucun' }}</p><p><strong>Contact :</strong> {{ unit.occupant_contact or '—' }}</p><p><strong>Période :</strong> {{ unit.occupancy_start|date_fmt if unit.occupancy_start else '—' }} → {{ unit.occupancy_end|date_fmt if unit.occupancy_end else '—' }}</p></div></div></div>
|
||||
<div class="col-lg-8"><div class="card mb-3"><div class="card-header">Pièces</div><div class="card-body"><div class="list-group mb-3">{% for room in unit.rooms %}<a class="list-group-item list-group-item-action d-flex justify-content-between" href="{{ url_for('rooms.detail', id=room.id) }}"><span>{{ room.name }}</span><span class="badge text-bg-secondary">{{ room.equipments|length }} équipement(s)</span></a>{% else %}<div class="text-muted">Aucune pièce.</div>{% endfor %}</div><form method="post" action="{{ url_for('housing.add_room', unit_id=unit.id) }}" class="row g-2"><div class="col"><input name="name" class="form-control" placeholder="Cuisine, séjour, chambre…" required></div><div class="col-sm-3"><input name="code" class="form-control" placeholder="Code"></div><div class="col-auto"><button class="btn btn-primary">Ajouter</button></div></form></div></div><div class="card"><div class="card-header">Patrimoine du logement</div><div class="list-group list-group-flush">{% for equipment in equipments %}<a href="{{ url_for('equipments.detail', id=equipment.id) }}" class="list-group-item list-group-item-action d-flex justify-content-between"><span>{{ equipment.name }} <small class="text-muted">· {{ equipment.room.name }}</small></span><span>{{ equipment.quantity }}</span></a>{% else %}<div class="list-group-item text-muted">Aucun équipement.</div>{% endfor %}</div></div></div></div>
|
||||
{% endblock %}
|
||||
7
app_new/templates/housing/form.html
Normal file
7
app_new/templates/housing/form.html
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{ 'Modifier' if unit else 'Nouveau' }} logement — GMAO{% endblock %}
|
||||
{% block content %}<div class="container" style="max-width:900px"><h1 class="h3 mb-4">{{ 'Modifier le' if unit else 'Nouveau' }} logement</h1><form method="post" class="card"><div class="card-body row g-3">
|
||||
<div class="col-md-8"><label class="form-label">Nom *</label><input name="name" class="form-control" value="{{ unit.name if unit else '' }}" placeholder="Ex. Logement 1" required></div><div class="col-md-4"><label class="form-label">Code</label><input name="code" class="form-control" value="{{ unit.code if unit and unit.code else '' }}"></div>
|
||||
<div class="col-md-6"><label class="form-label">Bâtiment *</label><select name="building_id" class="form-select" required><option value="">—</option>{% for building in buildings %}<option value="{{ building.id }}" {% if unit and unit.building_id == building.id %}selected{% endif %}>{{ building.site.name ~ ' · ' if building.site else '' }}{{ building.name }}</option>{% endfor %}</select></div><div class="col-md-3"><label class="form-label">Type</label><select name="housing_type" class="form-select"><option value="fonction">Fonction</option><option value="gardien">Gardien</option><option value="temporaire">Temporaire</option></select></div><div class="col-md-3"><label class="form-label">État</label><select name="occupancy_status" class="form-select">{% for value,label in [('vacant','Vacant'),('occupe','Occupé'),('travaux','En travaux'),('indisponible','Indisponible')] %}<option value="{{ value }}" {% if unit and unit.occupancy_status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></div>
|
||||
<div class="col-md-6"><label class="form-label">Occupant</label><input name="occupant_name" class="form-control" value="{{ unit.occupant_name if unit and unit.occupant_name else '' }}"></div><div class="col-md-6"><label class="form-label">Contact</label><input name="occupant_contact" class="form-control" value="{{ unit.occupant_contact if unit and unit.occupant_contact else '' }}"></div><div class="col-md-3"><label class="form-label">Début d'occupation</label><input type="date" name="occupancy_start" class="form-control" value="{{ unit.occupancy_start.isoformat() if unit and unit.occupancy_start else '' }}"></div><div class="col-md-3"><label class="form-label">Fin prévue/réelle</label><input type="date" name="occupancy_end" class="form-control" value="{{ unit.occupancy_end.isoformat() if unit and unit.occupancy_end else '' }}"></div><div class="col-md-6"><label class="form-label">Notes techniques</label><textarea name="notes" class="form-control">{{ unit.notes if unit and unit.notes else '' }}</textarea></div>
|
||||
</div><div class="card-footer"><button class="btn btn-primary">Enregistrer</button> <a href="{{ url_for('housing.index') }}" class="btn btn-outline-secondary">Annuler</a></div></form></div>{% endblock %}
|
||||
6
app_new/templates/housing/index.html
Normal file
6
app_new/templates/housing/index.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Logements de fonction — GMAO{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4"><div><h1 class="h3"><i class="bi bi-house-door"></i> Logements de fonction</h1><p class="text-muted mb-0">Occupation, pièces et patrimoine technique des logements.</p></div><a class="btn btn-primary" href="{{ url_for('housing.create') }}"><i class="bi bi-plus-lg"></i> Nouveau logement</a></div>
|
||||
<div class="row g-3">{% for unit in units %}<div class="col-md-6 col-xl-4"><div class="card h-100"><div class="card-body"><div class="d-flex justify-content-between"><h2 class="h5">{{ unit.name }}</h2><span class="badge {% if unit.occupancy_status == 'occupe' %}text-bg-success{% elif unit.occupancy_status == 'travaux' %}text-bg-warning{% else %}text-bg-secondary{% endif %}">{{ unit.occupancy_status|replace('_',' ') }}</span></div><div class="text-muted small">{{ unit.building.site.name if unit.building.site else 'Site non renseigné' }} · {{ unit.building.name }}</div><hr><div><strong>Occupant :</strong> {{ unit.occupant_name or 'Aucun' }}</div><div><strong>Pièces :</strong> {{ unit.rooms.count() }}</div></div><div class="card-footer"><a href="{{ url_for('housing.detail', unit_id=unit.id) }}" class="btn btn-sm btn-outline-primary">Ouvrir</a></div></div></div>{% else %}<div class="col-12"><div class="alert alert-info">Aucun logement enregistré.</div></div>{% endfor %}</div>
|
||||
{% endblock %}
|
||||
45
migrations/versions/d5e9f0a1b2c3_add_housing_units.py
Normal file
45
migrations/versions/d5e9f0a1b2c3_add_housing_units.py
Normal file
|
|
@ -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")
|
||||
Loading…
Reference in a new issue