feat: add admin, models, views, and templates for ITS assets
This commit is contained in:
parent
bb8d6f98c0
commit
7d5718dec2
11 changed files with 842 additions and 78 deletions
|
|
@ -451,7 +451,10 @@ admin.site.register(ComplementaryPoliceRegulation, AssetAdmin)
|
|||
admin.site.register(SignParkingSpot, AssetAdmin)
|
||||
|
||||
|
||||
from .models import ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract
|
||||
from .models import (
|
||||
ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract,
|
||||
ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
||||
)
|
||||
|
||||
admin.site.register(ITSAssetModel, AssetAdmin)
|
||||
|
||||
|
|
@ -468,6 +471,44 @@ class ITSGeoAssetContractAdmin(AssetAdmin):
|
|||
list_filter = ("contract",)
|
||||
|
||||
|
||||
class ITSBollardPlotInline(admin.TabularInline):
|
||||
model = ITSBollardPlot
|
||||
extra = 0
|
||||
fields = ("code", "position", "bollard_type", "model", "serial_number", "status")
|
||||
show_change_link = True
|
||||
|
||||
|
||||
class ITSBollardSystemAssetInline(admin.TabularInline):
|
||||
model = ITSBollardSystemAsset
|
||||
extra = 0
|
||||
fields = ("asset", "role", "comment")
|
||||
autocomplete_fields = ("asset",)
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(ITSBollardSystem)
|
||||
class ITSBollardSystemAdmin(AssetGISAdmin, AssetAdmin):
|
||||
search_fields = ("code", "name_fr", "name_nl", "location__code", "location__name_fr")
|
||||
list_display = ("code", "name_fr", "location", "width", "direction", "status", "plots_count")
|
||||
list_filter = ("status", "direction", "location")
|
||||
inlines = [ITSBollardPlotInline, ITSBollardSystemAssetInline]
|
||||
|
||||
|
||||
@admin.register(ITSBollardPlot)
|
||||
class ITSBollardPlotAdmin(AssetGISAdmin, AssetAdmin):
|
||||
search_fields = ("code", "name_fr", "system__code", "serial_number")
|
||||
list_display = ("code", "system", "position", "bollard_type", "model", "status")
|
||||
list_filter = ("bollard_type", "status", "system")
|
||||
|
||||
|
||||
@admin.register(ITSBollardSystemContract)
|
||||
class ITSBollardSystemContractAdmin(AssetAdmin):
|
||||
search_fields = ("system__code", "contract__contract_number")
|
||||
list_display = ("system", "contract", "status", "maintenance_type")
|
||||
list_filter = ("status", "maintenance_type", "contract")
|
||||
|
||||
|
||||
|
||||
from .models import ITSLocation, ITSLocationDocument, ITSLocationContract
|
||||
|
||||
admin.site.register(ITSLocation, AssetAdmin)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-08 10:19
|
||||
|
||||
import assets.models.core
|
||||
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', '0030_managementzone_managementzonedocument_and_more'),
|
||||
('contracts', '0014_contract_color_contract_geom'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='itsgeoasset',
|
||||
name='extra_data',
|
||||
field=models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ITSBollardSystem',
|
||||
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)),
|
||||
('external_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe')),
|
||||
('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)),
|
||||
('inspection_status', models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection")),
|
||||
('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')),
|
||||
('funding_program', models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet')),
|
||||
('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.LineStringField(blank=True, null=True, srid=3812, verbose_name='Géométrie (Ligne)')),
|
||||
('width', models.FloatField(blank=True, null=True, verbose_name='Largeur de passage (m)')),
|
||||
('direction', models.CharField(blank=True, choices=[('unidirectional', 'Sens unique'), ('bidirectional', 'Double sens')], default='bidirectional', max_length=50, null=True, verbose_name='Sens de circulation')),
|
||||
('extra_data', models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques')),
|
||||
('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='bollard_systems', to='assets.itslocation', verbose_name='Localisation ITS')),
|
||||
('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')),
|
||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel', verbose_name='Modèle de système')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsbollardsystem', 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': 'Système de bornes',
|
||||
'verbose_name_plural': 'Systèmes de bornes',
|
||||
},
|
||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ITSBollardSystemAsset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('camera_entry', 'Caméra entrée (LAPI)'), ('camera_exit', 'Caméra sortie (LAPI)'), ('camera_cctv', 'Caméra de contexte / surveillance'), ('cabinet', 'Armoire électrique / automate'), ('totem', 'Totem de commande'), ('traffic_light', 'Feu de franchissement'), ('loop', 'Boucle de détection'), ('other', 'Autre')], default='other', max_length=50, verbose_name="Rôle de l'équipement")),
|
||||
('comment', models.CharField(blank=True, max_length=255, null=True, verbose_name='Commentaire')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name="Date d'association")),
|
||||
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_system_links', to='assets.itsgeoasset', verbose_name='Équipement ITS')),
|
||||
('bollard_system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='system_assets', to='assets.itsbollardsystem', verbose_name='Système de bornes')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Équipement associé au système de bornes',
|
||||
'verbose_name_plural': 'Équipements associés au système de bornes',
|
||||
'unique_together': {('bollard_system', 'asset')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itsbollardsystem',
|
||||
name='associated_assets',
|
||||
field=models.ManyToManyField(blank=True, related_name='bollard_systems', through='assets.ITSBollardSystemAsset', to='assets.itsgeoasset', verbose_name='Équipements associés'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ITSBollardSystemContract',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||
('maintenance_type', models.CharField(choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other', max_length=50)),
|
||||
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_systems_contracts', to='contracts.contract')),
|
||||
('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_systems_contracts', to='assets.itsbollardsystem')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Contrat de système de bornes',
|
||||
'verbose_name_plural': 'Contrats de systèmes de bornes',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ITSBollardPlot',
|
||||
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)),
|
||||
('external_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe')),
|
||||
('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)),
|
||||
('inspection_status', models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection")),
|
||||
('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')),
|
||||
('funding_program', models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet')),
|
||||
('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.PointField(blank=True, null=True, srid=3812, verbose_name='Géométrie (Point)')),
|
||||
('position', models.PositiveSmallIntegerField(default=1, help_text='Ordre de gauche à droite (1, 2, 3...)', verbose_name='Position sur la traverse')),
|
||||
('bollard_type', models.CharField(choices=[('automatic', 'Automatique / Escamotable'), ('semi_automatic', 'Semi-automatique'), ('fixed', 'Fixe'), ('removable', 'Amovible')], default='automatic', max_length=50, verbose_name='Type de plot')),
|
||||
('extra_data', models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques')),
|
||||
('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='bollard_plots', to='assets.itslocation', verbose_name='Localisation ITS')),
|
||||
('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')),
|
||||
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel', verbose_name='Modèle de plot')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsbollardplot', 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')),
|
||||
('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plots', to='assets.itsbollardsystem', verbose_name='Système de bornes')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Plot de borne',
|
||||
'verbose_name_plural': 'Plots de bornes',
|
||||
'ordering': ['system', 'position'],
|
||||
'indexes': [models.Index(fields=['code'], name='assets_itsb_code_de6000_idx')],
|
||||
},
|
||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='itsbollardsystem',
|
||||
index=models.Index(fields=['code'], name='assets_itsb_code_a4c148_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='itsbollardsystemcontract',
|
||||
unique_together={('system', 'contract')},
|
||||
),
|
||||
]
|
||||
|
|
@ -8,7 +8,7 @@ from .structures import Structure, StructureLocation, StructureGeoAsset, Structu
|
|||
from .controlcenters import ControlCenter, ControlCenterLocation, ControlCenterGeoAsset, ControlCenterAssetModel, ControlCenterDocument, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument
|
||||
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 .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract, ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
||||
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
|
||||
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
|
||||
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
from django.contrib.gis.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
|
||||
import os
|
||||
|
||||
|
|
@ -91,6 +91,7 @@ class ITSGeoAsset(AbstractGeoAsset):
|
|||
location = models.ForeignKey(ITSLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
trafficlights_pole = models.ForeignKey("assets.TrafficLightPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Traffic Light Pole")
|
||||
lighting_pole = models.ForeignKey("assets.PublicLightingPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Lighting Pole")
|
||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
|
@ -119,6 +120,147 @@ class ITSGeoAsset(AbstractGeoAsset):
|
|||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsgeoasset', 'asset_id':self.id})
|
||||
|
||||
|
||||
BOLLARD_DIRECTION_CHOICES = (
|
||||
('unidirectional', _('Sens unique')),
|
||||
('bidirectional', _('Double sens')),
|
||||
)
|
||||
|
||||
class ITSBollardSystem(AbstractGeoAsset):
|
||||
"""
|
||||
Système / Traverse de bornes escamotables (représentation linéaire).
|
||||
Contrôle l'accès à une voie ou zone piétonne.
|
||||
"""
|
||||
geom = models.LineStringField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie (Ligne)"))
|
||||
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Modèle de système"))
|
||||
location = models.ForeignKey(ITSLocation, related_name="bollard_systems", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Localisation ITS"))
|
||||
width = models.FloatField(null=True, blank=True, verbose_name=_("Largeur de passage (m)"))
|
||||
direction = models.CharField(max_length=50, choices=BOLLARD_DIRECTION_CHOICES, default='bidirectional', null=True, blank=True, verbose_name=_("Sens de circulation"))
|
||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||
associated_assets = models.ManyToManyField(
|
||||
'assets.ITSGeoAsset',
|
||||
through='assets.ITSBollardSystemAsset',
|
||||
related_name="bollard_systems",
|
||||
blank=True,
|
||||
verbose_name=_("Équipements associés")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Système de bornes")
|
||||
verbose_name_plural = _("Systèmes de bornes")
|
||||
indexes = [
|
||||
models.Index(fields=["code"]),
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsbollardsystem', 'asset_id': self.id})
|
||||
|
||||
@property
|
||||
def plots_count(self):
|
||||
return self.plots.count()
|
||||
|
||||
|
||||
BOLLARD_TYPE_CHOICES = (
|
||||
('automatic', _('Automatique / Escamotable')),
|
||||
('semi_automatic', _('Semi-automatique')),
|
||||
('fixed', _('Fixe')),
|
||||
('removable', _('Amovible')),
|
||||
)
|
||||
|
||||
class ITSBollardPlot(AbstractGeoAsset):
|
||||
"""
|
||||
Plot / borne physique individuel sur un système de bornes.
|
||||
"""
|
||||
geom = models.PointField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie (Point)"))
|
||||
system = models.ForeignKey(ITSBollardSystem, on_delete=models.CASCADE, related_name="plots", verbose_name=_("Système de bornes"))
|
||||
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Modèle de plot"))
|
||||
position = models.PositiveSmallIntegerField(default=1, verbose_name=_("Position sur la traverse"), help_text=_("Ordre de gauche à droite (1, 2, 3...)"))
|
||||
bollard_type = models.CharField(max_length=50, choices=BOLLARD_TYPE_CHOICES, default='automatic', verbose_name=_("Type de plot"))
|
||||
location = models.ForeignKey(ITSLocation, related_name="bollard_plots", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Localisation ITS"))
|
||||
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.location_id and self.system and self.system.location_id:
|
||||
self.location = self.system.location
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.location_id and self.system and self.system.location_id:
|
||||
self.location = self.system.location
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Plot de borne")
|
||||
verbose_name_plural = _("Plots de bornes")
|
||||
ordering = ["system", "position"]
|
||||
indexes = [
|
||||
models.Index(fields=["code"]),
|
||||
]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsbollardplot', 'asset_id': self.id})
|
||||
|
||||
|
||||
class ITSBollardSystemAsset(models.Model):
|
||||
"""
|
||||
Table de liaison ManyToMany entre un ITSBollardSystem et des ITSGeoAsset
|
||||
(caméras LAPI, totems, armoires électriques, feux bicolores, boucles...).
|
||||
"""
|
||||
class AssetRole(models.TextChoices):
|
||||
CAMERA_ENTRY = "camera_entry", _("Caméra entrée (LAPI)")
|
||||
CAMERA_EXIT = "camera_exit", _("Caméra sortie (LAPI)")
|
||||
CAMERA_CCTV = "camera_cctv", _("Caméra de contexte / surveillance")
|
||||
CABINET = "cabinet", _("Armoire électrique / automate")
|
||||
TOTEM = "totem", _("Totem de commande")
|
||||
TRAFFIC_LIGHT = "traffic_light", _("Feu de franchissement")
|
||||
LOOP = "loop", _("Boucle de détection")
|
||||
OTHER = "other", _("Autre")
|
||||
|
||||
bollard_system = models.ForeignKey(
|
||||
ITSBollardSystem,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="system_assets",
|
||||
verbose_name=_("Système de bornes")
|
||||
)
|
||||
asset = models.ForeignKey(
|
||||
ITSGeoAsset,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="bollard_system_links",
|
||||
verbose_name=_("Équipement ITS")
|
||||
)
|
||||
role = models.CharField(
|
||||
max_length=50,
|
||||
choices=AssetRole.choices,
|
||||
default=AssetRole.OTHER,
|
||||
verbose_name=_("Rôle de l'équipement")
|
||||
)
|
||||
comment = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Commentaire"))
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date d'association"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Équipement associé au système de bornes")
|
||||
verbose_name_plural = _("Équipements associés au système de bornes")
|
||||
unique_together = ('bollard_system', 'asset')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.bollard_system.code} - {self.asset.code} ({self.get_role_display()})"
|
||||
|
||||
|
||||
class ITSBollardSystemContract(models.Model):
|
||||
system = models.ForeignKey(ITSBollardSystem, on_delete=models.CASCADE, related_name="bollard_systems_contracts")
|
||||
contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="bollard_systems_contracts")
|
||||
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
|
||||
maintenance_type = models.CharField(max_length=50, choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.system.code} - {self.contract.contract_number}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Contrat de système de bornes")
|
||||
verbose_name_plural = _("Contrats de systèmes de bornes")
|
||||
unique_together = ('system', 'contract')
|
||||
|
||||
|
||||
|
||||
|
||||
class ITSLocationContract(models.Model):
|
||||
location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="its_locations_contracts")
|
||||
|
|
|
|||
|
|
@ -111,6 +111,11 @@
|
|||
id: 'assets',
|
||||
type: 'circle',
|
||||
source: 'assets',
|
||||
filter: [
|
||||
'any',
|
||||
['==', ['geometry-type'], 'Point'],
|
||||
['==', ['geometry-type'], 'MultiPoint']
|
||||
],
|
||||
paint: {
|
||||
'circle-radius': [
|
||||
'interpolate',
|
||||
|
|
@ -135,7 +140,32 @@
|
|||
minzoom: 8
|
||||
};
|
||||
|
||||
const assets_layers_defs = [assets_layer_def];
|
||||
const assets_lines_layer_def = {
|
||||
id: 'assets-lines',
|
||||
type: 'line',
|
||||
source: 'assets',
|
||||
filter: [
|
||||
'any',
|
||||
['==', ['geometry-type'], 'LineString'],
|
||||
['==', ['geometry-type'], 'MultiLineString']
|
||||
],
|
||||
paint: {
|
||||
'line-color': [
|
||||
'case',
|
||||
['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b',
|
||||
'#0022ff'
|
||||
],
|
||||
'line-width': [
|
||||
'case',
|
||||
['boolean', ['feature-state', 'highlighted'], false], 6,
|
||||
4
|
||||
],
|
||||
'line-opacity': 0.85
|
||||
},
|
||||
minzoom: 8
|
||||
};
|
||||
|
||||
const assets_layers_defs = [assets_lines_layer_def, assets_layer_def];
|
||||
|
||||
|
||||
// Filtrer les modèles par catégorie dans le formulaire des assets
|
||||
|
|
|
|||
92
loko/assets/templates/its/detail_itsbollardplot.html
Normal file
92
loko/assets/templates/its/detail_itsbollardplot.html
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{% extends "assets/detail_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_blocks %}
|
||||
<!-- Système de bornes parent -->
|
||||
<div class="card mb-3" id="bollard-plot-system-card">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-diagram-3-fill text-primary me-2"></i>{% translate "Système de bornes de rattachement" %}
|
||||
</h5>
|
||||
{% if asset.system %}
|
||||
<a href="{{ asset.system.get_absolute_url }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir le système complet" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if asset.system %}
|
||||
<div class="row align-items-center mb-3">
|
||||
<div class="col-md-7">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="badge bg-dark rounded-pill fs-6 me-2">#{{ asset.position }}</span>
|
||||
<h5 class="mb-0">
|
||||
<a href="{{ asset.system.get_absolute_url }}" class="text-decoration-none">
|
||||
{{ asset.system.code }} - {{ asset.system.get_name|default:asset.system.code }}
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="text-muted small mb-2">
|
||||
<i class="bi bi-geo-alt me-1"></i>{{ asset.system.location|default:"Localisation non définie" }}
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% if asset.system.width %}
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi bi-arrows-expand me-1"></i>{% translate "Largeur" %} : {{ asset.system.width }} m
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if asset.system.direction %}
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi bi-signpost-split me-1"></i>{{ asset.system.get_direction_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if asset.system.model %}
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi bi-tag me-1"></i>{{ asset.system.model.get_name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 text-md-end mt-3 mt-md-0">
|
||||
<div class="p-3 bg-light rounded border text-start">
|
||||
<div class="small text-muted mb-1">{% translate "Traverse complète" %} ({{ asset.system.plots.count }} {% translate "plots" %}) :</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for sibling in asset.system.plots.all %}
|
||||
<a href="{{ sibling.get_absolute_url }}"
|
||||
class="btn btn-sm {% if sibling.id == asset.id %}btn-primary fw-bold{% else %}btn-outline-secondary{% endif %}"
|
||||
title="{{ sibling.code }} - {{ sibling.get_bollard_type_display }}">
|
||||
#{{ sibling.position }} ({{ sibling.code }})
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted text-center p-3">
|
||||
<i class="bi bi-exclamation-triangle text-warning me-2"></i>{% translate "Ce plot n'est actuellement rattaché à aucun système de bornes." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Métadonnées techniques (extra_data) -->
|
||||
{% if asset.extra_data %}
|
||||
<div class="card mb-3" id="bollard-plot-extra-data-card">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-code-slash text-secondary me-2"></i>{% translate "Métadonnées techniques" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
{% for key, val in asset.extra_data.items %}
|
||||
<dt class="col-sm-4 text-truncate" title="{{ key }}">{{ key }}</dt>
|
||||
<dd class="col-sm-8 mb-2"><code>{{ val }}</code></dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock extra_blocks %}
|
||||
218
loko/assets/templates/its/detail_itsbollardsystem.html
Normal file
218
loko/assets/templates/its/detail_itsbollardsystem.html
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
{% extends "assets/detail_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_blocks %}
|
||||
<!-- Plots de la traverse -->
|
||||
<div class="card mb-3" id="bollard-system-plots-card">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-geo-alt-fill text-primary me-2"></i>{% translate "Plots de la traverse" %}
|
||||
<span class="badge bg-primary ms-2">{{ asset.plots.count }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if asset.plots.all %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped mb-0 align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;" class="text-center">{% translate "Pos." %}</th>
|
||||
<th>{% translate "Code" %}</th>
|
||||
<th>{% translate "Type de borne" %}</th>
|
||||
<th>{% translate "Modèle" %}</th>
|
||||
<th>{% translate "N° de série" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th style="width: 110px;" class="text-end">{% translate "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for plot in asset.plots.all %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-dark rounded-pill">#{{ plot.position }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ plot.get_absolute_url }}" class="fw-bold text-decoration-none">
|
||||
{{ plot.code }}
|
||||
</a>
|
||||
{% if plot.get_name %}
|
||||
<div class="small text-muted">{{ plot.get_name }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if plot.bollard_type == 'automatic' %}
|
||||
<span class="badge bg-primary"><i class="bi bi-arrow-up-down me-1"></i>{{ plot.get_bollard_type_display }}</span>
|
||||
{% elif plot.bollard_type == 'semi_automatic' %}
|
||||
<span class="badge bg-info text-dark">{{ plot.get_bollard_type_display }}</span>
|
||||
{% elif plot.bollard_type == 'removable' %}
|
||||
<span class="badge bg-warning text-dark">{{ plot.get_bollard_type_display }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ plot.get_bollard_type_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ plot.model.get_name|default:"-" }}</td>
|
||||
<td><code>{{ plot.serial_number|default:"-" }}</code></td>
|
||||
<td>
|
||||
{% if plot.status == 'active' %}
|
||||
<span class="badge bg-success">{{ plot.get_status_display }}</span>
|
||||
{% elif plot.status == 'damaged' %}
|
||||
<span class="badge bg-warning text-dark">{{ plot.get_status_display }}</span>
|
||||
{% elif plot.status == 'to_replace' %}
|
||||
<span class="badge bg-danger">{{ plot.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ plot.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ plot.get_absolute_url }}" class="btn btn-sm btn-outline-primary" title="{% translate 'Voir le détail du plot' %}">
|
||||
<i class="bi bi-eye me-1"></i>{% translate "Voir" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="bi bi-cone-striped display-6 d-block mb-2 text-secondary opacity-50"></i>
|
||||
{% translate "Aucun plot physique n'est encore configuré sur ce système de bornes." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Équipements associés au système (Caméras LAPI, totems, armoires...) -->
|
||||
<div class="card mb-3" id="bollard-system-assets-card">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-hdd-network text-info me-2"></i>{% translate "Équipements associés" %}
|
||||
<span class="badge bg-secondary ms-2">{{ asset.system_assets.count }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if asset.system_assets.all %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped mb-0 align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Rôle" %}</th>
|
||||
<th>{% translate "Équipement" %}</th>
|
||||
<th>{% translate "Modèle" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th>{% translate "Commentaire" %}</th>
|
||||
<th style="width: 110px;" class="text-end">{% translate "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in asset.system_assets.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if 'camera' in item.role %}
|
||||
<span class="badge bg-info text-dark"><i class="bi bi-camera-video me-1"></i>{{ item.get_role_display }}</span>
|
||||
{% elif item.role == 'cabinet' %}
|
||||
<span class="badge bg-dark"><i class="bi bi-cpu me-1"></i>{{ item.get_role_display }}</span>
|
||||
{% elif item.role == 'totem' %}
|
||||
<span class="badge bg-primary"><i class="bi bi-tablet me-1"></i>{{ item.get_role_display }}</span>
|
||||
{% elif item.role == 'traffic_light' %}
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-traffic-light me-1"></i>{{ item.get_role_display }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ item.get_role_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ item.asset.get_absolute_url }}" class="fw-bold text-decoration-none">
|
||||
{{ item.asset.code }}
|
||||
</a>
|
||||
{% if item.asset.get_name %}
|
||||
<div class="small text-muted">{{ item.asset.get_name }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.asset.model.get_name|default:"-" }}</td>
|
||||
<td>
|
||||
{% if item.asset.status == 'active' %}
|
||||
<span class="badge bg-success">{{ item.asset.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ item.asset.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.comment|default:"-" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ item.asset.get_absolute_url }}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Voir équipement' %}">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="bi bi-camera-video display-6 d-block mb-2 text-secondary opacity-50"></i>
|
||||
{% translate "Aucun équipement périphérique (caméra, armoire, totem...) n'est actuellement associé." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contrats de maintenance liés -->
|
||||
{% if asset.bollard_systems_contracts.exists %}
|
||||
<div class="card mb-3" id="bollard-system-contracts-card">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-file-earmark-text text-secondary me-2"></i>{% translate "Contrats d'entretien" %}
|
||||
<span class="badge bg-secondary ms-2">{{ asset.bollard_systems_contracts.count }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-striped mb-0 align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Numéro de contrat" %}</th>
|
||||
<th>{% translate "Type de maintenance" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sc in asset.bollard_systems_contracts.all %}
|
||||
<tr>
|
||||
<td class="fw-bold">{{ sc.contract.contract_number }} - {{ sc.contract.name }}</td>
|
||||
<td>{{ sc.get_maintenance_type_display }}</td>
|
||||
<td>
|
||||
{% if sc.status == 'active' %}
|
||||
<span class="badge bg-success">{% translate "Actif" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ sc.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Métadonnées techniques (extra_data) -->
|
||||
{% if asset.extra_data %}
|
||||
<div class="card mb-3" id="bollard-system-extra-data-card">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-code-slash text-secondary me-2"></i>{% translate "Métadonnées techniques" %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
{% for key, val in asset.extra_data.items %}
|
||||
<dt class="col-sm-4 text-truncate" title="{{ key }}">{{ key }}</dt>
|
||||
<dd class="col-sm-8 mb-2"><code>{{ val }}</code></dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock extra_blocks %}
|
||||
|
|
@ -103,11 +103,11 @@ def get_asset_specific_fields(asset):
|
|||
from django.contrib.gis.db.models import GeometryField
|
||||
from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField,
|
||||
BooleanField, FloatField, DecimalField, IntegerField,
|
||||
DateField, DateTimeField, TextField)
|
||||
DateField, DateTimeField, TextField, JSONField)
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from assets.models import AbstractAsset, AbstractGeoAsset
|
||||
|
||||
excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField)
|
||||
excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField, JSONField)
|
||||
|
||||
base_field_names = {f.name for f in AbstractAsset._meta.fields}
|
||||
base_field_names |= {f.name for f in AbstractGeoAsset._meta.fields}
|
||||
|
|
|
|||
|
|
@ -1411,6 +1411,11 @@ CATEGORY_ASSET_MAPPING = {
|
|||
'LI_poles': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
||||
'publiclighting_luminaire': ('PublicLightingLuminaire', 'PublicLightingLuminaireModel'),
|
||||
'LI_lightfixtures': ('PublicLightingLightFixture', 'PublicLightingLightFixtureModel'),
|
||||
# ITS categories
|
||||
'its_bollard_system': ('ITSBollardSystem', 'ITSAssetModel'),
|
||||
'bollard_system': ('ITSBollardSystem', 'ITSAssetModel'),
|
||||
'its_bollard_plot': ('ITSBollardPlot', 'ITSAssetModel'),
|
||||
'bollard_plot': ('ITSBollardPlot', 'ITSAssetModel'),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1856,6 +1861,18 @@ def create_asset_api(request):
|
|||
except Exception:
|
||||
continue
|
||||
|
||||
# Handle bollard system (for bollard plots)
|
||||
system_id = data.get('system_id') or data.get('bollard_system_id')
|
||||
if system_id and _has_model_field(new_asset, 'system'):
|
||||
from assets.models import ITSBollardSystem
|
||||
try:
|
||||
system_obj = ITSBollardSystem.objects.get(pk=system_id)
|
||||
new_asset.system = system_obj
|
||||
if not getattr(new_asset, 'location', None) and system_obj.location:
|
||||
new_asset.location = system_obj.location
|
||||
except ITSBollardSystem.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Set additional data
|
||||
extra_data = data.get('data', {})
|
||||
if isinstance(extra_data, dict):
|
||||
|
|
|
|||
|
|
@ -461,6 +461,8 @@ ELEMENT_TYPE_LABELS = {
|
|||
'controlcentergeoasset': _('Équipement CdC'),
|
||||
# --- ITS ---
|
||||
'itsgeoasset': _('Équipement ITS'),
|
||||
'itsbollardsystem': _('Système de bornes'),
|
||||
'itsbollardplot': _('Plot de borne'),
|
||||
# --- Propreté ---
|
||||
'cleanlitterbin': _('Corbeille'),
|
||||
'cleanglasscontainer': _('Conteneur verre'),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ from collections import defaultdict
|
|||
from common.utils import check_thematic_access
|
||||
|
||||
from common.models import Thematic, UserConfig, UserContractAccess, UserThematics
|
||||
from assets.models import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, AssetCategory
|
||||
from assets.models import (
|
||||
ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, AssetCategory,
|
||||
ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
||||
)
|
||||
from assets.permissions import can_edit_location_for_thematic, get_allowed_actions_for_thematic
|
||||
|
||||
from assets.forms import ITSLocationDocumentForm
|
||||
|
|
@ -297,11 +300,19 @@ def its_assets_geojson(request):
|
|||
|
||||
if lon is None or lat is None:
|
||||
location = getattr(obj, 'location', None)
|
||||
system = getattr(obj, 'system', None)
|
||||
if location is None and system is not None:
|
||||
location = getattr(system, 'location', None)
|
||||
if location is not None:
|
||||
if lon is None:
|
||||
lon = getattr(location, 'lon', None)
|
||||
if lat is None:
|
||||
lat = getattr(location, 'lat', None)
|
||||
if (lon is None or lat is None) and system is not None:
|
||||
if lon is None:
|
||||
lon = getattr(system, 'lon', None)
|
||||
if lat is None:
|
||||
lat = getattr(system, 'lat', None)
|
||||
|
||||
if geometry is None and lon is not None and lat is not None:
|
||||
geometry = {
|
||||
|
|
@ -348,16 +359,34 @@ def its_assets_geojson(request):
|
|||
.select_related('location', 'category', 'model')
|
||||
.all()
|
||||
)
|
||||
bollard_system_qs = (
|
||||
ITSBollardSystem.objects
|
||||
.select_related('location', 'category', 'model')
|
||||
.all()
|
||||
)
|
||||
bollard_plot_qs = (
|
||||
ITSBollardPlot.objects
|
||||
.select_related('system', 'location', 'category', 'model')
|
||||
.all()
|
||||
)
|
||||
|
||||
# --------- Filtres métier existants
|
||||
if its_location_ids:
|
||||
its_asset_qs = its_asset_qs.filter(location__id__in=its_location_ids)
|
||||
bollard_system_qs = bollard_system_qs.filter(location__id__in=its_location_ids)
|
||||
bollard_plot_qs = bollard_plot_qs.filter(
|
||||
Q(location__id__in=its_location_ids) | Q(system__location__id__in=its_location_ids)
|
||||
)
|
||||
|
||||
if models:
|
||||
its_asset_qs = its_asset_qs.filter(model__id__in=models)
|
||||
bollard_system_qs = bollard_system_qs.filter(model__id__in=models)
|
||||
bollard_plot_qs = bollard_plot_qs.filter(model__id__in=models)
|
||||
|
||||
if categories:
|
||||
its_asset_qs = its_asset_qs.filter(category__id__in=categories)
|
||||
bollard_system_qs = bollard_system_qs.filter(category__id__in=categories)
|
||||
bollard_plot_qs = bollard_plot_qs.filter(category__id__in=categories)
|
||||
|
||||
if filter_geom:
|
||||
its_asset_qs = its_asset_qs.filter(
|
||||
|
|
@ -365,11 +394,24 @@ def its_assets_geojson(request):
|
|||
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
||||
Q(geom__isnull=True, location__isnull=True)
|
||||
)
|
||||
bollard_system_qs = bollard_system_qs.filter(
|
||||
Q(geom__intersects=filter_geom) |
|
||||
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
||||
Q(geom__isnull=True, location__isnull=True)
|
||||
)
|
||||
bollard_plot_qs = bollard_plot_qs.filter(
|
||||
Q(geom__intersects=filter_geom) |
|
||||
Q(geom__isnull=True, system__geom__intersects=filter_geom) |
|
||||
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
||||
Q(geom__isnull=True, system__isnull=True)
|
||||
)
|
||||
|
||||
# Recherche plein texte
|
||||
if request.method == 'POST':
|
||||
from assets.views.core import apply_search
|
||||
its_asset_qs = apply_search(its_asset_qs, data)
|
||||
bollard_system_qs = apply_search(bollard_system_qs, data)
|
||||
bollard_plot_qs = apply_search(bollard_plot_qs, data)
|
||||
|
||||
# --------- Limitation pour utilisateurs externes
|
||||
user = request.user
|
||||
|
|
@ -388,26 +430,32 @@ def its_assets_geojson(request):
|
|||
can_view_assets=True
|
||||
).values_list('contract_id', flat=True)
|
||||
|
||||
# Ne garder que les assets qui ont un contrat actif
|
||||
# auquel l'utilisateur a accès
|
||||
# Ne garder que les assets qui ont un contrat actif auquel l'utilisateur a accès
|
||||
its_asset_qs = its_asset_qs.filter(
|
||||
its_assets_contracts__status='active',
|
||||
its_assets_contracts__contract_id__in=accessible_contract_ids
|
||||
).distinct()
|
||||
bollard_system_qs = bollard_system_qs.filter(
|
||||
bollard_systems_contracts__status='active',
|
||||
bollard_systems_contracts__contract_id__in=accessible_contract_ids
|
||||
).distinct()
|
||||
bollard_plot_qs = bollard_plot_qs.filter(
|
||||
system__bollard_systems_contracts__status='active',
|
||||
system__bollard_systems_contracts__contract_id__in=accessible_contract_ids
|
||||
).distinct()
|
||||
else:
|
||||
# Non authentifié : ne rien exposer (plus sûr)
|
||||
its_asset_qs = its_asset_qs.none()
|
||||
|
||||
# --------- Cap de sécurité
|
||||
its_asset_qs = its_asset_qs[0:MAX_ASSET_FEATURES+1]
|
||||
if len(its_asset_qs) >= MAX_ASSET_FEATURES:
|
||||
return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413)
|
||||
bollard_system_qs = bollard_system_qs.none()
|
||||
bollard_plot_qs = bollard_plot_qs.none()
|
||||
|
||||
# --------- Construction GeoJSON
|
||||
features = []
|
||||
feature_count = 0
|
||||
for qs, asset_type in [
|
||||
(its_asset_qs, "itsgeoasset"),
|
||||
(bollard_system_qs, "itsbollardsystem"),
|
||||
(bollard_plot_qs, "itsbollardplot"),
|
||||
]:
|
||||
feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count)
|
||||
if limit_reached:
|
||||
|
|
@ -603,7 +651,9 @@ def its_assets_autocomplete(request):
|
|||
raw_query = raw_query_original.strip()
|
||||
|
||||
# CT ids (calculés une fois)
|
||||
equipment_ct_id = ContentType.objects.get_for_model(ITSGeoAsset).id
|
||||
geoasset_ct_id = ContentType.objects.get_for_model(ITSGeoAsset).id
|
||||
bollard_system_ct_id = ContentType.objects.get_for_model(ITSBollardSystem).id
|
||||
bollard_plot_ct_id = ContentType.objects.get_for_model(ITSBollardPlot).id
|
||||
location_ct_id = ContentType.objects.get_for_model(ITSLocation).id
|
||||
|
||||
qn = strip_accents_lower(raw_query)
|
||||
|
|
@ -626,68 +676,71 @@ def its_assets_autocomplete(request):
|
|||
# en cas d'ID invalide ou autre, on ignore le filtre
|
||||
location_ids = None
|
||||
|
||||
# Base queryset accent-insensible + actifs uniquement
|
||||
qs = (
|
||||
ITSGeoAsset.objects
|
||||
.annotate(
|
||||
code_u=Lower(Unaccent('code')),
|
||||
name_fr_u=Lower(Unaccent('name_fr')),
|
||||
name_nl_u=Lower(Unaccent('name_nl')),
|
||||
def query_model(model_class, ct_id, extra_select=()):
|
||||
q = (
|
||||
model_class.objects
|
||||
.annotate(
|
||||
code_u=Lower(Unaccent('code')),
|
||||
name_fr_u=Lower(Unaccent('name_fr')),
|
||||
name_nl_u=Lower(Unaccent('name_nl')),
|
||||
)
|
||||
)
|
||||
if has_search_term:
|
||||
q = q.filter(
|
||||
Q(code_u__contains=qn) |
|
||||
Q(name_fr_u__contains=qn) |
|
||||
Q(name_nl_u__contains=qn)
|
||||
)
|
||||
q = q.exclude(status='removed')
|
||||
q = q.select_related('location', 'model', 'category', *extra_select)
|
||||
|
||||
if location_ids:
|
||||
if hasattr(model_class, 'system'):
|
||||
q = q.filter(Q(location_id__in=location_ids) | Q(system__location_id__in=location_ids))
|
||||
else:
|
||||
q = q.filter(location_id__in=location_ids)
|
||||
|
||||
if category_id is not None:
|
||||
q = q.filter(
|
||||
Q(category_id=category_id) |
|
||||
Q(category__isnull=True, model__category_id=category_id)
|
||||
)
|
||||
|
||||
if has_search_term:
|
||||
priority = Case(
|
||||
When(code_u=qn, then=0),
|
||||
When(code_u__startswith=qn, then=1),
|
||||
When(name_fr_u__startswith=qn, then=2),
|
||||
When(name_nl_u__startswith=qn, then=3),
|
||||
When(code_u__contains=qn, then=4),
|
||||
When(name_fr_u__contains=qn, then=5),
|
||||
When(name_nl_u__contains=qn, then=6),
|
||||
default=7,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
q = q.annotate(_prio=priority).order_by('_prio', 'code')[:50]
|
||||
else:
|
||||
q = q.order_by('code')[:50]
|
||||
|
||||
results = []
|
||||
for obj in q:
|
||||
results.append((obj, ct_id))
|
||||
return results
|
||||
|
||||
all_results = (
|
||||
query_model(ITSGeoAsset, geoasset_ct_id) +
|
||||
query_model(ITSBollardSystem, bollard_system_ct_id) +
|
||||
query_model(ITSBollardPlot, bollard_plot_ct_id, ('system', 'system__location'))
|
||||
)
|
||||
|
||||
# Si un terme de recherche est fourni, filtrer dessus
|
||||
if has_search_term:
|
||||
qs = qs.filter(
|
||||
Q(code_u__contains=qn) |
|
||||
Q(name_fr_u__contains=qn) |
|
||||
Q(name_nl_u__contains=qn)
|
||||
)
|
||||
|
||||
qs = qs.exclude(status='removed') # <-- exclure les supprimés
|
||||
qs = qs.select_related(
|
||||
'location',
|
||||
'model',
|
||||
'category',
|
||||
)
|
||||
|
||||
if location_ids:
|
||||
qs = qs.filter(location_id__in=location_ids)
|
||||
if category_id is not None:
|
||||
qs = qs.filter(
|
||||
Q(category_id=category_id) |
|
||||
Q(category__isnull=True, model__category_id=category_id)
|
||||
)
|
||||
|
||||
|
||||
# Priorité : exact > code startswith > nom startswith > contient
|
||||
if has_search_term:
|
||||
priority = Case(
|
||||
When(code_u=qn, then=0),
|
||||
When(code_u__startswith=qn, then=1),
|
||||
When(name_fr_u__startswith=qn, then=2),
|
||||
When(name_nl_u__startswith=qn, then=3),
|
||||
When(code_u__contains=qn, then=4),
|
||||
When(name_fr_u__contains=qn, then=5),
|
||||
When(name_nl_u__contains=qn, then=6),
|
||||
default=7,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:100]
|
||||
else:
|
||||
# Sans terme de recherche, tri par code uniquement
|
||||
qs = qs.order_by('code')[:100]
|
||||
|
||||
data = []
|
||||
for a in qs:
|
||||
# equipment_type depuis model/category
|
||||
for a, ct_id in all_results[:100]:
|
||||
equipment_type = None
|
||||
if getattr(a, 'model', None):
|
||||
equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None)
|
||||
if not equipment_type and getattr(a, 'category', None):
|
||||
equipment_type = getattr(a.category, 'name', None)
|
||||
|
||||
# coordonnées: asset > geom(4326) > location
|
||||
lon = a.lon
|
||||
lat = a.lat
|
||||
|
||||
|
|
@ -698,24 +751,33 @@ def its_assets_autocomplete(request):
|
|||
g = a.geom.transform(4326, clone=True)
|
||||
coords = getattr(g, 'coords', None)
|
||||
if coords:
|
||||
if lon is None: lon = coords[0]
|
||||
if lat is None: lat = coords[1]
|
||||
if isinstance(coords[0], (int, float)):
|
||||
if lon is None: lon = coords[0]
|
||||
if lat is None: lat = coords[1]
|
||||
elif isinstance(coords[0], (list, tuple)) and coords[0]:
|
||||
# Pour LineString (ITSBollardSystem)
|
||||
mid = len(coords) // 2
|
||||
if lon is None: lon = coords[mid][0]
|
||||
if lat is None: lat = coords[mid][1]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if (lon is None or lat is None) and getattr(a, 'location', None):
|
||||
if lon is None and getattr(a.location, 'lon', None) is not None:
|
||||
lon = a.location.lon
|
||||
if lat is None and getattr(a.location, 'lat', None) is not None:
|
||||
lat = a.location.lat
|
||||
loc = getattr(a, 'location', None)
|
||||
if loc is None and hasattr(a, 'system') and a.system:
|
||||
loc = getattr(a.system, 'location', None)
|
||||
|
||||
if (lon is None or lat is None) and loc:
|
||||
if lon is None and getattr(loc, 'lon', None) is not None:
|
||||
lon = loc.lon
|
||||
if lat is None and getattr(loc, 'lat', None) is not None:
|
||||
lat = loc.lat
|
||||
|
||||
equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name()
|
||||
label = f"{a.code} - {equipment_label}" if a.code else equipment_label
|
||||
|
||||
location_label = (
|
||||
f"{a.location.get_name()} ({a.location.code})"
|
||||
if getattr(a, "location", None) else None
|
||||
f"{loc.get_name()} ({loc.code})"
|
||||
if loc else None
|
||||
)
|
||||
|
||||
data.append({
|
||||
|
|
@ -725,9 +787,9 @@ def its_assets_autocomplete(request):
|
|||
"equipment_id": a.id,
|
||||
"equipment_label": equipment_label,
|
||||
"equipment_type": equipment_type,
|
||||
"equipment_ct": equipment_ct_id,
|
||||
"equipment_ct": ct_id,
|
||||
|
||||
"location_id": a.location.id if getattr(a, "location", None) else None,
|
||||
"location_id": loc.id if loc else None,
|
||||
"location_label": location_label,
|
||||
"location_ct": location_ct_id,
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue