416 lines
16 KiB
Python
416 lines
16 KiB
Python
from django.conf import settings
|
|
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, gettext_lazy as _
|
|
from django.core.exceptions import ValidationError
|
|
|
|
import os
|
|
|
|
|
|
from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel
|
|
from common.models import Municipality, Document
|
|
from common.storages import PrivateMediaStorage
|
|
|
|
private_storage = PrivateMediaStorage()
|
|
|
|
|
|
|
|
STRUCTURE_TYPE_CHOICES = (
|
|
('tunnel', 'Tunnnel'),
|
|
('bridge', 'Pont'),
|
|
('viaduct', 'Viaduc'),
|
|
('overpass', 'Passerelle'),
|
|
('underpass', 'Passage souterrain'),
|
|
('gallery', 'Galerie technique'),
|
|
('atypical', 'Ouvrage atypique'),
|
|
)
|
|
|
|
|
|
# Structure locations
|
|
class StructureLocation(AbstractLocation):
|
|
geom = models.MultiPolygonField(srid=3812, null=True,blank=True)
|
|
parent_location = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_locations")
|
|
is_main_location = models.BooleanField(default=False, verbose_name="Main Location")
|
|
structure = models.ForeignKey('Structure', on_delete=models.SET_NULL, null=True, blank=True, related_name="all_locations")
|
|
|
|
def get_active_assets(self):
|
|
def collect_assets(location):
|
|
assets = list(location.assets.filter(status='active'))
|
|
for child in location.child_locations.all():
|
|
assets += collect_assets(child)
|
|
return assets
|
|
|
|
return collect_assets(self)
|
|
|
|
def get_all_assets(self):
|
|
def collect_assets(location):
|
|
assets = list(location.assets.all())
|
|
for child in location.child_locations.all():
|
|
assets += collect_assets(child)
|
|
return assets
|
|
|
|
return collect_assets(self)
|
|
|
|
def get_direct_child_assets(self):
|
|
return self.assets.all()
|
|
|
|
def clean(self):
|
|
if self.is_main_location and StructureLocation.objects.filter(structure=self.structure, is_main_location=True).exclude(pk=self.pk).exists():
|
|
raise ValidationError("Cette structure a déjà une localisation principale.")
|
|
|
|
def get_absolute_url(self):
|
|
return reverse(
|
|
'assets:structures_locations_detail',
|
|
kwargs={
|
|
# 'asset_model': content_type.model,
|
|
'location_id': self.id
|
|
}
|
|
)
|
|
|
|
|
|
class Meta:
|
|
verbose_name = "Structure Location"
|
|
verbose_name_plural = "Structure Locations"
|
|
ordering = ["code"]
|
|
|
|
|
|
class Structure(models.Model):
|
|
code = models.CharField(max_length=50, unique=True)
|
|
name_fr = models.CharField(max_length=200,null=True,blank=True)
|
|
name_nl = models.CharField(max_length=200,null=True,blank=True)
|
|
structure_type = models.CharField(max_length=200, choices=STRUCTURE_TYPE_CHOICES, null=True, blank=True)
|
|
main_location = models.OneToOneField(StructureLocation, on_delete=models.SET_NULL, null=True, blank=True, related_name="main_structure")
|
|
description = models.TextField(null=True, blank=True)
|
|
linked_folder = models.CharField(max_length=1024, null=True, blank=True)
|
|
structure_number = models.IntegerField(null=True, blank=True, verbose_name="Numéro de l'ouvrage")
|
|
|
|
def get_name(self):
|
|
lang = get_language()
|
|
name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr
|
|
return name
|
|
|
|
def get_active_assets(self):
|
|
return self.assets.filter(status='active')
|
|
|
|
def get_all_assets(self):
|
|
return self.assets.all()
|
|
|
|
|
|
def get_all_locations(self):
|
|
return self.all_locations.all()
|
|
|
|
def get_main_location(self):
|
|
return self.main_location
|
|
|
|
def __str__(self):
|
|
return self.code + " - " + self.get_name()
|
|
|
|
class Meta:
|
|
ordering = ["code"]
|
|
|
|
|
|
def upload_to_structure(instance, filename):
|
|
location = instance.structure
|
|
code = slugify(location.code or "unknown") # Fallback si code est None
|
|
return os.path.join("assets_documents", "structures", code, filename)
|
|
|
|
TU_DOCUMENT_TYPE_CHOICES = (
|
|
('as_built', 'As Built'),
|
|
('cme', "CME"),
|
|
('inspection_ce', 'Inspection G.C.'),
|
|
('inspection_em', 'Inspection E.M.'),
|
|
('pis', 'PIS'),
|
|
('other', 'Autre'),
|
|
)
|
|
|
|
# Structure Location Document
|
|
class StructureDocument(Document):
|
|
structure = models.ForeignKey(Structure, on_delete=models.CASCADE, related_name="documents", verbose_name="Associated Structure")
|
|
file = models.FileField(upload_to=upload_to_structure, storage=private_storage, verbose_name="Fichier", max_length=255)
|
|
thumbnail = models.ImageField(upload_to=upload_to_structure, storage=private_storage, null=True, blank=True, max_length=255)
|
|
document_type = models.CharField(max_length=50, choices=TU_DOCUMENT_TYPE_CHOICES, default='other', verbose_name="Type de document")
|
|
|
|
|
|
SC_MAINTENANCE_TYPE_CHOICES = (
|
|
('electromeca', 'Eléctromécanique'),
|
|
('civil_engineering', 'Génie civil'),
|
|
('urgent_cleaning', 'Nettoyage urgent'),
|
|
('other', 'Autre'),
|
|
)
|
|
|
|
class StructureContract(models.Model):
|
|
structure = models.ForeignKey(Structure, on_delete=models.CASCADE, related_name="contracts")
|
|
contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="structure_contracts")
|
|
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
|
|
maintenance_type = models.CharField(max_length=50, choices=SC_MAINTENANCE_TYPE_CHOICES, default='other')
|
|
|
|
def __str__(self):
|
|
return f"{self.structure.code} - {self.contract.contract_number}"
|
|
|
|
class Meta:
|
|
verbose_name = "Structure Contract"
|
|
verbose_name_plural = "Structure Contracts"
|
|
unique_together = ('structure', 'contract')
|
|
|
|
|
|
class StructureAssetModel(AbstractAssetModel):
|
|
class Meta:
|
|
verbose_name = "Structure Asset Model"
|
|
verbose_name_plural = "Structure Asset Models"
|
|
|
|
|
|
class StructureGeoAsset(AbstractGeoAsset):
|
|
model = models.ForeignKey(StructureAssetModel, on_delete=models.PROTECT)
|
|
location = models.ForeignKey(StructureLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True)
|
|
structure = models.ForeignKey(Structure, on_delete=models.SET_NULL, related_name="assets", null=True, blank=True)
|
|
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets")
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
|
self.geom = self.parent_asset.geom
|
|
self.lon = self.parent_asset.lon
|
|
self.lat = self.parent_asset.lat
|
|
self.geojson = self.parent_asset.geojson
|
|
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
|
self.location = self.parent_asset.location
|
|
if self.parent_asset and self.parent_asset.structure and not self.structure_id:
|
|
self.structure = self.parent_asset.structure
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
|
self.geom = self.parent_asset.geom
|
|
self.lon = self.parent_asset.lon
|
|
self.lat = self.parent_asset.lat
|
|
self.geojson = self.parent_asset.geojson
|
|
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
|
self.location = self.parent_asset.location
|
|
if self.parent_asset and self.parent_asset.structure and not self.structure_id:
|
|
self.structure = self.parent_asset.structure
|
|
super().save(*args, **kwargs)
|
|
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=["code"]),
|
|
]
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('assets:structures_assets_detail', kwargs={'asset_model': 'structuregeoasset', 'asset_id':self.id})
|
|
|
|
|
|
TE_PASSAGE_STATUS_CHOICES = (
|
|
('authorized', _('Autorisé sans condition particulière')),
|
|
('conditional', _('Autorisé sous conditions / prescriptions')),
|
|
('prior_study_required', _('Étude / calcul préalable obligatoire')),
|
|
('forbidden', _('Strictement interdit')),
|
|
('unknown', _('Non évalué')),
|
|
)
|
|
|
|
TE_LANE_POSITIONING_CHOICES = (
|
|
('standard', _('Voie de circulation normale')),
|
|
('center_axis', _('Dans l\'axe de l\'ouvrage / ligne médiane')),
|
|
('first_lane', _('Première bande de circulation')),
|
|
('tram_tracks', _('Sur les voies de tram')),
|
|
('specific_path', _('Trajectoire spécifique (voir prescriptions)')),
|
|
)
|
|
|
|
|
|
class StructureExceptionalTransport(models.Model):
|
|
structure = models.OneToOneField(
|
|
Structure,
|
|
on_delete=models.CASCADE,
|
|
related_name='exceptional_transport_info',
|
|
verbose_name=_("Ouvrage associé")
|
|
)
|
|
|
|
# --- Références administratives & Classification SPF / Région ---
|
|
fod_vnr = models.IntegerField(
|
|
null=True, blank=True,
|
|
verbose_name=_("Numéro d'ordre FOD/SPF (VNR)"),
|
|
help_text=_("Volgnummer pont dans la base fédérale")
|
|
)
|
|
national_id = models.CharField(
|
|
max_length=50, null=True, blank=True,
|
|
verbose_name=_("Identifiant national (IDN)")
|
|
)
|
|
bridge_class = models.CharField(
|
|
max_length=100, null=True, blank=True,
|
|
verbose_name=_("Classe de charge du pont (BKL)"),
|
|
help_text=_("Ex: T180152402024030, D18015..., T09015")
|
|
)
|
|
is_railway_metro_crossing = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Ouvrage ferroviaire / métro / tram (Spoorlijn)"),
|
|
help_text=_("Indique si l'ouvrage surplombe ou supporte des voies ferrées/métro/tram")
|
|
)
|
|
number_of_spans = models.PositiveSmallIntegerField(
|
|
null=True, blank=True,
|
|
verbose_name=_("Nombre de travées (OVS)")
|
|
)
|
|
|
|
# --- Capacités portantes & Tonnages ---
|
|
max_authorized_weight_tonnes = models.DecimalField(
|
|
max_digits=6, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Poids max. autorisé standard (MAXWA) [t]"),
|
|
help_text=_("Poids maximal standard sans étude particulière (ex: 44 t)")
|
|
)
|
|
min_guaranteed_capacity_tonnes = models.DecimalField(
|
|
max_digits=6, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Capacité minimale garantie (MINWR) [t]")
|
|
)
|
|
max_calculated_weight_tonnes = models.DecimalField(
|
|
max_digits=6, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Tonnage maximal admissible calculé (MX) [t]"),
|
|
help_text=_("Charge maximale absolue évaluée pour l'ouvrage (ex: 140t, 265t, 450t)")
|
|
)
|
|
max_axle_load_tonnes = models.DecimalField(
|
|
max_digits=5, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Charge max. par essieu [t/essieu]"),
|
|
help_text=_("Ex: 12.0, 12.8, 13.0 t/essieu")
|
|
)
|
|
min_axle_spacing_m = models.DecimalField(
|
|
max_digits=4, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Entraxe minimal entre essieux [m]")
|
|
)
|
|
|
|
# --- Gabarits géométriques & Hauteurs libres ---
|
|
clearance_height_left_m = models.DecimalField(
|
|
max_digits=4, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Hauteur libre gauche / entrée (H1) [m]")
|
|
)
|
|
clearance_height_center_m = models.DecimalField(
|
|
max_digits=4, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Hauteur libre dans l'axe (H2) [m]")
|
|
)
|
|
clearance_height_right_m = models.DecimalField(
|
|
max_digits=4, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Hauteur libre droite / sortie (H3) [m]")
|
|
)
|
|
usable_width_m = models.DecimalField(
|
|
max_digits=5, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Largeur utile roulable (BRE) [m]")
|
|
)
|
|
|
|
# --- Prescriptions de franchissement & Exploitation ---
|
|
passage_status = models.CharField(
|
|
max_length=30,
|
|
choices=TE_PASSAGE_STATUS_CHOICES,
|
|
default='unknown',
|
|
verbose_name=_("Statut d'admissibilité TE")
|
|
)
|
|
max_speed_kmh = models.PositiveIntegerField(
|
|
default=5, null=True, blank=True,
|
|
verbose_name=_("Vitesse maximale de franchissement (km/h)"),
|
|
help_text=_("Généralement 5 km/h pour supprimer les effets dynamiques")
|
|
)
|
|
is_braking_forbidden = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Interdiction formelle de freiner sur l'ouvrage"),
|
|
help_text=_("Notamment sur les dalles de toit de métro")
|
|
)
|
|
is_isolated_crossing_only = models.BooleanField(
|
|
default=True,
|
|
verbose_name=_("Convoi seul sur l'ouvrage"),
|
|
help_text=_("Aucune autre charge mobile simultanée autorisée sur l'ouvrage")
|
|
)
|
|
safety_distance_m = models.PositiveIntegerField(
|
|
default=5, null=True, blank=True,
|
|
verbose_name=_("Distance de sécurité / zone tampon (m)"),
|
|
help_text=_("Distance minimale avec tout autre véhicule (souvent 5m)")
|
|
)
|
|
traffic_interruption_required = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Interruption de circulation requise pendant le passage")
|
|
)
|
|
lane_positioning = models.CharField(
|
|
max_length=30,
|
|
choices=TE_LANE_POSITIONING_CHOICES,
|
|
default='standard',
|
|
verbose_name=_("Positionnement sur la chaussée")
|
|
)
|
|
is_escort_required = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Escorte policière ou accompagnement requis")
|
|
)
|
|
|
|
# --- Prescriptions textuelles bilingues ---
|
|
prescriptions_fr = models.TextField(
|
|
null=True, blank=True,
|
|
verbose_name=_("Prescriptions techniques (FR)")
|
|
)
|
|
prescriptions_nl = models.TextField(
|
|
null=True, blank=True,
|
|
verbose_name=_("Prescriptions techniques (NL)")
|
|
)
|
|
|
|
# --- Suivi & Métadonnées ---
|
|
last_study_date = models.DateField(
|
|
null=True, blank=True,
|
|
verbose_name=_("Date de la dernière étude / note de calcul")
|
|
)
|
|
last_study_ref = models.CharField(
|
|
max_length=150, null=True, blank=True,
|
|
verbose_name=_("Référence de la dernière note de calcul / Mission")
|
|
)
|
|
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
updated_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True, blank=True,
|
|
related_name='updated_structure_te_infos'
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("Transport exceptionnel (Ouvrage)")
|
|
verbose_name_plural = _("Transports exceptionnels (Ouvrages)")
|
|
|
|
def __str__(self):
|
|
return f"TE - {self.structure.code} ({self.get_passage_status_display()})"
|
|
|
|
|
|
class StructureExceptionalTransportNotice(models.Model):
|
|
RESULT_CHOICES = (
|
|
('positive', _('Favorable / Positif')),
|
|
('conditional', _('Favorable sous conditions')),
|
|
('negative', _('Défavorable / Négatif')),
|
|
)
|
|
|
|
structure = models.ForeignKey(
|
|
Structure,
|
|
on_delete=models.CASCADE,
|
|
related_name='exceptional_transport_notices',
|
|
verbose_name=_("Ouvrage")
|
|
)
|
|
mission_reference = models.CharField(
|
|
max_length=100,
|
|
verbose_name=_("Référence mission / Avis DITP"),
|
|
help_text=_("Ex: Mission 64, Avis DITP 468830")
|
|
)
|
|
notice_date = models.DateField(null=True, blank=True, verbose_name=_("Date de l'avis"))
|
|
result = models.CharField(max_length=20, choices=RESULT_CHOICES, default='positive', verbose_name=_("Résultat"))
|
|
|
|
evaluated_total_weight_tonnes = models.DecimalField(
|
|
max_digits=6, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Tonnage évalué [t]")
|
|
)
|
|
evaluated_axle_load_tonnes = models.DecimalField(
|
|
max_digits=5, decimal_places=2, null=True, blank=True,
|
|
verbose_name=_("Charge par essieu évaluée [t/essieu]")
|
|
)
|
|
comments = models.TextField(blank=True, verbose_name=_("Conditions / Remarques de l'avis"))
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
class Meta:
|
|
verbose_name = _("Avis / Mission d'étude TE")
|
|
verbose_name_plural = _("Avis / Missions d'études TE")
|
|
ordering = ['-notice_date', '-created_at']
|
|
|
|
def __str__(self):
|
|
return f"{self.structure.code} - {self.mission_reference} ({self.get_result_display()})"
|
|
|
|
|
|
|