diff --git a/loko/assets/admin.py b/loko/assets/admin.py index 931f0ff..6d81ed6 100644 --- a/loko/assets/admin.py +++ b/loko/assets/admin.py @@ -321,7 +321,10 @@ admin.site.register(ArtworkFountainDocument, AssetAdmin) admin.site.register(ArtworkMonumentDocument, AssetAdmin) -from .models import Structure, StructureLocation, StructureDocument, StructureAssetModel, StructureGeoAsset, StructureContract +from .models import ( + Structure, StructureLocation, StructureDocument, StructureAssetModel, StructureGeoAsset, StructureContract, + StructureExceptionalTransport, StructureExceptionalTransportNotice +) def make_folder_attachment_inline(thematic_code): @@ -359,12 +362,26 @@ StructureFolderAttachmentInline = make_folder_attachment_inline('structures') ControlCenterFolderAttachmentInline = make_folder_attachment_inline('control_centers') TrafficLightFolderAttachmentInline = make_folder_attachment_inline('traffic_lights') + +class StructureExceptionalTransportInline(admin.StackedInline): + model = StructureExceptionalTransport + extra = 0 + can_delete = False + verbose_name = "Transport exceptionnel (Convois lourds)" + verbose_name_plural = "Transport exceptionnel (Convois lourds)" + + +class StructureExceptionalTransportNoticeInline(admin.TabularInline): + model = StructureExceptionalTransportNotice + extra = 0 + + @admin.register(Structure) class StructureAdmin(AssetAdmin): search_fields = ("code", "name_fr", "name_nl") list_display = ("code", "name_fr", "structure_type") list_filter = ("structure_type",) - inlines = [StructureFolderAttachmentInline] + inlines = [StructureFolderAttachmentInline, StructureExceptionalTransportInline, StructureExceptionalTransportNoticeInline] @admin.register(StructureLocation) class StructureLocationAdmin(AssetAdmin): @@ -373,9 +390,21 @@ class StructureLocationAdmin(AssetAdmin): admin.site.register(StructureDocument, AssetAdmin) admin.site.register(StructureAssetModel, AssetAdmin) admin.site.register(StructureGeoAsset, AssetAdmin) - admin.site.register(StructureContract, AssetAdmin) +@admin.register(StructureExceptionalTransport) +class StructureExceptionalTransportAdmin(admin.ModelAdmin): + search_fields = ("structure__code", "structure__name_fr", "structure__name_nl", "bridge_class", "national_id", "fod_vnr") + list_display = ("structure", "passage_status", "max_authorized_weight_tonnes", "max_calculated_weight_tonnes", "max_speed_kmh", "updated_at") + list_filter = ("passage_status", "is_braking_forbidden", "is_isolated_crossing_only", "is_railway_metro_crossing") + +@admin.register(StructureExceptionalTransportNotice) +class StructureExceptionalTransportNoticeAdmin(admin.ModelAdmin): + search_fields = ("structure__code", "mission_reference") + list_display = ("structure", "mission_reference", "notice_date", "result", "evaluated_total_weight_tonnes") + list_filter = ("result", "notice_date") + + from .models import ControlCenter, ControlCenterLocation, ControlCenterDocument, ControlCenterAssetModel, ControlCenterGeoAsset, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument diff --git a/loko/assets/forms.py b/loko/assets/forms.py index 2d15324..6ad072a 100644 --- a/loko/assets/forms.py +++ b/loko/assets/forms.py @@ -388,4 +388,55 @@ class BuildingAssetForm(forms.ModelForm): 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) \ No newline at end of file + self.fields['category'].queryset = AssetCategory.objects.filter(thematic=t) + + +class StructureExceptionalTransportForm(forms.ModelForm): + """Formulaire d'édition des paramètres de transport exceptionnel sur un ouvrage.""" + class Meta: + from assets.models.structures import StructureExceptionalTransport + model = StructureExceptionalTransport + fields = [ + 'passage_status', 'fod_vnr', 'national_id', 'bridge_class', 'is_railway_metro_crossing', 'number_of_spans', + 'max_authorized_weight_tonnes', 'min_guaranteed_capacity_tonnes', 'max_calculated_weight_tonnes', + 'max_axle_load_tonnes', 'min_axle_spacing_m', + 'clearance_height_left_m', 'clearance_height_center_m', 'clearance_height_right_m', 'usable_width_m', + 'max_speed_kmh', 'is_braking_forbidden', 'is_isolated_crossing_only', 'safety_distance_m', + 'traffic_interruption_required', 'lane_positioning', 'is_escort_required', + 'prescriptions_fr', 'prescriptions_nl', + 'last_study_date', 'last_study_ref', + ] + widgets = { + 'passage_status': forms.Select(attrs={'class': 'form-select'}), + 'fod_vnr': forms.NumberInput(attrs={'class': 'form-control'}), + 'national_id': forms.TextInput(attrs={'class': 'form-control'}), + 'bridge_class': forms.TextInput(attrs={'class': 'form-control'}), + 'is_railway_metro_crossing': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'number_of_spans': forms.NumberInput(attrs={'class': 'form-control'}), + 'max_authorized_weight_tonnes': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'min_guaranteed_capacity_tonnes': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'max_calculated_weight_tonnes': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'max_axle_load_tonnes': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'min_axle_spacing_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'clearance_height_left_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'clearance_height_center_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'clearance_height_right_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'usable_width_m': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), + 'max_speed_kmh': forms.NumberInput(attrs={'class': 'form-control'}), + 'is_braking_forbidden': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'is_isolated_crossing_only': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'safety_distance_m': forms.NumberInput(attrs={'class': 'form-control'}), + 'traffic_interruption_required': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'lane_positioning': forms.Select(attrs={'class': 'form-select'}), + 'is_escort_required': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + 'prescriptions_fr': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + 'prescriptions_nl': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + 'last_study_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), + 'last_study_ref': forms.TextInput(attrs={'class': 'form-control'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for name, field in self.fields.items(): + if not isinstance(field.widget, forms.CheckboxInput): + field.required = False \ No newline at end of file diff --git a/loko/assets/migrations/0024_structureexceptionaltransport_and_more.py b/loko/assets/migrations/0024_structureexceptionaltransport_and_more.py new file mode 100644 index 0000000..086a868 --- /dev/null +++ b/loko/assets/migrations/0024_structureexceptionaltransport_and_more.py @@ -0,0 +1,75 @@ +# Generated by Django 6.0.7 on 2026-08-21 08:20 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0023_add_trafficlight_programming_category'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='StructureExceptionalTransport', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('fod_vnr', models.IntegerField(blank=True, help_text='Volgnummer pont dans la base fédérale', null=True, verbose_name="Numéro d'ordre FOD/SPF (VNR)")), + ('national_id', models.CharField(blank=True, max_length=50, null=True, verbose_name='Identifiant national (IDN)')), + ('bridge_class', models.CharField(blank=True, help_text='Ex: T180152402024030, D18015..., T09015', max_length=100, null=True, verbose_name='Classe de charge du pont (BKL)')), + ('is_railway_metro_crossing', models.BooleanField(default=False, help_text="Indique si l'ouvrage surplombe ou supporte des voies ferrées/métro/tram", verbose_name='Ouvrage ferroviaire / métro / tram (Spoorlijn)')), + ('number_of_spans', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Nombre de travées (OVS)')), + ('max_authorized_weight_tonnes', models.DecimalField(blank=True, decimal_places=2, help_text='Poids maximal standard sans étude particulière (ex: 44 t)', max_digits=6, null=True, verbose_name='Poids max. autorisé standard (MAXWA) [t]')), + ('min_guaranteed_capacity_tonnes', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True, verbose_name='Capacité minimale garantie (MINWR) [t]')), + ('max_calculated_weight_tonnes', models.DecimalField(blank=True, decimal_places=2, help_text="Charge maximale absolue évaluée pour l'ouvrage (ex: 140t, 265t, 450t)", max_digits=6, null=True, verbose_name='Tonnage maximal admissible calculé (MX) [t]')), + ('max_axle_load_tonnes', models.DecimalField(blank=True, decimal_places=2, help_text='Ex: 12.0, 12.8, 13.0 t/essieu', max_digits=5, null=True, verbose_name='Charge max. par essieu [t/essieu]')), + ('min_axle_spacing_m', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name='Entraxe minimal entre essieux [m]')), + ('clearance_height_left_m', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name='Hauteur libre gauche / entrée (H1) [m]')), + ('clearance_height_center_m', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name="Hauteur libre dans l'axe (H2) [m]")), + ('clearance_height_right_m', models.DecimalField(blank=True, decimal_places=2, max_digits=4, null=True, verbose_name='Hauteur libre droite / sortie (H3) [m]')), + ('usable_width_m', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='Largeur utile roulable (BRE) [m]')), + ('passage_status', models.CharField(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é')], default='unknown', max_length=30, verbose_name="Statut d'admissibilité TE")), + ('max_speed_kmh', models.PositiveIntegerField(blank=True, default=5, help_text='Généralement 5 km/h pour supprimer les effets dynamiques', null=True, verbose_name='Vitesse maximale de franchissement (km/h)')), + ('is_braking_forbidden', models.BooleanField(default=False, help_text='Notamment sur les dalles de toit de métro', verbose_name="Interdiction formelle de freiner sur l'ouvrage")), + ('is_isolated_crossing_only', models.BooleanField(default=True, help_text="Aucune autre charge mobile simultanée autorisée sur l'ouvrage", verbose_name="Convoi seul sur l'ouvrage")), + ('safety_distance_m', models.PositiveIntegerField(blank=True, default=5, help_text='Distance minimale avec tout autre véhicule (souvent 5m)', null=True, verbose_name='Distance de sécurité / zone tampon (m)')), + ('traffic_interruption_required', models.BooleanField(default=False, verbose_name='Interruption de circulation requise pendant le passage')), + ('lane_positioning', models.CharField(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)')], default='standard', max_length=30, verbose_name='Positionnement sur la chaussée')), + ('is_escort_required', models.BooleanField(default=False, verbose_name='Escorte policière ou accompagnement requis')), + ('prescriptions_fr', models.TextField(blank=True, null=True, verbose_name='Prescriptions techniques (FR)')), + ('prescriptions_nl', models.TextField(blank=True, null=True, verbose_name='Prescriptions techniques (NL)')), + ('last_study_date', models.DateField(blank=True, null=True, verbose_name='Date de la dernière étude / note de calcul')), + ('last_study_ref', models.CharField(blank=True, max_length=150, null=True, verbose_name='Référence de la dernière note de calcul / Mission')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('structure', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='exceptional_transport_info', to='assets.structure', verbose_name='Ouvrage associé')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_structure_te_infos', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Transport exceptionnel (Ouvrage)', + 'verbose_name_plural': 'Transports exceptionnels (Ouvrages)', + }, + ), + migrations.CreateModel( + name='StructureExceptionalTransportNotice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('mission_reference', models.CharField(help_text='Ex: Mission 64, Avis DITP 468830', max_length=100, verbose_name='Référence mission / Avis DITP')), + ('notice_date', models.DateField(blank=True, null=True, verbose_name="Date de l'avis")), + ('result', models.CharField(choices=[('positive', 'Favorable / Positif'), ('conditional', 'Favorable sous conditions'), ('negative', 'Défavorable / Négatif')], default='positive', max_length=20, verbose_name='Résultat')), + ('evaluated_total_weight_tonnes', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True, verbose_name='Tonnage évalué [t]')), + ('evaluated_axle_load_tonnes', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=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(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('structure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exceptional_transport_notices', to='assets.structure', verbose_name='Ouvrage')), + ], + options={ + 'verbose_name': "Avis / Mission d'étude TE", + 'verbose_name_plural': "Avis / Missions d'études TE", + 'ordering': ['-notice_date', '-created_at'], + }, + ), + ] diff --git a/loko/assets/models/__init__.py b/loko/assets/models/__init__.py index c2882e1..ef9106d 100644 --- a/loko/assets/models/__init__.py +++ b/loko/assets/models/__init__.py @@ -4,7 +4,7 @@ from .trafficlights import TrafficLightIntersection, TrafficLightIntersectionDoc from .publiclighting import PublicLightingStreet, PublicLightingStreetDocument, PublicLightingPoleModel, PublicLightingPole, PublicLightingLightFixtureModel, PublicLightingLightFixture, PublicLightingElectricalCabinetModel, PublicLightingElectricalCabinet from .artworks import ArtworkMonument, ArtworkFountain, ArtworkFountainDocument, ArtworkMonumentDocument, ArtworkStreet from .nature import NatureLocation, NatureAssetModel, NatureGreenSurface, NatureGreenSurfaceDocument, NatureTree, NatureShrub, NatureBench, GreenSurfacesComment, GreenSurfacesCommentPhoto, NatureRWIZ, NatureRWIAsset, NatureRWIAssetDocument -from .structures import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel, StructureDocument, StructureContract +from .structures import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel, StructureDocument, StructureContract, StructureExceptionalTransport, StructureExceptionalTransportNotice 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 diff --git a/loko/assets/models/structures.py b/loko/assets/models/structures.py index 32211c0..edbc81e 100644 --- a/loko/assets/models/structures.py +++ b/loko/assets/models/structures.py @@ -1,11 +1,8 @@ -# ----------------------------- -# Engineering Structures Models -# ----------------------------- - +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 +from django.utils.translation import get_language, gettext_lazy as _ from django.core.exceptions import ValidationError import os @@ -202,3 +199,218 @@ class StructureGeoAsset(AbstractGeoAsset): 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()})" + + + diff --git a/loko/assets/permissions.py b/loko/assets/permissions.py index 060967a..27213ee 100644 --- a/loko/assets/permissions.py +++ b/loko/assets/permissions.py @@ -742,3 +742,45 @@ def location_asset_edit_permission_required(thematic_code): return wrapper return decorator + + +def can_view_exceptional_transport(user): + """ + Vérifie si l'utilisateur a le droit de visualiser les données et documents de transport exceptionnel sur les ouvrages. + """ + if not user or not user.is_authenticated: + return False + if getattr(user, 'is_superuser', False) or getattr(user, 'is_staff', False): + return True + user_config = getattr(user, 'config', None) + if not user_config: + try: + user_config = UserConfig.objects.get(user=user) + except Exception: + return False + if user_config.has_role('admin') or user_config.has_role('top_manager'): + return True + return bool(getattr(user_config, 'has_exceptional_transport_profile', False)) + + +def can_edit_exceptional_transport(user): + """ + Vérifie si l'utilisateur a le droit de modifier les données de transport exceptionnel sur les ouvrages. + """ + if not user or not user.is_authenticated: + return False + if getattr(user, 'is_superuser', False): + return True + user_config = getattr(user, 'config', None) + if not user_config: + try: + user_config = UserConfig.objects.get(user=user) + except Exception: + return False + if user_config.has_role('admin'): + return True + return bool( + getattr(user_config, 'has_exceptional_transport_profile', False) + and getattr(user_config, 'exceptional_transport_can_edit', False) + ) + diff --git a/loko/assets/templates/assets/structures_location_detail.html b/loko/assets/templates/assets/structures_location_detail.html index 15f9f3f..ec4cc9b 100644 --- a/loko/assets/templates/assets/structures_location_detail.html +++ b/loko/assets/templates/assets/structures_location_detail.html @@ -266,6 +266,301 @@ {% endif %} + + {% if can_view_exceptional_transport and location.structure %} +
| {% translate "Poids max. standard (MAXWA)" %} : | +{% if te_info.max_authorized_weight_tonnes %}{{ te_info.max_authorized_weight_tonnes }} t{% else %}-{% endif %} | +
|---|---|
| {% translate "Tonnage max. calculé (MX)" %} : | +{% if te_info.max_calculated_weight_tonnes %}{{ te_info.max_calculated_weight_tonnes }} t{% else %}-{% endif %} | +
| {% translate "Capacité min. garantie (MINWR)" %} : | +{% if te_info.min_guaranteed_capacity_tonnes %}{{ te_info.min_guaranteed_capacity_tonnes }} t{% else %}-{% endif %} | +
| {% translate "Charge max. / essieu" %} : | +{% if te_info.max_axle_load_tonnes %}{{ te_info.max_axle_load_tonnes }} t/essieu{% else %}-{% endif %} | +
| {% translate "Entraxe minimal essieux" %} : | +{% if te_info.min_axle_spacing_m %}{{ te_info.min_axle_spacing_m }} m{% else %}-{% endif %} | +
| {% translate "Classe de pont (BKL)" %} : | +{{ te_info.bridge_class|default:"-" }} |
+
|---|---|
| {% translate "N° FOD / ID National" %} : | +{{ te_info.fod_vnr|default:"-" }} / {{ te_info.national_id|default:"-" }} | +
| {% translate "Hauteurs libres (G / Axe / D)" %} : | ++ {{ te_info.clearance_height_left_m|default:"-" }} m / + {{ te_info.clearance_height_center_m|default:"-" }} m / + {{ te_info.clearance_height_right_m|default:"-" }} m + | +
| {% translate "Largeur utile (BRE)" %} : | +{% if te_info.usable_width_m %}{{ te_info.usable_width_m }} m{% else %}-{% endif %} | +
| {% translate "Franchissement ferroviaire / métro" %} : | ++ {% if te_info.is_railway_metro_crossing %} + {% translate "Oui" %} + {% else %} + {% translate "Non" %} + {% endif %} + | +
{{ te_info.prescriptions_fr }}
+{{ te_info.prescriptions_nl }}
+{% translate "Aucune prescription textuelle spécifique enregistrée." %}
+ {% endif %} + + {% if te_info.last_study_ref or te_info.last_study_date %} +{% translate "Aucun document spécifique dans le répertoire Transport exceptionnel." %}
+| {% translate "Mission / Réf" %} | +{% translate "Date" %} | +{% translate "Résultat" %} | +{% translate "Tonnage évalué" %} | +{% translate "Charge / essieu" %} | +{% translate "Commentaires" %} | +
|---|---|---|---|---|---|
| {{ notice.mission_reference }} | +{{ notice.notice_date|date:"d/m/Y"|default:"-" }} | ++ {% if notice.result == 'positive' %} + {% translate "Favorable" %} + {% elif notice.result == 'conditional' %} + {% translate "Sous conditions" %} + {% else %} + {% translate "Défavorable" %} + {% endif %} + | +{% if notice.evaluated_total_weight_tonnes %}{{ notice.evaluated_total_weight_tonnes }} t{% else %}-{% endif %} | +{% if notice.evaluated_axle_load_tonnes %}{{ notice.evaluated_axle_load_tonnes }} t{% else %}-{% endif %} | +{{ notice.comments|default:"-" }} | +