loko/streetup/assets/models/buildings.py

217 lines
10 KiB
Python

# -----------------------------
# Public Buildings Models
# -----------------------------
from django.contrib.gis.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User
from assets.models import AbstractAsset, AbstractGeoAsset, AbstractAssetModel, ASSET_STATUS_CHOICES
from common.models import Document
from common.storages import PrivateMediaStorage
private_storage = PrivateMediaStorage()
def get_usage_type_choices(standard='BE'):
"""
Renvoie la liste des choix de type d'usage adaptés à la norme régionale active (BE, FR, INT).
"""
from common.utils import get_regional_terms
terms = get_regional_terms(standard)
admin_label = _("Administratif") + f" ({terms['admin_building']})"
return (
('administrative', admin_label),
('education', _('Enseignement / École / Crèche')),
('sports', _('Sport & Équipement sportif')),
('culture', _('Culture / Médiathèque / Musée')),
('social', _('Social / Centre communautaire')),
('technical', _('Technique / Ateliers / Stock')),
('housing', _('Logement communal')),
('other', _('Autre bâtiment public')),
)
USAGE_TYPE_CHOICES = get_usage_type_choices('BE')
BUILDING_STATUS_CHOICES = (
('in_service', _('En service')),
('under_renovation', _('En travaux / Rénovation')),
('decommissioned', _('Désaffecté')),
('ceded', _('Cédé / Vendu')),
)
DPE_RATING_CHOICES = (
('A', 'A'),
('B', 'B'),
('C', 'C'),
('D', 'D'),
('E', 'E'),
('F', 'F'),
('G', 'G'),
)
ERP_CATEGORY_CHOICES = (
('1', _('1ère catégorie (> 1500 pers.)')),
('2', _('2ème catégorie (301 - 1500 pers.)')),
('3', _('3ème catégorie (101 - 300 pers.)')),
('4', _('4ème catégorie (jusqu\'à 100 pers.)')),
('5', _('5ème catégorie (seuils spécifiques)')),
)
ERP_TYPE_CHOICES = (
('J', _('J - Structures d\'accueil pour personnes âgées ou handicapées')),
('L', _('L - Salles de spectacles, de réunions, polyvalentes')),
('M', _('M - Magasins, centres commerciaux')),
('N', _('N - Restaurants et débits de boissons')),
('O', _('O - Hôtels et pensions de famille')),
('P', _('P - Salles de danse et salles de jeux')),
('R', _('R - Établissements d\'éveil, d\'enseignement, colonies de vacances')),
('S', _('S - Bibliothèques, centres de documentation')),
('T', _('T - Salles d\'expositions')),
('U', _('U - Établissements de soins')),
('V', _('V - Établissements de culte')),
('W', _('W - Administrations, banques, bureaux')),
('X', _('X - Établissements sportifs couverts')),
('Y', _('Y - Musées')),
)
EQUIPMENT_TYPE_CHOICES = (
('hvac', _('Chauffage / CVC / Chaudière')),
('elevator', _('Ascenseur / Monte-charge')),
('fire_safety', _('Sécurité incendie / Extincteur / RIA')),
('electricity', _('Tableau électrique / TGBT')),
('alarm', _('Alarme / Contrôle d\'accès')),
('meter', _('Compteur (Eau / Gaz / Électricité)')),
('sanitary', _('Plomberie / Sanitaires')),
('other', _('Autre équipement technique')),
)
class PublicBuilding(AbstractGeoAsset):
"""
Modèle représentant un Bâtiment Public (Patrimoine bâti).
"""
geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise cartographique"))
address = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Adresse complète"))
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
# Propriété & Gestion
owner = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Propriétaire"))
manager = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="managed_buildings", verbose_name=_("Gestionnaire référent"))
occupant_entity = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Direction / Service occupant"))
# Usage & Statut
usage_type = models.CharField(max_length=50, choices=USAGE_TYPE_CHOICES, default='administrative', verbose_name=_("Type d'usage principal"))
building_status = models.CharField(max_length=50, choices=BUILDING_STATUS_CHOICES, default='in_service', verbose_name=_("Statut d'occupation"))
# Caractéristiques physiques
construction_year = models.IntegerField(null=True, blank=True, verbose_name=_("Année de construction"))
floors_count = models.IntegerField(default=1, verbose_name=_("Nombre d'étages hors-sol"))
basement_count = models.IntegerField(default=0, verbose_name=_("Nombre de niveaux en sous-sol"))
total_surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface brute totale (m²)"))
usable_surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface utile (m²)"))
# Réglementation & Accueil du Public
is_erp = models.BooleanField(default=True, verbose_name=_("Accueil & Sécurité du Public (ERP / PMR)"))
erp_category = models.CharField(max_length=10, choices=ERP_CATEGORY_CHOICES, null=True, blank=True, verbose_name=_("Catégorie d'accueil / ERP"))
erp_type = models.CharField(max_length=10, choices=ERP_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Type d'établissement / ERP"))
max_capacity = models.IntegerField(null=True, blank=True, verbose_name=_("Jauge maximale d'accueil du public"))
# Diagnostics & Énergie
dpe_rating = models.CharField(max_length=2, choices=DPE_RATING_CHOICES, null=True, blank=True, verbose_name=_("Performance Énergétique (PEB / DPE / EPC)"))
ges_rating = models.CharField(max_length=2, choices=DPE_RATING_CHOICES, null=True, blank=True, verbose_name=_("Émissions de Carbone & GES"))
has_asbestos_diag = models.BooleanField(default=False, verbose_name=_("Diagnostic Amiante réalisé"))
has_lead_diag = models.BooleanField(default=False, verbose_name=_("Diagnostic Plomb réalisé"))
pmr_accessible = models.BooleanField(default=False, verbose_name=_("Accessible PMR"))
class Meta:
verbose_name = _("Bâtiment public")
verbose_name_plural = _("Bâtiments publics")
ordering = ['code', 'name_fr']
def get_absolute_url(self):
return reverse('assets:publicbuilding_detail', kwargs={'pk': self.pk})
def get_usage_type_display(self, standard=None):
"""
Renvoie le libellé du type d'usage adapté dynamiquement à la norme régionale (BE, FR, INT).
"""
from common.utils import get_regional_terms
if self.usage_type == 'administrative':
terms = get_regional_terms(standard or getattr(self, '_regional_standard', 'BE'))
return f"Administratif ({terms['admin_building']})"
return dict(USAGE_TYPE_CHOICES).get(self.usage_type, self.usage_type)
class BuildingLevel(models.Model):
"""
Modèle représentant un niveau / étage au sein d'un bâtiment public.
"""
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="levels", verbose_name=_("Bâtiment"))
code = models.CharField(max_length=50, verbose_name=_("Code du niveau"))
name = models.CharField(max_length=200, verbose_name=_("Nom du niveau"))
level_number = models.IntegerField(default=0, verbose_name=_("Numéro d'étage (0=RDC, 1=1er, -1=Sous-sol)"))
floor_plan = models.FileField(upload_to='buildings/plans/%Y/%m/', storage=private_storage, null=True, blank=True, verbose_name=_("Plan d'étage"))
class Meta:
verbose_name = _("Niveau / Étage")
verbose_name_plural = _("Niveaux / Étages")
ordering = ['building', 'level_number']
def __str__(self):
return f"{self.building.code} - {self.name} ({self.level_number})"
class BuildingRoom(models.Model):
"""
Modèle représentant une pièce / un local dans un bâtiment.
"""
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="rooms", verbose_name=_("Bâtiment"))
level = models.ForeignKey(BuildingLevel, on_delete=models.SET_NULL, null=True, blank=True, related_name="rooms", verbose_name=_("Niveau / Étage"))
code = models.CharField(max_length=50, verbose_name=_("Code du local"))
name = models.CharField(max_length=200, verbose_name=_("Nom de la pièce"))
room_type = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Type de local (ex: Bureau, Classe, Sanitaires)"))
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface (m²)"))
class Meta:
verbose_name = _("Pièce / Local")
verbose_name_plural = _("Pièces / Locaux")
ordering = ['building', 'code']
def __str__(self):
return f"{self.building.code} - {self.code} ({self.name})"
class BuildingEquipment(AbstractAsset):
"""
Modèle représentant un équipement technique hébergé dans un bâtiment.
"""
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="equipments", verbose_name=_("Bâtiment"))
room = models.ForeignKey(BuildingRoom, on_delete=models.SET_NULL, null=True, blank=True, related_name="equipments", verbose_name=_("Local"))
equipment_type = models.CharField(max_length=50, choices=EQUIPMENT_TYPE_CHOICES, default='hvac', verbose_name=_("Type d'équipement"))
last_control_date = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle réglementaire"))
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle réglementaire"))
class Meta:
verbose_name = _("Équipement technique de bâtiment")
verbose_name_plural = _("Équipements techniques de bâtiment")
ordering = ['building', 'code']
def upload_to_building(instance, filename):
return f"buildings/{instance.building.code}/{filename}"
class PublicBuildingDocument(Document):
"""
Documents associés à un bâtiment public (plans, notices ERP, DPE).
"""
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="documents", verbose_name=_("Bâtiment"))
file = models.FileField(upload_to=upload_to_building, storage=private_storage, verbose_name=_("Fichier"), max_length=255)
document_type = models.CharField(max_length=50, default='other', verbose_name=_("Type de document"))
class Meta:
verbose_name = _("Document de bâtiment")
verbose_name_plural = _("Documents de bâtiment")