refactor: unify building location hierarchy and consolidate asset management using a recursive tree structure
This commit is contained in:
parent
a4f6769c7d
commit
044e01add8
15 changed files with 921 additions and 265 deletions
|
|
@ -648,18 +648,20 @@ admin.site.register(CleanGlassContainer, AssetAdmin)
|
||||||
admin.site.register(CleanLocationContract, AssetAdmin)
|
admin.site.register(CleanLocationContract, AssetAdmin)
|
||||||
|
|
||||||
from .models import CleanLitterBinPhoto, CleanLitterBinObservation
|
from .models import CleanLitterBinPhoto, CleanLitterBinObservation
|
||||||
from .models import PublicBuilding, BuildingLevel, BuildingRoom, BuildingEquipment, PublicBuildingDocument
|
from .models import PublicBuilding, BuildingAsset, PublicBuildingDocument
|
||||||
|
|
||||||
admin.site.register(CleanLitterBinPhoto, AssetAdmin)
|
|
||||||
admin.site.register(CleanLitterBinObservation, AssetAdmin)
|
|
||||||
|
|
||||||
@admin.register(PublicBuilding)
|
@admin.register(PublicBuilding)
|
||||||
class PublicBuildingAdmin(admin.GISModelAdmin):
|
class PublicBuildingAdmin(admin.GISModelAdmin):
|
||||||
list_display = ("code", "name_fr", "usage_type", "building_status", "owner", "manager", "is_erp", "dpe_rating")
|
list_display = ("code", "name_fr", "location_type", "parent_location", "usage_type", "building_status", "owner", "manager", "is_erp", "dpe_rating")
|
||||||
list_filter = ("usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible")
|
list_filter = ("location_type", "usage_type", "building_status", "is_erp", "erp_category", "dpe_rating", "pmr_accessible")
|
||||||
search_fields = ("code", "name_fr", "name_nl", "address", "owner")
|
search_fields = ("code", "name_fr", "name_nl", "address", "owner")
|
||||||
|
raw_id_fields = ("parent_location", "manager")
|
||||||
|
|
||||||
|
@admin.register(BuildingAsset)
|
||||||
|
class BuildingAssetAdmin(admin.GISModelAdmin):
|
||||||
|
list_display = ("code", "name_fr", "asset_type", "building", "location", "parent_asset", "status", "obsolescence")
|
||||||
|
list_filter = ("asset_type", "status", "obsolescence")
|
||||||
|
search_fields = ("code", "name_fr", "name_nl", "serial_number")
|
||||||
|
raw_id_fields = ("building", "location", "parent_asset")
|
||||||
|
|
||||||
admin.site.register(BuildingLevel, AssetAdmin)
|
|
||||||
admin.site.register(BuildingRoom, AssetAdmin)
|
|
||||||
admin.site.register(BuildingEquipment, AssetAdmin)
|
|
||||||
admin.site.register(PublicBuildingDocument, AssetAdmin)
|
admin.site.register(PublicBuildingDocument, AssetAdmin)
|
||||||
|
|
@ -274,17 +274,19 @@ LOCATION_FORM_REGISTRY = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
from assets.models.buildings import PublicBuilding, BuildingAsset
|
||||||
|
|
||||||
class PublicBuildingForm(forms.ModelForm):
|
class PublicBuildingForm(forms.ModelForm):
|
||||||
"""Formulaire de création et d'édition d'un Bâtiment Public."""
|
"""Formulaire de création et d'édition d'une Localisation de Bâtiment Public."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PublicBuilding
|
model = PublicBuilding
|
||||||
fields = [
|
fields = [
|
||||||
'code', 'name_fr', 'name_nl', 'address', 'description',
|
'code', 'name_fr', 'name_nl', 'parent_location', 'location_type', 'is_main_location',
|
||||||
'owner', 'manager', 'occupant_entity',
|
'address', 'description', 'owner', 'manager', 'occupant_entity',
|
||||||
'usage_type', 'building_status',
|
'usage_type', 'building_status',
|
||||||
'construction_year', 'floors_count', 'basement_count',
|
'construction_year', 'floors_count', 'basement_count',
|
||||||
'total_surface_m2', 'usable_surface_m2',
|
'total_surface_m2', 'usable_surface_m2', 'surface_m2', 'level_number',
|
||||||
'is_erp', 'erp_category', 'erp_type', 'max_capacity', 'pmr_accessible',
|
'is_erp', 'erp_category', 'erp_type', 'max_capacity', 'pmr_accessible',
|
||||||
'dpe_rating', 'ges_rating', 'has_asbestos_diag', 'has_lead_diag',
|
'dpe_rating', 'ges_rating', 'has_asbestos_diag', 'has_lead_diag',
|
||||||
'lat', 'lon',
|
'lat', 'lon',
|
||||||
|
|
@ -293,6 +295,9 @@ class PublicBuildingForm(forms.ModelForm):
|
||||||
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'name_fr': forms.TextInput(attrs={'class': 'form-control'}),
|
'name_fr': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'name_nl': forms.TextInput(attrs={'class': 'form-control'}),
|
'name_nl': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'parent_location': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'location_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'is_main_location': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
'address': forms.TextInput(attrs={'class': 'form-control'}),
|
'address': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
'owner': forms.TextInput(attrs={'class': 'form-control'}),
|
'owner': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
|
@ -305,6 +310,8 @@ class PublicBuildingForm(forms.ModelForm):
|
||||||
'basement_count': forms.NumberInput(attrs={'class': 'form-control'}),
|
'basement_count': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
'total_surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
'total_surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
||||||
'usable_surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
'usable_surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
||||||
|
'surface_m2': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any'}),
|
||||||
|
'level_number': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||||
'is_erp': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
'is_erp': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
'erp_category': forms.Select(attrs={'class': 'form-select'}),
|
'erp_category': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'erp_type': forms.Select(attrs={'class': 'form-select'}),
|
'erp_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
|
@ -322,6 +329,8 @@ class PublicBuildingForm(forms.ModelForm):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['name_fr'].required = False
|
self.fields['name_fr'].required = False
|
||||||
self.fields['name_nl'].required = False
|
self.fields['name_nl'].required = False
|
||||||
|
self.fields['parent_location'].required = False
|
||||||
|
self.fields['location_type'].required = False
|
||||||
self.fields['address'].required = False
|
self.fields['address'].required = False
|
||||||
self.fields['description'].required = False
|
self.fields['description'].required = False
|
||||||
self.fields['owner'].required = False
|
self.fields['owner'].required = False
|
||||||
|
|
@ -332,6 +341,8 @@ class PublicBuildingForm(forms.ModelForm):
|
||||||
self.fields['basement_count'].required = False
|
self.fields['basement_count'].required = False
|
||||||
self.fields['total_surface_m2'].required = False
|
self.fields['total_surface_m2'].required = False
|
||||||
self.fields['usable_surface_m2'].required = False
|
self.fields['usable_surface_m2'].required = False
|
||||||
|
self.fields['surface_m2'].required = False
|
||||||
|
self.fields['level_number'].required = False
|
||||||
self.fields['erp_category'].required = False
|
self.fields['erp_category'].required = False
|
||||||
self.fields['erp_type'].required = False
|
self.fields['erp_type'].required = False
|
||||||
self.fields['max_capacity'].required = False
|
self.fields['max_capacity'].required = False
|
||||||
|
|
@ -339,3 +350,42 @@ class PublicBuildingForm(forms.ModelForm):
|
||||||
self.fields['ges_rating'].required = False
|
self.fields['ges_rating'].required = False
|
||||||
self.fields['lat'].required = False
|
self.fields['lat'].required = False
|
||||||
self.fields['lon'].required = False
|
self.fields['lon'].required = False
|
||||||
|
|
||||||
|
|
||||||
|
class BuildingAssetForm(forms.ModelForm):
|
||||||
|
"""Formulaire pour les assets/équipements physiques d'un bâtiment."""
|
||||||
|
class Meta:
|
||||||
|
model = BuildingAsset
|
||||||
|
fields = [
|
||||||
|
'code', 'name_fr', 'name_nl', 'building', 'location', 'parent_asset',
|
||||||
|
'asset_type', 'category', 'status', 'obsolescence', 'installation_date',
|
||||||
|
'serial_number', 'last_control_date', 'next_control_date', 'note'
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'code': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'name_fr': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'name_nl': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'building': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'location': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'parent_asset': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'asset_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'category': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'obsolescence': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'installation_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
||||||
|
'serial_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
|
'last_control_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||||
|
'next_control_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||||
|
'note': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for field_name in ['name_fr', 'name_nl', 'location', 'parent_asset', 'category', 'installation_date', 'serial_number', 'last_control_date', 'next_control_date', 'note']:
|
||||||
|
if field_name in self.fields:
|
||||||
|
self.fields[field_name].required = False
|
||||||
|
from common.models import Thematic
|
||||||
|
from assets.models import AssetCategory
|
||||||
|
t = Thematic.objects.filter(code='publicbuildings').first()
|
||||||
|
if t and 'category' in self.fields:
|
||||||
|
self.fields['category'].queryset = AssetCategory.objects.filter(thematic=t)
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-07-27 12:00
|
||||||
|
|
||||||
|
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', '0005_buildinglevel_publicbuilding_buildingroom_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='buildinglevel',
|
||||||
|
name='building',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='buildingroom',
|
||||||
|
name='level',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='buildingroom',
|
||||||
|
name='building',
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='publicbuilding',
|
||||||
|
options={'ordering': ['code', 'name_fr'], 'verbose_name': 'Localisation Bâtiment public', 'verbose_name_plural': 'Localisations Bâtiments publics'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='brand',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='category',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='installation_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='last_corrective_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='last_inspection_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='last_preventive_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='last_survey_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='marked_as_removed_by',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='marked_as_removed_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='note',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='obsolescence',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='replaced_by',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='serial_number',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='status',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='uninstallation_date',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='validation_status',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='warranty_duration',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='floor_plan',
|
||||||
|
field=models.FileField(blank=True, null=True, storage=common.storages.PrivateMediaStorage(), upload_to='buildings/plans/%Y/%m/', verbose_name="Plan d'étage"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='is_main_location',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Localisation principale'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='level_number',
|
||||||
|
field=models.IntegerField(default=0, verbose_name="Numéro d'étage (ex: 0=RDC, 1=1er, -1=Sous-sol)"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='location_type',
|
||||||
|
field=models.CharField(choices=[('site', 'Site / Complexe'), ('building', 'Bâtiment / Bloc'), ('level', 'Niveau / Étage'), ('room', 'Pièce / Local'), ('other', 'Autre localisation')], default='building', max_length=50, verbose_name='Type de localisation'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='parent_location',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_locations', to='assets.publicbuilding', verbose_name='Localisation parente'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='surface_m2',
|
||||||
|
field=models.FloatField(blank=True, null=True, verbose_name='Surface spécifique (m²)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='dpe_rating',
|
||||||
|
field=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='Performance Énergétique (PEB / DPE / EPC)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='erp_category',
|
||||||
|
field=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 d'accueil / ERP"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='erp_type',
|
||||||
|
field=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 d'établissement / ERP"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='ges_rating',
|
||||||
|
field=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='Émissions de Carbone & GES'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='is_erp',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Accueil & Sécurité du Public (ERP / PMR)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='updated_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='publicbuilding',
|
||||||
|
name='usage_type',
|
||||||
|
field=models.CharField(choices=[('administrative', 'Administrative (Town Hall / Municipal Building)'), ('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"),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BuildingAsset',
|
||||||
|
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)),
|
||||||
|
('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812)),
|
||||||
|
('geojson', models.TextField(blank=True, null=True)),
|
||||||
|
('asset_type', models.CharField(choices=[('civil_engineering', 'Génie civil / Bâti'), ('facade', 'Façade / Mur extérieur'), ('wall', 'Mur intérieur / Cloison'), ('roof', 'Toiture / Couverture'), ('hvac', 'Chauffage / CVC / Chaudière'), ('elevator', 'Ascenseur / Monte-charge'), ('electricity', 'Électricité / TGBT'), ('fire_safety', 'Sécurité incendie / Extincteur / RIA'), ('alarm', "Alarme / Contrôle d'accès"), ('meter', 'Compteur (Eau / Gaz / Électricité)'), ('sanitary', 'Plomberie / Sanitaires'), ('other', 'Autre équipement / asset')], default='civil_engineering', max_length=50, verbose_name="Type d'asset / É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')),
|
||||||
|
('building', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_building_assets', to='assets.publicbuilding', verbose_name='Bâtiment / Site principal')),
|
||||||
|
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
||||||
|
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='location_assets', to='assets.publicbuilding', verbose_name='Localisation spécifique')),
|
||||||
|
('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')),
|
||||||
|
('parent_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_assets', to='assets.buildingasset', verbose_name='Asset parent')),
|
||||||
|
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.buildingasset', 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': 'Asset / Équipement de bâtiment',
|
||||||
|
'verbose_name_plural': 'Assets / Équipements de bâtiment',
|
||||||
|
'ordering': ['building', 'code'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='BuildingEquipment',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='BuildingLevel',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='BuildingRoom',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -10,4 +10,4 @@ from .sign import SignStreet, SignStreetDocument, SignPole, SignPoleModel, SignP
|
||||||
from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain
|
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 .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract
|
||||||
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
|
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
|
||||||
from .buildings import PublicBuilding, BuildingLevel, BuildingRoom, BuildingEquipment, PublicBuildingDocument
|
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
|
||||||
|
|
@ -7,7 +7,7 @@ from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from assets.models import AbstractAsset, AbstractGeoAsset, AbstractAssetModel, ASSET_STATUS_CHOICES
|
from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel, ASSET_STATUS_CHOICES
|
||||||
from common.models import Document
|
from common.models import Document
|
||||||
from common.storages import PrivateMediaStorage
|
from common.storages import PrivateMediaStorage
|
||||||
|
|
||||||
|
|
@ -76,23 +76,43 @@ ERP_TYPE_CHOICES = (
|
||||||
('Y', _('Y - Musées')),
|
('Y', _('Y - Musées')),
|
||||||
)
|
)
|
||||||
|
|
||||||
EQUIPMENT_TYPE_CHOICES = (
|
LOCATION_TYPE_CHOICES = (
|
||||||
|
('site', _('Site / Complexe')),
|
||||||
|
('building', _('Bâtiment / Bloc')),
|
||||||
|
('level', _('Niveau / Étage')),
|
||||||
|
('room', _('Pièce / Local')),
|
||||||
|
('other', _('Autre localisation')),
|
||||||
|
)
|
||||||
|
|
||||||
|
BUILDING_ASSET_TYPE_CHOICES = (
|
||||||
|
('civil_engineering', _('Génie civil / Bâti')),
|
||||||
|
('facade', _('Façade / Mur extérieur')),
|
||||||
|
('wall', _('Mur intérieur / Cloison')),
|
||||||
|
('roof', _('Toiture / Couverture')),
|
||||||
('hvac', _('Chauffage / CVC / Chaudière')),
|
('hvac', _('Chauffage / CVC / Chaudière')),
|
||||||
('elevator', _('Ascenseur / Monte-charge')),
|
('elevator', _('Ascenseur / Monte-charge')),
|
||||||
|
('electricity', _('Électricité / TGBT')),
|
||||||
('fire_safety', _('Sécurité incendie / Extincteur / RIA')),
|
('fire_safety', _('Sécurité incendie / Extincteur / RIA')),
|
||||||
('electricity', _('Tableau électrique / TGBT')),
|
|
||||||
('alarm', _('Alarme / Contrôle d\'accès')),
|
('alarm', _('Alarme / Contrôle d\'accès')),
|
||||||
('meter', _('Compteur (Eau / Gaz / Électricité)')),
|
('meter', _('Compteur (Eau / Gaz / Électricité)')),
|
||||||
('sanitary', _('Plomberie / Sanitaires')),
|
('sanitary', _('Plomberie / Sanitaires')),
|
||||||
('other', _('Autre équipement technique')),
|
('other', _('Autre équipement / asset')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# For backward compatibility
|
||||||
|
EQUIPMENT_TYPE_CHOICES = BUILDING_ASSET_TYPE_CHOICES
|
||||||
|
|
||||||
class PublicBuilding(AbstractGeoAsset):
|
|
||||||
|
class PublicBuilding(AbstractLocation):
|
||||||
"""
|
"""
|
||||||
Modèle représentant un Bâtiment Public (Patrimoine bâti).
|
Modèle représentant une Localisation du Patrimoine Bâti (Complexe, Bâtiment, Bloc, Étage, Local).
|
||||||
|
Les localisations peuvent former une arborescence parent-enfant.
|
||||||
"""
|
"""
|
||||||
geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise cartographique"))
|
geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise cartographique"))
|
||||||
|
parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations", verbose_name=_("Localisation parente"))
|
||||||
|
location_type = models.CharField(max_length=50, choices=LOCATION_TYPE_CHOICES, default='building', verbose_name=_("Type de localisation"))
|
||||||
|
is_main_location = models.BooleanField(default=False, verbose_name=_("Localisation principale"))
|
||||||
|
|
||||||
address = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Adresse complète"))
|
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"))
|
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
|
||||||
|
|
||||||
|
|
@ -111,6 +131,9 @@ class PublicBuilding(AbstractGeoAsset):
|
||||||
basement_count = models.IntegerField(default=0, verbose_name=_("Nombre de niveaux en sous-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²)"))
|
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²)"))
|
usable_surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface utile (m²)"))
|
||||||
|
surface_m2 = models.FloatField(null=True, blank=True, verbose_name=_("Surface spécifique (m²)"))
|
||||||
|
level_number = models.IntegerField(default=0, verbose_name=_("Numéro d'étage (ex: 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"))
|
||||||
|
|
||||||
# Réglementation & Accueil du Public
|
# Réglementation & Accueil du Public
|
||||||
is_erp = models.BooleanField(default=True, verbose_name=_("Accueil & Sécurité du Public (ERP / PMR)"))
|
is_erp = models.BooleanField(default=True, verbose_name=_("Accueil & Sécurité du Public (ERP / PMR)"))
|
||||||
|
|
@ -126,8 +149,8 @@ class PublicBuilding(AbstractGeoAsset):
|
||||||
pmr_accessible = models.BooleanField(default=False, verbose_name=_("Accessible PMR"))
|
pmr_accessible = models.BooleanField(default=False, verbose_name=_("Accessible PMR"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Bâtiment public")
|
verbose_name = _("Localisation Bâtiment public")
|
||||||
verbose_name_plural = _("Bâtiments publics")
|
verbose_name_plural = _("Localisations Bâtiments publics")
|
||||||
ordering = ['code', 'name_fr']
|
ordering = ['code', 'name_fr']
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
|
|
@ -143,63 +166,65 @@ class PublicBuilding(AbstractGeoAsset):
|
||||||
return f"Administratif ({terms['admin_building']})"
|
return f"Administratif ({terms['admin_building']})"
|
||||||
return dict(USAGE_TYPE_CHOICES).get(self.usage_type, self.usage_type)
|
return dict(USAGE_TYPE_CHOICES).get(self.usage_type, self.usage_type)
|
||||||
|
|
||||||
|
def get_child_locations(self):
|
||||||
|
return self.child_locations.all()
|
||||||
|
|
||||||
class BuildingLevel(models.Model):
|
def get_active_assets(self):
|
||||||
|
def collect_assets(loc):
|
||||||
|
assets = list(loc.location_assets.filter(status='active'))
|
||||||
|
for child in loc.child_locations.all():
|
||||||
|
assets += collect_assets(child)
|
||||||
|
return assets
|
||||||
|
return collect_assets(self)
|
||||||
|
|
||||||
|
def get_all_assets(self):
|
||||||
|
def collect_assets(loc):
|
||||||
|
if loc.parent_location is None:
|
||||||
|
assets = list(loc.location_assets.select_related('location', 'parent_asset', 'building').all() | loc.all_building_assets.select_related('location', 'parent_asset', 'building').filter(location__isnull=True))
|
||||||
|
else:
|
||||||
|
assets = list(loc.location_assets.select_related('location', 'parent_asset', 'building').all())
|
||||||
|
for child in loc.child_locations.all():
|
||||||
|
assets += collect_assets(child)
|
||||||
|
return assets
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
unique_assets = []
|
||||||
|
for a in collect_assets(self):
|
||||||
|
if a.id not in seen:
|
||||||
|
seen.add(a.id)
|
||||||
|
unique_assets.append(a)
|
||||||
|
return unique_assets
|
||||||
|
|
||||||
|
def get_direct_child_assets(self):
|
||||||
|
return self.location_assets.all()
|
||||||
|
|
||||||
|
|
||||||
|
class BuildingAsset(AbstractGeoAsset):
|
||||||
"""
|
"""
|
||||||
Modèle représentant un niveau / étage au sein d'un bâtiment public.
|
Modèle représentant un asset ou équipement physique du bâtiment (Génie civil / Bâti, Façades, Toitures, CVC, Ascenseurs...).
|
||||||
|
Les assets peuvent former une arborescence parent-enfant (ex: Génie civil -> Façade Nord, Toiture, Ascenseur).
|
||||||
"""
|
"""
|
||||||
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="levels", verbose_name=_("Bâtiment"))
|
building = models.ForeignKey(PublicBuilding, on_delete=models.CASCADE, related_name="all_building_assets", verbose_name=_("Bâtiment / Site principal"))
|
||||||
code = models.CharField(max_length=50, verbose_name=_("Code du niveau"))
|
location = models.ForeignKey(PublicBuilding, on_delete=models.SET_NULL, null=True, blank=True, related_name="location_assets", verbose_name=_("Localisation spécifique"))
|
||||||
name = models.CharField(max_length=200, verbose_name=_("Nom du niveau"))
|
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets", verbose_name=_("Asset parent"))
|
||||||
level_number = models.IntegerField(default=0, verbose_name=_("Numéro d'étage (0=RDC, 1=1er, -1=Sous-sol)"))
|
asset_type = models.CharField(max_length=50, choices=BUILDING_ASSET_TYPE_CHOICES, default='civil_engineering', verbose_name=_("Type d'asset / Équipement"))
|
||||||
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"))
|
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"))
|
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle réglementaire"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Équipement technique de bâtiment")
|
verbose_name = _("Asset / Équipement de bâtiment")
|
||||||
verbose_name_plural = _("Équipements techniques de bâtiment")
|
verbose_name_plural = _("Assets / Équipements de bâtiment")
|
||||||
ordering = ['building', 'code']
|
ordering = ['building', 'code']
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingequipment', 'asset_id': self.pk})
|
return reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingasset', 'asset_id': self.pk})
|
||||||
|
|
||||||
|
def get_child_assets(self):
|
||||||
|
return self.child_assets.all()
|
||||||
|
|
||||||
|
|
||||||
|
# Backward compatibility alias
|
||||||
|
BuildingEquipment = BuildingAsset
|
||||||
|
|
||||||
|
|
||||||
def upload_to_building(instance, filename):
|
def upload_to_building(instance, filename):
|
||||||
|
|
@ -217,4 +242,3 @@ class PublicBuildingDocument(Document):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Document de bâtiment")
|
verbose_name = _("Document de bâtiment")
|
||||||
verbose_name_plural = _("Documents de bâtiment")
|
verbose_name_plural = _("Documents de bâtiment")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,8 @@ def _get_location_thematic(location):
|
||||||
'cleanlocation': 'clean',
|
'cleanlocation': 'clean',
|
||||||
'publiclightingstreet': 'publiclighting',
|
'publiclightingstreet': 'publiclighting',
|
||||||
'signstreet': 'sign',
|
'signstreet': 'sign',
|
||||||
|
'publicbuilding': 'publicbuildings',
|
||||||
|
'buildinglocation': 'publicbuildings',
|
||||||
}
|
}
|
||||||
thematic_code = thematic_mappings.get(class_name)
|
thematic_code = thematic_mappings.get(class_name)
|
||||||
if not thematic_code:
|
if not thematic_code:
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ var locationsFetchSignatures = {};
|
||||||
var assetsFetchSignatures = {};
|
var assetsFetchSignatures = {};
|
||||||
|
|
||||||
// Assets: thematics that always load all assets (small datasets)
|
// Assets: thematics that always load all assets (small datasets)
|
||||||
const ALWAYS_LOAD_ASSETS_THEMATICS = new Set(['artworks', 'controlcenters', 'publicbuildings', 'buildings']);
|
const ALWAYS_LOAD_ASSETS_THEMATICS = new Set(['artworks', 'controlcenters']);
|
||||||
const MIN_ASSETS_ZOOM = 14; // zoom level at which bbox-based asset loading triggers
|
const MIN_ASSETS_ZOOM = 14; // zoom level at which bbox-based asset loading triggers
|
||||||
var assetsLoadModes = {}; // thematic_code → null | 'always' | 'filter' | 'bbox'
|
var assetsLoadModes = {}; // thematic_code → null | 'always' | 'filter' | 'bbox'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<ul class="list-group list-group-flush bg-transparent">
|
||||||
|
{% for asset_node in asset_nodes %}
|
||||||
|
<li class="list-group-item bg-transparent border-0 py-1 px-0">
|
||||||
|
<!-- Row Header -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between py-1 px-2">
|
||||||
|
<!-- Contenu gauche (indenté selon la profondeur) -->
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap" style="padding-left: calc(({{ depth|default:1 }} - 1) * 1.25rem);">
|
||||||
|
{% if asset_node.children %}
|
||||||
|
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn collapsed"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#tree-asset-{{ asset_node.asset.id }}"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="bi bi-chevron-right small"></i>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span class="px-1"></span>
|
||||||
|
{% endif %}
|
||||||
|
<i class="bi bi-gear-fill text-info small"></i>
|
||||||
|
|
||||||
|
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
|
||||||
|
<a href="{{ asset_node.asset.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
|
||||||
|
{{ asset_node.asset.code }}
|
||||||
|
</a>
|
||||||
|
<span class="small">{{ asset_node.asset.get_name }}</span>
|
||||||
|
<span class="badge bg-info-subtle text-info border small ms-1">{{ asset_node.asset.get_asset_type_display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if asset_node.children %}
|
||||||
|
<div id="tree-asset-{{ asset_node.asset.id }}" class="collapse mt-1">
|
||||||
|
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=asset_node.children depth=depth|default:1|add:1 only %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="equipments-tab" data-bs-toggle="tab" data-bs-target="#equipments" type="button" role="tab">
|
<button class="nav-link" id="equipments-tab" data-bs-toggle="tab" data-bs-target="#equipments" type="button" role="tab">
|
||||||
<i class="bi bi-gear me-1"></i>{% translate "Équipements" %} ({{ equipments.count }})
|
<i class="bi bi-gear me-1"></i>{% translate "Assets & Équipements" %} ({{ equipments|length }})
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
|
|
@ -145,93 +145,72 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card 3: Arborescence Niveaux, Pièces & Équipements -->
|
<!-- Card 3: Arborescence Unifiée des Localisations & Assets -->
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light fw-bold d-flex justify-content-between align-items-center">
|
||||||
<span><i class="bi bi-diagram-3 me-2"></i>{% translate "Arborescence du bâtiment" %}</span>
|
<span><i class="bi bi-diagram-3 me-2"></i>{% translate "Arborescence Hiérarchique" %}</span>
|
||||||
<span class="badge bg-primary-subtle text-primary border">{{ levels.count }} {% translate "niveau(x)" %}</span>
|
<span class="badge bg-primary-subtle text-primary border">{{ location_tree.children|length }} {% translate "sous-localisation(s)" %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-3">
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
{% for level in levels %}
|
<!-- Localisation Parente (si existante) -->
|
||||||
<li class="list-group-item p-3">
|
{% if parent_location %}
|
||||||
|
<div class="card bg-light border-secondary-subtle p-2 mb-3">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn"
|
<i class="bi bi-geo-alt-fill text-danger fs-5"></i>
|
||||||
type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#level-collapse-{{ level.id }}"
|
|
||||||
aria-expanded="true">
|
|
||||||
<i class="bi bi-chevron-down small"></i>
|
|
||||||
</button>
|
|
||||||
<i class="bi bi-layers text-primary fs-5"></i>
|
|
||||||
<span class="fw-bold fs-6">{{ level.name }}</span>
|
|
||||||
<span class="text-muted small">({% translate "Niveau" %} {{ level.level_number }})</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span class="badge bg-secondary-subtle text-secondary border me-1">{{ level.rooms.all|length }} {% translate "local(aux)" %}</span>
|
<div class="small text-muted fw-semibold">{% translate "Localisation parente" %} :</div>
|
||||||
|
<a href="{{ parent_location.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline">
|
||||||
|
{{ parent_location.code }} - {{ parent_location.get_name }}
|
||||||
|
</a>
|
||||||
|
<span class="badge bg-secondary-subtle text-dark border small ms-1">{{ parent_location.get_location_type_display }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="{{ parent_location.get_absolute_url }}" class="btn btn-sm btn-outline-secondary py-0 px-2 small">
|
||||||
|
<i class="bi bi-arrow-up me-1"></i>{% translate "Voir la parente" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Locaux sous ce niveau -->
|
<!-- Localisation Courante (Racine de l'affichage) -->
|
||||||
<div id="level-collapse-{{ level.id }}" class="collapse show mt-3 ms-4">
|
<div class="p-2 border rounded bg-white mb-2">
|
||||||
<ul class="list-group list-group-flush border-start border-2 ps-2">
|
|
||||||
{% for room in level.rooms.all %}
|
|
||||||
<li class="list-group-item border-0 py-2">
|
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
{% if room.equipments.all %}
|
<i class="bi bi-building text-primary fs-5"></i>
|
||||||
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn collapsed"
|
<span class="fw-bold fs-6 text-primary">{{ building.code }}</span>
|
||||||
|
<span class="fw-bold fs-6">- {{ building.get_name }}</span>
|
||||||
|
<span class="badge bg-primary text-white ms-1">{{ building.get_location_type_display }}</span>
|
||||||
|
|
||||||
|
{% if location_tree.assets %}
|
||||||
|
<button class="btn btn-sm btn-outline-info py-0 px-2 small ms-2 text-nowrap"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="collapse"
|
data-bs-toggle="collapse"
|
||||||
data-bs-target="#room-collapse-{{ room.id }}"
|
data-bs-target="#tree-assets-root"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
<i class="bi bi-chevron-right small"></i>
|
<i class="bi bi-tools me-1"></i>Assets ({{ location_tree.assets|length }})
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
|
||||||
<span class="px-2"></span>
|
|
||||||
{% endif %}
|
|
||||||
<i class="bi bi-door-open text-secondary"></i>
|
|
||||||
<strong class="small">{{ room.code }}</strong> - <span class="small">{{ room.name }}</span>
|
|
||||||
{% if room.surface_m2 %}
|
|
||||||
<span class="badge bg-light text-dark border small">{{ room.surface_m2 }} m²</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if room.equipments.all %}
|
|
||||||
<span class="badge bg-info-subtle text-info border small">{{ room.equipments.all|length }} {% translate "équipement(s)" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Équipements dans ce local -->
|
<!-- Collapsible Assets de la localisation courante (NON VISIBLES PAR DÉFAUT) -->
|
||||||
{% if room.equipments.all %}
|
{% if location_tree.assets %}
|
||||||
<div id="room-collapse-{{ room.id }}" class="collapse mt-2 ms-4">
|
<div id="tree-assets-root" class="collapse mt-2 ms-4">
|
||||||
<ul class="list-group list-group-flush border-start border-2 border-info ps-2">
|
<div class="card card-body bg-light border-info-subtle p-2 mb-2">
|
||||||
{% for eq in room.equipments.all %}
|
<div class="fw-bold small text-info mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ building.code }} :</div>
|
||||||
<li class="list-group-item border-0 py-1 d-flex align-items-center justify-content-between">
|
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=location_tree.assets only %}
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<i class="bi bi-gear text-info"></i>
|
|
||||||
<a href="{{ eq.get_absolute_url }}" class="fw-bold small text-decoration-none">{{ eq.code }}</a>
|
|
||||||
<span class="small">{{ eq.get_name }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-primary py-0 px-2 small">
|
|
||||||
<i class="bi bi-eye me-1"></i>Détail
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
|
||||||
{% empty %}
|
<!-- Sous-localisations & leurs assets -->
|
||||||
<li class="list-group-item border-0 text-muted small fst-italic">{% translate "Aucun local enregistré pour ce niveau." %}</li>
|
<div class="mt-2">
|
||||||
{% endfor %}
|
{% include "assets/buildings/publicbuilding_location_tree.html" with node=location_tree depth=1 only %}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
{% empty %}
|
|
||||||
<li class="list-group-item text-center text-muted py-4 mb-0">{% translate "Aucun niveau enregistré pour ce bâtiment." %}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -307,38 +286,72 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. Équipements Tab -->
|
<!-- 2. Assets & Équipements Tab -->
|
||||||
<div class="tab-pane fade" id="equipments" role="tabpanel">
|
<div class="tab-pane fade" id="equipments" role="tabpanel">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title h6 mb-0"><i class="bi bi-tools me-2"></i>{% translate "Assets Physiques & Équipements de la Localisation et de ses sous-localisations" %}</h5>
|
||||||
|
<span class="badge bg-info-subtle text-info border">{{ equipments|length }} {% translate "asset(s)" %}</span>
|
||||||
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 50px;"></th>
|
|
||||||
<th>{% translate "Code / Nom" %}</th>
|
<th>{% translate "Code / Nom" %}</th>
|
||||||
<th>{% translate "Type d'équipement" %}</th>
|
<th>{% translate "Type d'asset" %}</th>
|
||||||
<th>{% translate "Local" %}</th>
|
<th>{% translate "Localisation spécifique" %}</th>
|
||||||
|
<th>{% translate "Asset Parent" %}</th>
|
||||||
|
<th>{% translate "Statut" %}</th>
|
||||||
<th>{% translate "Prochain contrôle" %}</th>
|
<th>{% translate "Prochain contrôle" %}</th>
|
||||||
|
<th class="text-end pe-3">{% translate "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for eq in equipments %}
|
{% for eq in equipments %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-light border" title="{% translate 'Détail de l\'équipement' %}">
|
<a href="{{ eq.get_absolute_url }}" class="fw-bold text-decoration-none">{{ eq.code }}</a>
|
||||||
<i class="bi bi-info-circle"></i>
|
<div class="small text-muted">{{ eq.get_name }}</div>
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ eq.get_absolute_url }}" class="fw-bold text-decoration-none">{{ eq.code }}</a> - {{ eq.get_name }}
|
<span class="badge bg-info-subtle text-info border">
|
||||||
|
{{ eq.get_asset_type_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if eq.location %}
|
||||||
|
<a href="{{ eq.location.get_absolute_url }}" class="small text-dark text-decoration-none">
|
||||||
|
<i class="bi bi-geo-alt me-1"></i>{{ eq.location.code }} - {{ eq.location.get_name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="small text-muted">{{ eq.building.code }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if eq.parent_asset %}
|
||||||
|
<a href="{{ eq.parent_asset.get_absolute_url }}" class="small text-decoration-none">
|
||||||
|
<i class="bi bi-diagram-2 me-1"></i>{{ eq.parent_asset.code }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {% if eq.status == 'active' %}bg-success-subtle text-success border{% else %}bg-secondary-subtle text-secondary border{% endif %}">
|
||||||
|
{{ eq.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ eq.next_control_date|date:"d/m/Y"|default:"-" }}</td>
|
||||||
|
<td class="text-end pe-3">
|
||||||
|
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap" title="{% translate 'Détail de l\'asset' %}">
|
||||||
|
<i class="bi bi-eye me-1"></i>{% translate "Détail" %}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ eq.get_equipment_type_display }}</td>
|
|
||||||
<td>{{ eq.room.name|default:"-" }}</td>
|
|
||||||
<td>{{ eq.next_control_date|default:"-" }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="text-center text-muted py-4">{% translate "Aucun équipement technique enregistré." %}</td>
|
<td colspan="7" class="text-center text-muted py-4">{% translate "Aucun asset ou équipement technique enregistré." %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -346,6 +359,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 3. Interventions Tab -->
|
<!-- 3. Interventions Tab -->
|
||||||
<div class="tab-pane fade" id="interventions" role="tabpanel">
|
<div class="tab-pane fade" id="interventions" role="tabpanel">
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,22 @@
|
||||||
{{ form.name_nl }}
|
{{ form.name_nl }}
|
||||||
{% if form.name_nl.errors %}<div class="text-danger small mt-1">{{ form.name_nl.errors.0 }}</div>{% endif %}
|
{% if form.name_nl.errors %}<div class="text-danger small mt-1">{{ form.name_nl.errors.0 }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="{{ form.location_type.id_for_label }}" class="form-label fw-semibold">{% translate "Type de localisation" %}</label>
|
||||||
|
{{ form.location_type }}
|
||||||
|
{% if form.location_type.errors %}<div class="text-danger small mt-1">{{ form.location_type.errors.0 }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="{{ form.parent_location.id_for_label }}" class="form-label fw-semibold">{% translate "Localisation parente" %}</label>
|
||||||
|
{{ form.parent_location }}
|
||||||
|
{% if form.parent_location.errors %}<div class="text-danger small mt-1">{{ form.parent_location.errors.0 }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end mb-2">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.is_main_location }}
|
||||||
|
<label for="{{ form.is_main_location.id_for_label }}" class="form-check-label fw-semibold">{% translate "Localisation principale" %}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<label for="{{ form.address.id_for_label }}" class="form-label fw-semibold">{% translate "Adresse complète" %}</label>
|
<label for="{{ form.address.id_for_label }}" class="form-label fw-semibold">{% translate "Adresse complète" %}</label>
|
||||||
{{ form.address }}
|
{{ form.address }}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% translate "Code / Nom" %}</th>
|
<th>{% translate "Code / Nom" %}</th>
|
||||||
|
<th>{% translate "Type & Parent" %}</th>
|
||||||
<th>{% translate "Usage principal" %}</th>
|
<th>{% translate "Usage principal" %}</th>
|
||||||
<th>{% translate "Propriétaire & Gestionnaire" %}</th>
|
<th>{% translate "Propriétaire & Gestionnaire" %}</th>
|
||||||
<th>{% translate "ERP" %}</th>
|
<th>{% translate "ERP" %}</th>
|
||||||
|
|
@ -89,6 +90,14 @@
|
||||||
</a>
|
</a>
|
||||||
<div class="small text-muted">{{ b.get_name }}</div>
|
<div class="small text-muted">{{ b.get_name }}</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-primary-subtle text-primary border me-1">
|
||||||
|
{{ b.get_location_type_display }}
|
||||||
|
</span>
|
||||||
|
{% if b.parent_location %}
|
||||||
|
<div class="small text-muted mt-1"><i class="bi bi-diagram-2 me-1"></i>{{ b.parent_location.code }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary-subtle text-dark border">
|
<span class="badge bg-secondary-subtle text-dark border">
|
||||||
{{ b.get_usage_type_display }}
|
{{ b.get_usage_type_display }}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<ul class="list-group list-group-flush border-0">
|
||||||
|
{% for child_node in node.children %}
|
||||||
|
<li class="list-group-item border-0 py-1 px-0">
|
||||||
|
<!-- Row Header -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between py-1 px-3">
|
||||||
|
<!-- Contenu gauche (indenté selon la profondeur) -->
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap" style="padding-left: calc(({{ depth|default:1 }} - 1) * 1.25rem);">
|
||||||
|
{% if child_node.children or child_node.assets %}
|
||||||
|
<button class="btn btn-sm btn-light border p-0 px-1 tree-toggle-btn"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#tree-loc-{{ child_node.loc.id }}"
|
||||||
|
aria-expanded="true">
|
||||||
|
<i class="bi bi-chevron-down small"></i>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span class="px-2"></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if child_node.loc.location_type == 'building' or child_node.loc.location_type == 'site' %}
|
||||||
|
<i class="bi bi-building text-primary"></i>
|
||||||
|
{% elif child_node.loc.location_type == 'level' %}
|
||||||
|
<i class="bi bi-layers text-secondary"></i>
|
||||||
|
{% elif child_node.loc.location_type == 'room' %}
|
||||||
|
<i class="bi bi-door-open text-info"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-geo-alt text-dark"></i>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Code en bleu pour indiquer clairement un lien cliquable -->
|
||||||
|
<a href="{{ child_node.loc.get_absolute_url }}" class="fw-bold text-primary text-decoration-underline small">
|
||||||
|
{{ child_node.loc.code }}
|
||||||
|
</a>
|
||||||
|
<span class="small text-muted">- {{ child_node.loc.get_name }}</span>
|
||||||
|
<span class="badge bg-primary-subtle text-primary border small">{{ child_node.loc.get_location_type_display }}</span>
|
||||||
|
|
||||||
|
{% if child_node.assets %}
|
||||||
|
<button class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#tree-assets-loc-{{ child_node.loc.id }}"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="bi bi-tools me-1"></i>Assets ({{ child_node.assets|length }})
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible Assets for this location (NON VISIBLES PAR DÉFAUT) -->
|
||||||
|
{% if child_node.assets %}
|
||||||
|
<div id="tree-assets-loc-{{ child_node.loc.id }}" class="collapse mt-2" style="padding-left: calc({{ depth|default:1 }} * 1.25rem);">
|
||||||
|
<div class="card card-body bg-light border-info-subtle p-2 mb-2 me-3">
|
||||||
|
<div class="fw-bold small text-info mb-1"><i class="bi bi-tools me-1"></i>{% translate "Assets rattachés à" %} {{ child_node.loc.code }} :</div>
|
||||||
|
{% include "assets/buildings/publicbuilding_asset_tree.html" with asset_nodes=child_node.assets depth=depth|default:1|add:1 only %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Collapsible Child Locations -->
|
||||||
|
{% if child_node.children %}
|
||||||
|
<div id="tree-loc-{{ child_node.loc.id }}" class="collapse show">
|
||||||
|
{% include "assets/buildings/publicbuilding_location_tree.html" with node=child_node depth=depth|default:1|add:1 only %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
{% if not node.assets %}
|
||||||
|
<li class="list-group-item border-0 text-muted small fst-italic py-2 ps-3">
|
||||||
|
{% translate "Aucune sous-localisation." %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
@ -10,7 +10,7 @@ RUN_DXF_TESTS = os.getenv('RUN_DXF_TESTS', '0') == '1'
|
||||||
|
|
||||||
from common.models import Thematic, UserConfig, UserThematics
|
from common.models import Thematic, UserConfig, UserThematics
|
||||||
from assets.models import NatureLocation, TrafficLightIntersection, TrafficLightRadarModel, TrafficLightRadar, AssetCategory
|
from assets.models import NatureLocation, TrafficLightIntersection, TrafficLightRadarModel, TrafficLightRadar, AssetCategory
|
||||||
from assets.models.buildings import PublicBuilding, BuildingLevel, BuildingRoom, BuildingEquipment
|
from assets.models.buildings import PublicBuilding, BuildingAsset, BuildingEquipment
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2380,58 +2380,81 @@ class TrafficLightPoleSignPanelsTest(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from assets.models.buildings import BuildingAsset
|
||||||
|
|
||||||
class PublicBuildingTestCase(TestCase):
|
class PublicBuildingTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.building = PublicBuilding.objects.create(
|
# Complexe sportif (Localisation Racine)
|
||||||
code="BAT-001",
|
self.complex = PublicBuilding.objects.create(
|
||||||
name_fr="Hôtel de Ville",
|
code="CMP-001",
|
||||||
name_nl="Stadhuis",
|
name_fr="Complexe Sportif Municipal",
|
||||||
address="Grand-Place 1, 1000 Bruxelles",
|
name_nl="Gemeentelijk Sportcomplex",
|
||||||
|
address="Rue du Sport 1, 1000 Bruxelles",
|
||||||
owner="Ville de Bruxelles",
|
owner="Ville de Bruxelles",
|
||||||
usage_type="administrative",
|
usage_type="sports",
|
||||||
|
location_type="site",
|
||||||
building_status="in_service",
|
building_status="in_service",
|
||||||
is_erp=True,
|
is_erp=True,
|
||||||
erp_category="1",
|
erp_category="1",
|
||||||
erp_type="W",
|
erp_type="X",
|
||||||
dpe_rating="B",
|
|
||||||
ges_rating="A",
|
|
||||||
geom=MultiPolygon(Polygon(((0, 0), (0, 10), (10, 10), (10, 0), (0, 0)))),
|
geom=MultiPolygon(Polygon(((0, 0), (0, 10), (10, 10), (10, 0), (0, 0)))),
|
||||||
)
|
)
|
||||||
|
# Bloc A (Localisation Enfant)
|
||||||
def test_public_building_creation(self):
|
self.bloc_a = PublicBuilding.objects.create(
|
||||||
self.assertEqual(self.building.code, "BAT-001")
|
code="BLOC-A",
|
||||||
self.assertEqual(self.building.owner, "Ville de Bruxelles")
|
name_fr="Bloc A",
|
||||||
self.assertTrue(self.building.is_erp)
|
parent_location=self.complex,
|
||||||
self.assertEqual(self.building.dpe_rating, "B")
|
location_type="building",
|
||||||
|
|
||||||
def test_building_levels_and_rooms(self):
|
|
||||||
level = BuildingLevel.objects.create(
|
|
||||||
building=self.building,
|
|
||||||
code="NIV-0",
|
|
||||||
name="Rez-de-chaussée",
|
|
||||||
level_number=0
|
|
||||||
)
|
)
|
||||||
room = BuildingRoom.objects.create(
|
# Étage 0 (Localisation Enfant du Bloc A)
|
||||||
building=self.building,
|
self.etage_0 = PublicBuilding.objects.create(
|
||||||
level=level,
|
code="ETG-0",
|
||||||
code="BUR-101",
|
name_fr="Étage 0",
|
||||||
name="Bureau du Bourgmestre",
|
parent_location=self.bloc_a,
|
||||||
room_type="Bureau",
|
location_type="level",
|
||||||
surface_m2=45.5
|
level_number=0,
|
||||||
)
|
)
|
||||||
self.assertEqual(level.building, self.building)
|
|
||||||
self.assertEqual(room.level, level)
|
|
||||||
self.assertEqual(room.surface_m2, 45.5)
|
|
||||||
|
|
||||||
def test_building_equipment(self):
|
def test_public_building_location_hierarchy(self):
|
||||||
eq = BuildingEquipment.objects.create(
|
self.assertEqual(self.complex.code, "CMP-001")
|
||||||
building=self.building,
|
self.assertEqual(self.complex.location_type, "site")
|
||||||
code="CVC-001",
|
self.assertEqual(self.bloc_a.parent_location, self.complex)
|
||||||
name_fr="Chaudière principale",
|
self.assertEqual(self.etage_0.parent_location, self.bloc_a)
|
||||||
equipment_type="hvac"
|
self.assertIn(self.bloc_a, self.complex.get_child_locations())
|
||||||
|
|
||||||
|
def test_building_asset_tree_with_civil_engineering(self):
|
||||||
|
# Génie civil / Bâti A (Asset Racine)
|
||||||
|
civil_asset = BuildingAsset.objects.create(
|
||||||
|
building=self.complex,
|
||||||
|
location=self.bloc_a,
|
||||||
|
code="BATI-A",
|
||||||
|
name_fr="Bâti Bloc A",
|
||||||
|
asset_type="civil_engineering"
|
||||||
)
|
)
|
||||||
self.assertEqual(eq.building, self.building)
|
# Asset Enfant: Façade Nord
|
||||||
self.assertEqual(eq.equipment_type, "hvac")
|
facade_asset = BuildingAsset.objects.create(
|
||||||
|
building=self.complex,
|
||||||
|
location=self.bloc_a,
|
||||||
|
parent_asset=civil_asset,
|
||||||
|
code="FAC-N",
|
||||||
|
name_fr="Façade Nord",
|
||||||
|
asset_type="facade"
|
||||||
|
)
|
||||||
|
# Asset Enfant: Chaudière
|
||||||
|
hvac_asset = BuildingAsset.objects.create(
|
||||||
|
building=self.complex,
|
||||||
|
location=self.etage_0,
|
||||||
|
parent_asset=civil_asset,
|
||||||
|
code="CVC-01",
|
||||||
|
name_fr="Chaudière",
|
||||||
|
asset_type="hvac"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(civil_asset.asset_type, "civil_engineering")
|
||||||
|
self.assertEqual(facade_asset.parent_asset, civil_asset)
|
||||||
|
self.assertEqual(hvac_asset.parent_asset, civil_asset)
|
||||||
|
self.assertIn(facade_asset, civil_asset.get_child_assets())
|
||||||
|
self.assertIn(hvac_asset, civil_asset.get_child_assets())
|
||||||
|
|
||||||
|
|
||||||
class PublicBuildingEditViewTest(TestCase):
|
class PublicBuildingEditViewTest(TestCase):
|
||||||
|
|
@ -2485,6 +2508,7 @@ class PublicBuildingEditViewTest(TestCase):
|
||||||
url = reverse('assets:publicbuilding_edit', kwargs={'pk': self.building.pk})
|
url = reverse('assets:publicbuilding_edit', kwargs={'pk': self.building.pk})
|
||||||
post_data = {
|
post_data = {
|
||||||
'code': 'BAT-999',
|
'code': 'BAT-999',
|
||||||
|
'location_type': 'building',
|
||||||
'name_fr': 'Bâtiment Modifié',
|
'name_fr': 'Bâtiment Modifié',
|
||||||
'name_nl': 'Gewijzigd Gebouw',
|
'name_nl': 'Gewijzigd Gebouw',
|
||||||
'address': 'Rue du Test 123',
|
'address': 'Rue du Test 123',
|
||||||
|
|
|
||||||
|
|
@ -11,23 +11,17 @@ from django.contrib.gis.geos import GEOSGeometry
|
||||||
from common.models import UserThematics
|
from common.models import UserThematics
|
||||||
from common.utils import check_thematic_access, get_regional_terms
|
from common.utils import check_thematic_access, get_regional_terms
|
||||||
from assets.permissions import can_edit_asset
|
from assets.permissions import can_edit_asset
|
||||||
from assets.forms import PublicBuildingForm
|
from assets.forms import PublicBuildingForm, BuildingAssetForm
|
||||||
from assets.models.buildings import (
|
from assets.models.buildings import (
|
||||||
PublicBuilding, BuildingLevel, BuildingRoom, BuildingEquipment, PublicBuildingDocument,
|
PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument,
|
||||||
USAGE_TYPE_CHOICES, BUILDING_STATUS_CHOICES, DPE_RATING_CHOICES, ERP_CATEGORY_CHOICES
|
USAGE_TYPE_CHOICES, BUILDING_STATUS_CHOICES, DPE_RATING_CHOICES, ERP_CATEGORY_CHOICES,
|
||||||
|
LOCATION_TYPE_CHOICES, BUILDING_ASSET_TYPE_CHOICES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def publicbuildings_locations_geojson(request):
|
def publicbuildings_locations_geojson(request):
|
||||||
"""
|
"""
|
||||||
Vue GeoJSON pour les emplacements des bâtiments publics (vide car les bâtiments sont gérés directement comme assets).
|
Vue GeoJSON renvoyant la collection des localisations de bâtiments publics pour la carte unifiée.
|
||||||
"""
|
|
||||||
return JsonResponse({"type": "FeatureCollection", "features": []})
|
|
||||||
|
|
||||||
|
|
||||||
def publicbuildings_assets_geojson(request):
|
|
||||||
"""
|
|
||||||
Vue GeoJSON renvoyant la collection de bâtiments publics pour la carte unifiée des assets.
|
|
||||||
"""
|
"""
|
||||||
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
|
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
|
||||||
|
|
||||||
|
|
@ -38,9 +32,14 @@ def publicbuildings_assets_geojson(request):
|
||||||
return JsonResponse({"error": "Invalid JSON"}, status=400)
|
return JsonResponse({"error": "Invalid JSON"}, status=400)
|
||||||
|
|
||||||
statuses = data.get('asset_status', [])
|
statuses = data.get('asset_status', [])
|
||||||
categories = data.get('category') or []
|
location_types = data.get('location_type') or []
|
||||||
if isinstance(categories, str):
|
if isinstance(location_types, str):
|
||||||
categories = [categories]
|
location_types = [location_types]
|
||||||
|
usage_types = data.get('usage_type') or []
|
||||||
|
if isinstance(usage_types, str):
|
||||||
|
usage_types = [usage_types]
|
||||||
|
is_main_loc = data.get('is_main_location')
|
||||||
|
is_erp_val = data.get('is_erp')
|
||||||
|
|
||||||
filter_geom_raw = data.get("filter_geom")
|
filter_geom_raw = data.get("filter_geom")
|
||||||
if filter_geom_raw:
|
if filter_geom_raw:
|
||||||
|
|
@ -54,14 +53,30 @@ def publicbuildings_assets_geojson(request):
|
||||||
search_q = (data.get('q') or '').strip()
|
search_q = (data.get('q') or '').strip()
|
||||||
else:
|
else:
|
||||||
statuses = request.GET.getlist('asset_status')
|
statuses = request.GET.getlist('asset_status')
|
||||||
categories = request.GET.getlist('category', [])
|
location_types = request.GET.getlist('location_type', [])
|
||||||
|
usage_types = request.GET.getlist('usage_type', [])
|
||||||
|
is_main_loc = request.GET.get('is_main_location')
|
||||||
|
is_erp_val = request.GET.get('is_erp')
|
||||||
filter_geom = None
|
filter_geom = None
|
||||||
search_q = request.GET.get('q', '').strip()
|
search_q = request.GET.get('q', '').strip()
|
||||||
|
|
||||||
qs = PublicBuilding.objects.all()
|
qs = PublicBuilding.objects.all()
|
||||||
|
|
||||||
if categories:
|
if location_types:
|
||||||
qs = qs.filter(category__id__in=categories)
|
qs = qs.filter(location_type__in=location_types)
|
||||||
|
|
||||||
|
if usage_types:
|
||||||
|
qs = qs.filter(usage_type__in=usage_types)
|
||||||
|
|
||||||
|
if is_main_loc in ['true', '1', True]:
|
||||||
|
qs = qs.filter(is_main_location=True)
|
||||||
|
elif is_main_loc in ['false', '0', False]:
|
||||||
|
qs = qs.filter(is_main_location=False)
|
||||||
|
|
||||||
|
if is_erp_val in ['true', '1', True]:
|
||||||
|
qs = qs.filter(is_erp=True)
|
||||||
|
elif is_erp_val in ['false', '0', False]:
|
||||||
|
qs = qs.filter(is_erp=False)
|
||||||
|
|
||||||
if statuses:
|
if statuses:
|
||||||
qs = qs.filter(building_status__in=statuses)
|
qs = qs.filter(building_status__in=statuses)
|
||||||
|
|
@ -83,12 +98,17 @@ def publicbuildings_assets_geojson(request):
|
||||||
|
|
||||||
for b in qs:
|
for b in qs:
|
||||||
geometry = None
|
geometry = None
|
||||||
if b.geom:
|
if b.geojson:
|
||||||
|
try:
|
||||||
|
geometry = json.loads(b.geojson)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not geometry and b.geom:
|
||||||
geom_4326 = b.geom.clone()
|
geom_4326 = b.geom.clone()
|
||||||
geom_4326.transform(4326)
|
geom_4326.transform(4326)
|
||||||
centroid = geom_4326.centroid
|
centroid = geom_4326.centroid
|
||||||
geometry = {"type": "Point", "coordinates": [centroid.x, centroid.y]}
|
geometry = {"type": "Point", "coordinates": [centroid.x, centroid.y]}
|
||||||
elif b.lon is not None and b.lat is not None:
|
elif not geometry and b.lon is not None and b.lat is not None:
|
||||||
geometry = {"type": "Point", "coordinates": [b.lon, b.lat]}
|
geometry = {"type": "Point", "coordinates": [b.lon, b.lat]}
|
||||||
|
|
||||||
detail_url = reverse('assets:publicbuilding_detail', kwargs={'pk': b.pk})
|
detail_url = reverse('assets:publicbuilding_detail', kwargs={'pk': b.pk})
|
||||||
|
|
@ -100,8 +120,8 @@ def publicbuildings_assets_geojson(request):
|
||||||
"properties": {
|
"properties": {
|
||||||
"code": b.code,
|
"code": b.code,
|
||||||
"name": b.get_name(),
|
"name": b.get_name(),
|
||||||
"category": b.category.get_name() if b.category else _("Bâtiment public"),
|
"location_type": b.location_type,
|
||||||
"category_id": b.category_id,
|
"location_type_display": b.get_location_type_display(),
|
||||||
"status": b.building_status,
|
"status": b.building_status,
|
||||||
"building_status": b.building_status,
|
"building_status": b.building_status,
|
||||||
"building_status_display": b.get_building_status_display(),
|
"building_status_display": b.get_building_status_display(),
|
||||||
|
|
@ -119,6 +139,100 @@ def publicbuildings_assets_geojson(request):
|
||||||
"detail_url": detail_url,
|
"detail_url": detail_url,
|
||||||
"element_type": get_element_type_label("publicbuilding"),
|
"element_type": get_element_type_label("publicbuilding"),
|
||||||
"thematic": "publicbuildings",
|
"thematic": "publicbuildings",
|
||||||
|
"is_location": True,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return JsonResponse({"type": "FeatureCollection", "features": features})
|
||||||
|
|
||||||
|
|
||||||
|
def publicbuildings_assets_geojson(request):
|
||||||
|
"""
|
||||||
|
Vue GeoJSON renvoyant la collection des assets physiques de bâtiments (Génie civil, Façades, CVC, Ascenseurs...) pour la carte unifiée.
|
||||||
|
"""
|
||||||
|
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({"error": "Invalid JSON"}, status=400)
|
||||||
|
|
||||||
|
statuses = data.get('asset_status', [])
|
||||||
|
categories = data.get('category', []) or data.get('categories', [])
|
||||||
|
if isinstance(categories, (str, int)):
|
||||||
|
categories = [categories]
|
||||||
|
asset_types = data.get('asset_type') or []
|
||||||
|
if isinstance(asset_types, str):
|
||||||
|
asset_types = [asset_types]
|
||||||
|
|
||||||
|
search_q = (data.get('q') or '').strip()
|
||||||
|
else:
|
||||||
|
statuses = request.GET.getlist('asset_status')
|
||||||
|
categories = request.GET.getlist('category')
|
||||||
|
asset_types = request.GET.getlist('asset_type', [])
|
||||||
|
search_q = request.GET.get('q', '').strip()
|
||||||
|
|
||||||
|
qs = BuildingAsset.objects.select_related('building', 'location', 'parent_asset', 'category').all()
|
||||||
|
|
||||||
|
if categories:
|
||||||
|
qs = qs.filter(category_id__in=categories)
|
||||||
|
|
||||||
|
if asset_types:
|
||||||
|
qs = qs.filter(asset_type__in=asset_types)
|
||||||
|
|
||||||
|
if statuses:
|
||||||
|
qs = qs.filter(status__in=statuses)
|
||||||
|
|
||||||
|
if search_q:
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(code__icontains=search_q) |
|
||||||
|
Q(name_fr__icontains=search_q) |
|
||||||
|
Q(name_nl__icontains=search_q)
|
||||||
|
)
|
||||||
|
|
||||||
|
features = []
|
||||||
|
from assets.views.core import get_element_type_label
|
||||||
|
|
||||||
|
for a in qs:
|
||||||
|
geometry = None
|
||||||
|
if a.geojson:
|
||||||
|
try:
|
||||||
|
geometry = json.loads(a.geojson)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not geometry and a.geom:
|
||||||
|
geom_4326 = a.geom.clone()
|
||||||
|
geom_4326.transform(4326)
|
||||||
|
geometry = {"type": "Point", "coordinates": [geom_4326.x, geom_4326.y]}
|
||||||
|
elif not geometry and a.lon is not None and a.lat is not None:
|
||||||
|
geometry = {"type": "Point", "coordinates": [a.lon, a.lat]}
|
||||||
|
elif not geometry and a.building and a.building.lon is not None and a.building.lat is not None:
|
||||||
|
geometry = {"type": "Point", "coordinates": [a.building.lon, a.building.lat]}
|
||||||
|
|
||||||
|
detail_url = reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingasset', 'asset_id': a.pk})
|
||||||
|
|
||||||
|
features.append({
|
||||||
|
"type": "Feature",
|
||||||
|
"id": a.id,
|
||||||
|
"geometry": geometry,
|
||||||
|
"properties": {
|
||||||
|
"code": a.code,
|
||||||
|
"name": a.get_name(),
|
||||||
|
"asset_type": a.asset_type,
|
||||||
|
"asset_type_display": a.get_asset_type_display(),
|
||||||
|
"category_id": a.category_id,
|
||||||
|
"category_name": a.category.get_name() if a.category else '',
|
||||||
|
"status": a.status,
|
||||||
|
"status_display": a.get_status_display(),
|
||||||
|
"building_code": a.building.code if a.building else '',
|
||||||
|
"building_name": a.building.get_name() if a.building else '',
|
||||||
|
"parent_asset_id": a.parent_asset_id,
|
||||||
|
"color": "#17a2b8",
|
||||||
|
"display_color": "#17a2b8",
|
||||||
|
"detail_url": detail_url,
|
||||||
|
"element_type": get_element_type_label("buildingasset"),
|
||||||
|
"thematic": "publicbuildings",
|
||||||
"is_location": False,
|
"is_location": False,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -128,12 +242,11 @@ def publicbuildings_assets_geojson(request):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def buildings_index(request):
|
def buildings_index(request):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Vue principale (Index / Liste) des Bâtiments Publics.
|
Vue principale (Index / Liste) des Localisations Bâtiments Publics.
|
||||||
"""
|
"""
|
||||||
lang = get_language()
|
lang = get_language()
|
||||||
buildings = PublicBuilding.objects.all()
|
buildings = PublicBuilding.objects.select_related('parent_location', 'manager').all()
|
||||||
|
|
||||||
# Search filter
|
# Search filter
|
||||||
search_query = request.GET.get('q', '').strip()
|
search_query = request.GET.get('q', '').strip()
|
||||||
|
|
@ -156,6 +269,11 @@ def buildings_index(request):
|
||||||
if status_filter:
|
if status_filter:
|
||||||
buildings = buildings.filter(building_status=status_filter)
|
buildings = buildings.filter(building_status=status_filter)
|
||||||
|
|
||||||
|
# Location type filter
|
||||||
|
location_type_filter = request.GET.get('location_type')
|
||||||
|
if location_type_filter:
|
||||||
|
buildings = buildings.filter(location_type=location_type_filter)
|
||||||
|
|
||||||
# ERP filter
|
# ERP filter
|
||||||
erp_filter = request.GET.get('is_erp')
|
erp_filter = request.GET.get('is_erp')
|
||||||
if erp_filter in ['true', '1']:
|
if erp_filter in ['true', '1']:
|
||||||
|
|
@ -178,9 +296,11 @@ def buildings_index(request):
|
||||||
'search_query': search_query,
|
'search_query': search_query,
|
||||||
'usage_type_choices': USAGE_TYPE_CHOICES,
|
'usage_type_choices': USAGE_TYPE_CHOICES,
|
||||||
'building_status_choices': BUILDING_STATUS_CHOICES,
|
'building_status_choices': BUILDING_STATUS_CHOICES,
|
||||||
|
'location_type_choices': LOCATION_TYPE_CHOICES,
|
||||||
'dpe_rating_choices': DPE_RATING_CHOICES,
|
'dpe_rating_choices': DPE_RATING_CHOICES,
|
||||||
'selected_usage': usage_filter,
|
'selected_usage': usage_filter,
|
||||||
'selected_status': status_filter,
|
'selected_status': status_filter,
|
||||||
|
'selected_location_type': location_type_filter,
|
||||||
'selected_erp': erp_filter,
|
'selected_erp': erp_filter,
|
||||||
'selected_dpe': dpe_filter,
|
'selected_dpe': dpe_filter,
|
||||||
'active_tab': 'buildings',
|
'active_tab': 'buildings',
|
||||||
|
|
@ -188,22 +308,44 @@ def buildings_index(request):
|
||||||
return render(request, 'assets/buildings/publicbuilding_list.html', context)
|
return render(request, 'assets/buildings/publicbuilding_list.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
def build_asset_node(asset):
|
||||||
|
return {
|
||||||
|
'asset': asset,
|
||||||
|
'children': [build_asset_node(c) for c in asset.child_assets.all()]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_location_node(loc):
|
||||||
|
if loc.parent_location is None:
|
||||||
|
loc_assets = loc.location_assets.filter(parent_asset__isnull=True) | loc.all_building_assets.filter(location__isnull=True, parent_asset__isnull=True)
|
||||||
|
loc_assets = loc_assets.distinct().prefetch_related('child_assets')
|
||||||
|
else:
|
||||||
|
loc_assets = loc.location_assets.filter(parent_asset__isnull=True).prefetch_related('child_assets')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'loc': loc,
|
||||||
|
'children': [build_location_node(c) for c in loc.child_locations.all().prefetch_related('child_locations', 'location_assets')],
|
||||||
|
'assets': [build_asset_node(a) for a in loc_assets],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def building_detail(request, pk):
|
def building_detail(request, pk):
|
||||||
"""
|
"""
|
||||||
Vue détaillée d'un Bâtiment Public avec ses niveaux, pièces, équipements, documents, interventions et cartographie.
|
Vue détaillée d'une Localisation Bâtiment Public avec son arborescence unifiée de localisations et d'assets physiques.
|
||||||
"""
|
"""
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from interventions.models import Intervention
|
from interventions.models import Intervention
|
||||||
from interventions.permissions import filter_viewable_interventions_for_user
|
from interventions.permissions import filter_viewable_interventions_for_user
|
||||||
|
|
||||||
building = get_object_or_404(PublicBuilding, pk=pk)
|
building = get_object_or_404(PublicBuilding.objects.select_related('parent_location', 'manager'), pk=pk)
|
||||||
can_edit = can_edit_asset(request.user, building)
|
can_edit = can_edit_asset(request.user, building)
|
||||||
regional_terms = get_regional_terms(getattr(building, '_regional_standard', 'BE'))
|
regional_terms = get_regional_terms(getattr(building, '_regional_standard', 'BE'))
|
||||||
|
|
||||||
levels = building.levels.all().prefetch_related('rooms')
|
# Arborescence unifiée (Localisation courante + sous-localisations + assets rattachés)
|
||||||
rooms = building.rooms.all()
|
location_tree = build_location_node(building)
|
||||||
equipments = building.equipments.all()
|
parent_location = building.parent_location
|
||||||
|
|
||||||
documents = building.documents.all()
|
documents = building.documents.all()
|
||||||
|
|
||||||
content_type = ContentType.objects.get_for_model(PublicBuilding)
|
content_type = ContentType.objects.get_for_model(PublicBuilding)
|
||||||
|
|
@ -219,19 +361,24 @@ def building_detail(request, pk):
|
||||||
building_geojson = None
|
building_geojson = None
|
||||||
center_lon, center_lat = None, None
|
center_lon, center_lat = None, None
|
||||||
if building.geom:
|
if building.geom:
|
||||||
|
try:
|
||||||
geom_4326 = building.geom.clone()
|
geom_4326 = building.geom.clone()
|
||||||
geom_4326.transform(4326)
|
geom_4326.transform(4326)
|
||||||
building_geojson = json.loads(geom_4326.json)
|
building_geojson = json.loads(geom_4326.json)
|
||||||
centroid = geom_4326.centroid
|
centroid = geom_4326.centroid
|
||||||
center_lon, center_lat = centroid.x, centroid.y
|
center_lon, center_lat = centroid.x, centroid.y
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif building.lon is not None and building.lat is not None:
|
||||||
|
center_lon, center_lat = building.lon, building.lat
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'building': building,
|
'building': building,
|
||||||
'can_edit': can_edit,
|
'can_edit': can_edit,
|
||||||
'regional_terms': regional_terms,
|
'regional_terms': regional_terms,
|
||||||
'levels': levels,
|
'location_tree': location_tree,
|
||||||
'rooms': rooms,
|
'parent_location': parent_location,
|
||||||
'equipments': equipments,
|
'equipments': building.get_all_assets(),
|
||||||
'documents': documents,
|
'documents': documents,
|
||||||
'interventions': interventions,
|
'interventions': interventions,
|
||||||
'content_type': content_type,
|
'content_type': content_type,
|
||||||
|
|
@ -245,7 +392,7 @@ def building_detail(request, pk):
|
||||||
@login_required
|
@login_required
|
||||||
def publicbuilding_edit(request, pk):
|
def publicbuilding_edit(request, pk):
|
||||||
"""
|
"""
|
||||||
Vue d'édition d'un Bâtiment Public réservée aux utilisateurs autorisés.
|
Vue d'édition d'une Localisation Bâtiment Public réservée aux utilisateurs autorisés.
|
||||||
"""
|
"""
|
||||||
building = get_object_or_404(PublicBuilding, pk=pk)
|
building = get_object_or_404(PublicBuilding, pk=pk)
|
||||||
|
|
||||||
|
|
@ -256,7 +403,7 @@ def publicbuilding_edit(request, pk):
|
||||||
form = PublicBuildingForm(request.POST, instance=building)
|
form = PublicBuildingForm(request.POST, instance=building)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
saved_building = form.save()
|
saved_building = form.save()
|
||||||
messages.success(request, _("Le bâtiment '%s' a été mis à jour avec succès.") % saved_building.code)
|
messages.success(request, _("La localisation '%s' a été mise à jour avec succès.") % saved_building.code)
|
||||||
return redirect('assets:publicbuilding_detail', pk=saved_building.pk)
|
return redirect('assets:publicbuilding_detail', pk=saved_building.pk)
|
||||||
else:
|
else:
|
||||||
form = PublicBuildingForm(instance=building)
|
form = PublicBuildingForm(instance=building)
|
||||||
|
|
@ -288,7 +435,7 @@ def publicbuilding_edit(request, pk):
|
||||||
@login_required
|
@login_required
|
||||||
def publicbuildings_assets_detail(request, asset_model, asset_id):
|
def publicbuildings_assets_detail(request, asset_model, asset_id):
|
||||||
"""
|
"""
|
||||||
Vue générique pour le détail d'un équipement/asset d'un bâtiment public.
|
Vue générique pour le détail d'un asset/équipement physique d'un bâtiment public.
|
||||||
"""
|
"""
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from assets.views.core import get_asset_edit_context
|
from assets.views.core import get_asset_edit_context
|
||||||
|
|
|
||||||
|
|
@ -709,6 +709,43 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
||||||
'options': quality_level_options,
|
'options': quality_level_options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Public buildings specific choices
|
||||||
|
from assets.models.buildings import USAGE_TYPE_CHOICES
|
||||||
|
building_usage_options = [
|
||||||
|
{'value': str(key), 'label': str(value)}
|
||||||
|
for key, value in USAGE_TYPE_CHOICES
|
||||||
|
]
|
||||||
|
|
||||||
|
# Public buildings specific filters
|
||||||
|
for thematic_code in ['publicbuildings', 'buildings']:
|
||||||
|
if thematic_code in accessible_codes:
|
||||||
|
config[thematic_code] = [
|
||||||
|
{
|
||||||
|
'key': 'usage_type',
|
||||||
|
'label': _("Type d'usage (Localisation)"),
|
||||||
|
'type': 'multi',
|
||||||
|
'options': building_usage_options,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'key': 'is_main_location',
|
||||||
|
'label': _('Localisation principale'),
|
||||||
|
'type': 'single',
|
||||||
|
'options': [
|
||||||
|
{'value': 'true', 'label': _('Oui')},
|
||||||
|
{'value': 'false', 'label': _('Non')},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'key': 'is_erp',
|
||||||
|
'label': _('Accueil ERP / PMR'),
|
||||||
|
'type': 'single',
|
||||||
|
'options': [
|
||||||
|
{'value': 'true', 'label': _('Oui')},
|
||||||
|
{'value': 'false', 'label': _('Non')},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Sign assets specific filter
|
# Sign assets specific filter
|
||||||
if 'sign' in accessible_codes:
|
if 'sign' in accessible_codes:
|
||||||
config.setdefault('sign', []).append({
|
config.setdefault('sign', []).append({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue