from django.contrib.gis.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ from assets.models import AbstractLocation, AbstractGeoAsset from assets.models.sign import SignStreet, SignPole, SignPanel SPOT_TYPE_CHOICES = ( ('bicycle', _('Stationnement vélo')), ('scooter_drop_zone', _('Drop zone trottinettes / EDPM')), ('cargo_bike', _('Stationnement vélo cargo')), ('delivery', _('Zone de livraison / Chargement-déchargement')), ('pmr', _('Emplacement PMR')), ('kiss_and_ride', _('Arrêt minute / Court-durée (CD)')), ('car_parking', _('Stationnement automobile')), ('carsharing', _('Autopartage')), ('ev_charging', _('Recharge véhicule électrique')), ('other', _('Autre')), ) STAND_TYPE_CHOICES = ( ('normal', _('Arceau standard (U inversé)')), ('cargo', _('Arceau vélo cargo')), ('mixed', _('Mixte (normal et cargo)')), ('rack', _('Râteau / Pince-roue')), ('box', _('Box / Abri sécurisé')), ('other', _('Autre')), ) MARKING_QUALITY_CHOICES = ( ('good', _('Bon état')), ('medium', _('Moyen')), ('damaged', _('Dégradé')), ('missing', _('Absent / Effacé')), ) class ParkingLocation(AbstractLocation): """ Modèle représentant un secteur ou zone d'ensemble de stationnement. """ geom = models.MultiPolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise cartographique")) description = models.TextField(null=True, blank=True, verbose_name=_("Description")) address = models.CharField(max_length=500, null=True, blank=True, verbose_name=_("Adresse")) class Meta: verbose_name = _("Zone de stationnement") verbose_name_plural = _("Zones de stationnement") ordering = ['code', 'name_fr'] class ParkingSpot(AbstractGeoAsset): """ Modèle représentant un emplacement ou zone de stationnement vélo/trottinette/PMR/livraison. Utilise une géométrie en Polygone. """ geom = models.PolygonField(srid=3812, null=True, blank=True, verbose_name=_("Emprise polygone")) location = models.ForeignKey( ParkingLocation, on_delete=models.SET_NULL, null=True, blank=True, related_name='parking_spots', verbose_name=_("Zone / Localisation parente") ) street = models.ForeignKey( SignStreet, on_delete=models.SET_NULL, null=True, blank=True, related_name='parking_spots_new', verbose_name=_("Rue") ) spot_type = models.CharField( max_length=50, choices=SPOT_TYPE_CHOICES, default='bicycle', verbose_name=_("Type d'emplacement") ) # Métriques et types d'arceaux vélos standard_stand_count = models.IntegerField(default=0, verbose_name=_("Nombre d'arceaux vélos classiques")) cargo_stand_count = models.IntegerField(default=0, verbose_name=_("Nombre d'arceaux vélos cargo")) other_stand_count = models.IntegerField(default=0, verbose_name=_("Autres arceaux / agencements")) stand_type = models.CharField( max_length=50, choices=STAND_TYPE_CHOICES, default='normal', verbose_name=_("Type d'arceau principal") ) has_scooter_drop_zone = models.BooleanField(default=False, verbose_name=_("Drop zone trottinettes / EDPM")) scooter_capacity = models.IntegerField(null=True, blank=True, verbose_name=_("Capacité estimée trottinettes")) # Métriques générales & stationnement auto/livraison/PMR capacity = models.IntegerField(null=True, blank=True, verbose_name=_("Capacité générale (nbr véhicules)")) is_paid = models.BooleanField(default=False, verbose_name=_("Payant")) time_limit_minutes = models.IntegerField(null=True, blank=True, verbose_name=_("Durée maximale (minutes)")) marking_quality = models.CharField( max_length=50, choices=MARKING_QUALITY_CHOICES, default='good', verbose_name=_("Qualité du marquage au sol") ) is_temporary = models.BooleanField(default=False, verbose_name=_("Temporaire (chantier/événement)")) # Conformité & résultat d'inspection is_compliant = models.BooleanField(default=True, verbose_name=_("Conforme aux spécifications")) non_compliance_reason = models.TextField(null=True, blank=True, verbose_name=_("Motif de non-conformité")) # Signalisation associée poles = models.ManyToManyField( SignPole, related_name='parking_spots_attached_new', blank=True, verbose_name=_("Poteaux de signalisation associés") ) panels = models.ManyToManyField( SignPanel, related_name='parking_spots_attached_new', blank=True, verbose_name=_("Panneaux de signalisation associés") ) @property def total_stand_count(self): """Retourne le nombre total d'arceaux (classiques + cargo + autres).""" return (self.standard_stand_count or 0) + (self.cargo_stand_count or 0) + (self.other_stand_count or 0) class Meta: verbose_name = _("Emplacement de stationnement") verbose_name_plural = _("Emplacements de stationnement") ordering = ['code', 'name_fr'] def get_absolute_url(self): return reverse('assets:sign_assets_detail', kwargs={'asset_model': 'parking_spot', 'asset_id': self.id}) def __str__(self): type_label = self.get_spot_type_display() name = self.get_name() if name: return f"{self.code} - {type_label} ({name})" return f"{self.code} - {type_label}"