diff --git a/streetup/assets/admin.py b/streetup/assets/admin.py index 4641362..ec9c7dd 100644 --- a/streetup/assets/admin.py +++ b/streetup/assets/admin.py @@ -648,6 +648,18 @@ admin.site.register(CleanGlassContainer, AssetAdmin) admin.site.register(CleanLocationContract, AssetAdmin) from .models import CleanLitterBinPhoto, CleanLitterBinObservation +from .models import PublicBuilding, BuildingLevel, BuildingRoom, BuildingEquipment, PublicBuildingDocument admin.site.register(CleanLitterBinPhoto, AssetAdmin) -admin.site.register(CleanLitterBinObservation, AssetAdmin) \ No newline at end of file +admin.site.register(CleanLitterBinObservation, AssetAdmin) + +@admin.register(PublicBuilding) +class PublicBuildingAdmin(admin.GISModelAdmin): + list_display = ("code", "name_fr", "usage_type", "building_status", "owner", "manager", "is_erp", "dpe_rating") + list_filter = ("usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible") + search_fields = ("code", "name_fr", "name_nl", "address", "owner") + +admin.site.register(BuildingLevel, AssetAdmin) +admin.site.register(BuildingRoom, AssetAdmin) +admin.site.register(BuildingEquipment, AssetAdmin) +admin.site.register(PublicBuildingDocument, AssetAdmin) \ No newline at end of file diff --git a/streetup/assets/migrations/0005_buildinglevel_publicbuilding_buildingroom_and_more.py b/streetup/assets/migrations/0005_buildinglevel_publicbuilding_buildingroom_and_more.py new file mode 100644 index 0000000..e5b4d14 --- /dev/null +++ b/streetup/assets/migrations/0005_buildinglevel_publicbuilding_buildingroom_and_more.py @@ -0,0 +1,171 @@ +# Generated by Django 6.0.7 on 2026-07-26 13:20 + +import assets.models.buildings +import common.storages +import django.contrib.gis.db.models.fields +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0004_initial'), + ('documents', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BuildingLevel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='buildings/plans/%Y/%m/', verbose_name="Plan d'étage")), + ], + options={ + 'verbose_name': 'Niveau / Étage', + 'verbose_name_plural': 'Niveaux / Étages', + 'ordering': ['building', 'level_number'], + }, + ), + migrations.CreateModel( + name='PublicBuilding', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('lon', models.FloatField(blank=True, null=True)), + ('lat', models.FloatField(blank=True, null=True)), + ('geojson', models.TextField(blank=True, null=True)), + ('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise cartographique')), + ('address', models.CharField(blank=True, max_length=500, null=True, verbose_name='Adresse complète')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description')), + ('owner', models.CharField(blank=True, max_length=255, null=True, verbose_name='Propriétaire')), + ('occupant_entity', models.CharField(blank=True, max_length=255, null=True, verbose_name='Direction / Service occupant')), + ('usage_type', models.CharField(choices=[('administrative', 'Administratif / Mairie'), ('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')], default='administrative', max_length=50, verbose_name="Type d'usage principal")), + ('building_status', models.CharField(choices=[('in_service', 'En service'), ('under_renovation', 'En travaux / Rénovation'), ('decommissioned', 'Désaffecté'), ('ceded', 'Cédé / Vendu')], default='in_service', max_length=50, verbose_name="Statut d'occupation")), + ('construction_year', models.IntegerField(blank=True, null=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(blank=True, null=True, verbose_name='Surface brute totale (m²)')), + ('usable_surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface utile (m²)')), + ('is_erp', models.BooleanField(default=True, verbose_name='Établissement Recevant du Public (ERP)')), + ('erp_category', models.CharField(blank=True, 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)')], max_length=10, null=True, verbose_name='Catégorie ERP')), + ('erp_type', models.CharField(blank=True, 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')], max_length=10, null=True, verbose_name='Type ERP')), + ('max_capacity', models.IntegerField(blank=True, null=True, verbose_name="Jauge maximale d'accueil du public")), + ('dpe_rating', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Diagnostic de Performance Énergétique (DPE)')), + ('ges_rating', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=2, null=True, verbose_name='Émission de Gaz à Effet de Serre (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')), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')), + ('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_buildings', to=settings.AUTH_USER_MODEL, verbose_name='Gestionnaire référent')), + ('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')), + ('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.publicbuilding', verbose_name='Remplacé par')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')), + ], + options={ + 'verbose_name': 'Bâtiment public', + 'verbose_name_plural': 'Bâtiments publics', + 'ordering': ['code', 'name_fr'], + }, + ), + migrations.CreateModel( + name='BuildingRoom', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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(blank=True, max_length=100, null=True, verbose_name='Type de local (ex: Bureau, Classe, Sanitaires)')), + ('surface_m2', models.FloatField(blank=True, null=True, verbose_name='Surface (m²)')), + ('level', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rooms', to='assets.buildinglevel', verbose_name='Niveau / Étage')), + ('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='assets.publicbuilding', verbose_name='Bâtiment')), + ], + options={ + 'verbose_name': 'Pièce / Local', + 'verbose_name_plural': 'Pièces / Locaux', + 'ordering': ['building', 'code'], + }, + ), + migrations.AddField( + model_name='buildinglevel', + name='building', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='assets.publicbuilding', verbose_name='Bâtiment'), + ), + migrations.CreateModel( + name='BuildingEquipment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(max_length=50)), + ('name_fr', models.CharField(blank=True, max_length=200, null=True)), + ('name_nl', models.CharField(blank=True, max_length=200, null=True)), + ('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)), + ('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)), + ('installation_date', models.DateTimeField(blank=True, null=True)), + ('uninstallation_date', models.DateTimeField(blank=True, null=True)), + ('serial_number', models.CharField(blank=True, max_length=200, null=True)), + ('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')), + ('last_inspection_date', models.DateTimeField(blank=True, null=True)), + ('last_preventive_date', models.DateTimeField(blank=True, null=True)), + ('last_corrective_date', models.DateTimeField(blank=True, null=True)), + ('last_survey_date', models.DateTimeField(blank=True, null=True)), + ('brand', models.CharField(blank=True, max_length=500, null=True)), + ('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')), + ('note', models.TextField(blank=True, null=True, verbose_name='Note')), + ('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('equipment_type', models.CharField(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')], default='hvac', max_length=50, verbose_name="Type d'équipement")), + ('last_control_date', models.DateField(blank=True, null=True, verbose_name='Dernier contrôle réglementaire')), + ('next_control_date', models.DateField(blank=True, null=True, verbose_name='Prochain contrôle réglementaire')), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')), + ('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')), + ('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.buildingequipment', verbose_name='Remplacé par')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')), + ('room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='equipments', to='assets.buildingroom', verbose_name='Local')), + ('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='equipments', to='assets.publicbuilding', verbose_name='Bâtiment')), + ], + options={ + 'verbose_name': 'Équipement technique de bâtiment', + 'verbose_name_plural': 'Équipements techniques de bâtiment', + 'ordering': ['building', 'code'], + }, + ), + migrations.CreateModel( + name='PublicBuildingDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description du document')), + ('upload_date', models.DateTimeField(auto_now=True, verbose_name='Date de téléversement')), + ('status', models.CharField(choices=[('active', 'Actif'), ('archived', 'Archivé')], default='active')), + ('file', models.FileField(max_length=255, storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.buildings.upload_to_building, verbose_name='Fichier')), + ('document_type', models.CharField(default='other', max_length=50, verbose_name='Type de document')), + ('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.publicbuilding', verbose_name='Bâtiment')), + ('managed_document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_sources', to='documents.manageddocument')), + ('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Ajouté par')), + ], + options={ + 'verbose_name': 'Document de bâtiment', + 'verbose_name_plural': 'Documents de bâtiment', + }, + ), + ] diff --git a/streetup/assets/models/__init__.py b/streetup/assets/models/__init__.py index 35e2c7e..d98e884 100644 --- a/streetup/assets/models/__init__.py +++ b/streetup/assets/models/__init__.py @@ -9,4 +9,5 @@ from .controlcenters import ControlCenter, ControlCenterLocation, ControlCenterG from .sign import SignStreet, SignStreetDocument, SignPole, SignPoleModel, SignPanel, SignPanelModel, SignPanelType, ComplementaryPoliceRegulation, SignParkingSpot from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract -from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract \ No newline at end of file +from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract +from .buildings import PublicBuilding, BuildingLevel, BuildingRoom, BuildingEquipment, PublicBuildingDocument \ No newline at end of file diff --git a/streetup/assets/models/buildings.py b/streetup/assets/models/buildings.py new file mode 100644 index 0000000..83b9e55 --- /dev/null +++ b/streetup/assets/models/buildings.py @@ -0,0 +1,197 @@ +# ----------------------------- +# 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() + +USAGE_TYPE_CHOICES = ( + ('administrative', _('Administratif / Mairie')), + ('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')), +) + +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 ERP + is_erp = models.BooleanField(default=True, verbose_name=_("Établissement Recevant du Public (ERP)")) + erp_category = models.CharField(max_length=10, choices=ERP_CATEGORY_CHOICES, null=True, blank=True, verbose_name=_("Catégorie ERP")) + erp_type = models.CharField(max_length=10, choices=ERP_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Type 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=_("Diagnostic de Performance Énergétique (DPE)")) + ges_rating = models.CharField(max_length=2, choices=DPE_RATING_CHOICES, null=True, blank=True, verbose_name=_("Émission de Gaz à Effet de Serre (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}) + + +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") + diff --git a/streetup/assets/static/assets/asset_add_modal.js b/streetup/assets/static/assets/asset_add_modal.js index d21cdd8..f201b11 100644 --- a/streetup/assets/static/assets/asset_add_modal.js +++ b/streetup/assets/static/assets/asset_add_modal.js @@ -588,7 +588,9 @@ 'controlcenters': 'controlcentergeoasset', 'its': 'itsgeoasset', 'sign': 'signgeoasset', - 'roads': 'roadgeoasset' + 'roads': 'roadgeoasset', + 'publicbuildings': 'publicbuilding', + 'buildings': 'publicbuilding' }; return mapping[thematicCode] || 'geoasset'; } diff --git a/streetup/assets/static/assets/unified_index.js b/streetup/assets/static/assets/unified_index.js index fab0123..733a369 100644 --- a/streetup/assets/static/assets/unified_index.js +++ b/streetup/assets/static/assets/unified_index.js @@ -38,7 +38,7 @@ var locationsFetchSignatures = {}; var assetsFetchSignatures = {}; // Assets: thematics that always load all assets (small datasets) -const ALWAYS_LOAD_ASSETS_THEMATICS = new Set(['artworks', 'controlcenters']); +const ALWAYS_LOAD_ASSETS_THEMATICS = new Set(['artworks', 'controlcenters', 'publicbuildings', 'buildings']); const MIN_ASSETS_ZOOM = 14; // zoom level at which bbox-based asset loading triggers var assetsLoadModes = {}; // thematic_code → null | 'always' | 'filter' | 'bbox' diff --git a/streetup/assets/static/assets/unified_map_layers.js b/streetup/assets/static/assets/unified_map_layers.js index 40a150d..956a583 100644 --- a/streetup/assets/static/assets/unified_map_layers.js +++ b/streetup/assets/static/assets/unified_map_layers.js @@ -209,6 +209,8 @@ function loadUnifiedMapIcons(map) { { name: 'icon-artworkfountain', codepoint: 'f617', color: '#1e88e5' }, // water — bleu fontaine { name: 'icon-artworkmonument', codepoint: 'f8f9', color: '#f24141' }, // person-standing — statue { name: 'icon-controlcenters', codepoint: 'f301', color: '#7b1fa2' }, // display-fill — violet + { name: 'icon-publicbuildings', codepoint: 'f1ab', color: '#2B579A' }, // building — bleu nuit + { name: 'icon-publicbuilding', codepoint: 'f1ab', color: '#2B579A' }, // building — bleu nuit { name: 'icon-roads', codepoint: 'f47e', color: '#607d8b' }, // map-fill — bleu-gris ]; @@ -285,6 +287,8 @@ const THEMATIC_COLOR_EXPR = [ 'clean', '#5e5e5e', // grey — cleanliness 'artworks', '#f24141', // coral — artworks / monuments 'controlcenters', '#7b1fa2', // purple — control centres + 'publicbuildings', '#2B579A', // navy blue — public buildings + 'buildings', '#2B579A', // navy blue — public buildings 'roads', [ 'match', ['get', 'asset_type'], 'roadway', '#4a4a4a', @@ -508,6 +512,8 @@ const _unified_assets_symbol = { 'clean', 'icon-clean', 'artworks', 'icon-artworks', 'controlcenters', 'icon-controlcenters', + 'publicbuildings', 'icon-publicbuildings', + 'publicbuilding', 'icon-publicbuilding', 'roads', 'icon-roads', 'icon-roads' // fallback ] diff --git a/streetup/assets/templates/assets/buildings/publicbuilding_detail.html b/streetup/assets/templates/assets/buildings/publicbuilding_detail.html new file mode 100644 index 0000000..84e0857 --- /dev/null +++ b/streetup/assets/templates/assets/buildings/publicbuilding_detail.html @@ -0,0 +1,193 @@ +{% extends "home/base.html" %} +{% load static i18n %} + +{% block title %}{{ building.code }} - {{ building.get_name }} | StreetUp{% endblock %} + +{% block content %} +
{{ building.address|default:_("Adresse non renseignée") }}
+| {% translate "Propriétaire" %} : | +{{ building.owner|default:"-" }} | +
|---|---|
| {% translate "Gestionnaire référent" %} : | +{{ building.manager.get_full_name|default:building.manager.username|default:"-" }} | +
| {% translate "Service occupant" %} : | +{{ building.occupant_entity|default:"-" }} | +
| {% translate "Année de construction" %} : | +{{ building.construction_year|default:"-" }} | +
|---|---|
| {% translate "Étages / Sous-sols" %} : | +{{ building.floors_count }} étage(s) / {{ building.basement_count }} sous-sol(s) | +
| {% translate "Surface Utile / Brut" %} : | +{{ building.usable_surface_m2|default:"-" }} m² / {{ building.total_surface_m2|default:"-" }} m² | +
| {% translate "Jauge ERP" %} : | +{{ building.max_capacity|default:"-" }} personnes | +
| {% translate "Accessibilité PMR" %} : | +{% if building.pmr_accessible %}Oui{% else %}Non{% endif %} | +
{% translate "Aucun niveau enregistré pour ce bâtiment." %}
+ {% endfor %} +| {% translate "Code / Nom" %} | +{% translate "Type d'équipement" %} | +{% translate "Local" %} | +{% translate "Prochain contrôle" %} | +
|---|---|---|---|
| {{ eq.code }} - {{ eq.get_name }} | +{{ eq.get_equipment_type_display }} | +{{ eq.room.name|default:"-" }} | +{{ eq.next_control_date|default:"-" }} | +
| {% translate "Aucun équipement technique enregistré." %} | +|||
{% translate "Patrimoine bâti, pièces, équipements techniques et diagnostics ERP" %}
+| {% translate "Code / Nom" %} | +{% translate "Usage principal" %} | +{% translate "Propriétaire & Gestionnaire" %} | +{% translate "ERP" %} | +{% translate "DPE / GES" %} | +{% translate "Statut" %} | +{% translate "Actions" %} | +
|---|---|---|---|---|---|---|
|
+
+ {{ b.code }}
+
+ {{ b.get_name }}
+ |
+ + + {{ b.get_usage_type_display }} + + | +
+ {% if b.owner %}
+ {% translate "Propriétaire" %}: {{ b.owner }}
+ {% endif %}
+ {% if b.manager %}
+ {% translate "Gestionnaire" %}: {{ b.manager.get_full_name|default:b.manager.username }}
+ {% endif %}
+ |
+ + {% if b.is_erp %} + + ERP Cat {{ b.erp_category|default:"-" }} ({{ b.erp_type|default:"-" }}) + + {% else %} + {% translate "Non ERP" %} + {% endif %} + | ++ {% if b.dpe_rating %} + DPE {{ b.dpe_rating }} + {% endif %} + {% if b.ges_rating %} + GES {{ b.ges_rating }} + {% endif %} + | ++ + {{ b.get_building_status_display }} + + | ++ + + + + + + | +
| + + {% translate "Aucun bâtiment public ne correspond à vos critères." %} + | +||||||