# --------------------- # Clean Models # --------------------- from django.contrib.gis.db import models from django.urls import reverse from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ from assets.models import AbstractLocation, AbstractAsset, AbstractGeoAsset, AbstractAssetModel from common.models import Municipality, Document from common.storages import PrivateMediaStorage FIXATION_TYPE_CHOICES = ( ('cemented', _('Cemented to ground')), ('bolted', _('Bolted to ground')), ('pole_mounted', _('Pole-mounted')), ('wall_mounted', _('Wall-mounted')), ('freestanding', _('Freestanding')), ('other', _('Other')), ) GLASS_TYPE_CHOICES = ( ('clear', _('Clear glass')), ('colored', _('Coloured glass')), ('mixed', _('Mixed')), ) private_storage = PrivateMediaStorage() # Clean Location class CleanLocation(AbstractLocation): geom = models.MultiPolygonField(srid=3812, null=True,blank=True) municipality = models.ForeignKey(Municipality, on_delete=models.SET_NULL, null=True, blank=True, related_name="clean_locations") road_code = models.CharField(max_length=50, null=True, blank=True) section_code = models.CharField(max_length=50, null=True, blank=True) street_level = models.CharField(max_length=50, null=True, blank=True) def get_active_assets(self): # Retrieve associated assets litter_bins = self.litter_bins.filter(status='active') glass_containers = self.glass_containers.filter(status='active') return { "litter_bins": litter_bins, "glass_containers": glass_containers, } def get_all_assets(self): # Retrieve associated assets litter_bins = self.litter_bins.all() glass_containers = self.glass_containers.all() return { "litter_bins": litter_bins, "glass_containers": glass_containers, } def get_direct_child_assets(self): # Retrieve associated assets litter_bins = self.litter_bins.all() glass_containers = self.glass_containers.all() return { "litter_bins": litter_bins, "glass_containers": glass_containers, } def get_absolute_url(self): return reverse("assets:clean_location_detail", kwargs={"location_id": self.id}) # --------------------- # Litter Bin # --------------------- class CleanLitterBinModel(AbstractAssetModel): """Reference model / template for a litter bin.""" color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour")) ral = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("RAL code")) material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material")) volume = models.FloatField(null=True, blank=True, verbose_name=_("Volume (litres)")) has_cover = models.BooleanField(default=False, verbose_name=_("Cover")) has_ashtray = models.BooleanField(default=False, verbose_name=_("Built-in ashtray")) class Meta: verbose_name = _("Litter bin model") verbose_name_plural = _("Litter bin models") ordering = ["code"] def __str__(self): return f"{self.code} - {self.get_name()}" class CleanLitterBin(AbstractGeoAsset): """Street litter bin / waste basket.""" location = models.ForeignKey( CleanLocation, related_name="litter_bins", on_delete=models.PROTECT, verbose_name=_("Location"), ) model = models.ForeignKey( CleanLitterBinModel, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Model"), ) fixation_type = models.CharField( max_length=50, choices=FIXATION_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Fixation type"), ) color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour")) material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material")) volume = models.FloatField(null=True, blank=True, verbose_name=_("Volume (litres)")) has_cover = models.BooleanField(default=False, verbose_name=_("Cover")) has_ashtray = models.BooleanField(default=False, verbose_name=_("Built-in ashtray")) has_extinguisher = models.BooleanField(default=False, verbose_name=_("Built-in extinguisher stub (éteignoir)")) emptying_frequency = models.CharField( max_length=200, null=True, blank=True, verbose_name=_("Emptying frequency"), ) collection_route = models.CharField( max_length=200, null=True, blank=True, verbose_name=_("Collection route (tournée)"), ) external_id = models.IntegerField( null=True, blank=True, verbose_name=_("External ID (source system)"), ) remarks = models.TextField(null=True, blank=True, verbose_name=_("Remarks")) class Meta: verbose_name = _("Litter bin") verbose_name_plural = _("Litter bins") indexes = [ models.Index(fields=["code"]), ] def get_absolute_url(self): return reverse("assets:clean_assets_detail", kwargs={"asset_model": "litterbin", "asset_id": self.id}) # --------------------- # Glass Container # --------------------- class CleanGlassContainerModel(AbstractAssetModel): """Reference model / template for a glass recycling container.""" color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour")) ral = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("RAL code")) material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material")) capacity = models.FloatField(null=True, blank=True, verbose_name=_("Capacity (litres)")) is_underground = models.BooleanField(default=False, verbose_name=_("Underground")) nb_openings = models.IntegerField(null=True, blank=True, verbose_name=_("Number of openings")) class Meta: verbose_name = _("Glass container model") verbose_name_plural = _("Glass container models") ordering = ["code"] def __str__(self): return f"{self.code} - {self.get_name()}" class CleanGlassContainer(AbstractGeoAsset): """Glass recycling container (bottle bank).""" location = models.ForeignKey( CleanLocation, related_name="glass_containers", on_delete=models.PROTECT, verbose_name=_("Location"), ) model = models.ForeignKey( CleanGlassContainerModel, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Model"), ) glass_type = models.CharField( max_length=50, choices=GLASS_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Accepted glass type"), ) capacity = models.FloatField(null=True, blank=True, verbose_name=_("Capacity (litres)")) is_underground = models.BooleanField(default=False, verbose_name=_("Underground")) nb_openings = models.IntegerField(null=True, blank=True, verbose_name=_("Number of openings")) color = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Colour")) material = models.CharField(max_length=200, null=True, blank=True, verbose_name=_("Material")) emptying_frequency = models.CharField( max_length=200, null=True, blank=True, verbose_name=_("Emptying frequency"), ) operator = models.CharField( max_length=255, null=True, blank=True, verbose_name=_("Operator / manager"), ) remarks = models.TextField(null=True, blank=True, verbose_name=_("Remarks")) class Meta: verbose_name = _("Glass container") verbose_name_plural = _("Glass containers") indexes = [ models.Index(fields=["code"]), ] def get_absolute_url(self): return reverse("assets:clean_assets_detail", kwargs={"asset_model": "glasscontainer", "asset_id": self.id}) class CleanLocationContract(models.Model): location = models.ForeignKey(CleanLocation, on_delete=models.CASCADE, related_name="contracts") contract = models.ForeignKey('contracts.Contract', on_delete=models.CASCADE, related_name="clean_location_contracts") status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active') maintenance_type = models.CharField(max_length=50, choices=[('clean', 'Nettoyage'), ('collection', 'Ramassage'), ('other', 'Autre')], default='other') class Meta: verbose_name = "Clean Location Contract" verbose_name_plural = "Clean Location Contracts" # --------------------- # Litter Bin Photo # --------------------- class CleanLitterBinPhoto(models.Model): """Photo associated with a litter bin.""" litter_bin = models.ForeignKey( CleanLitterBin, on_delete=models.CASCADE, related_name="photos", verbose_name=_("Litter bin"), ) photo = models.ImageField( upload_to="clean/litter_bin_photos/", null=True, blank=True, verbose_name=_("Photo"), storage=private_storage, ) source_url = models.URLField( max_length=500, null=True, blank=True, verbose_name=_("Source URL (original)"), ) order = models.PositiveSmallIntegerField(default=1, verbose_name=_("Order")) taken_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Taken at")) caption = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Caption")) class Meta: verbose_name = _("Litter bin photo") verbose_name_plural = _("Litter bin photos") ordering = ["litter_bin", "order"] def __str__(self): return f"Photo {self.order} – {self.litter_bin}" # --------------------- # Litter Bin Observation (sol + accessibilité) # --------------------- NEIGHBOURHOOD_TYPE_CHOICES = ( ('importante_ponctuelle', _('Important – ponctuel')), ('importante_continue', _('Important – continu')), ('faible', _('Faible')), ('autre', _('Autre')), ) PEDESTRIAN_DENSITY_CHOICES = ( ('high', _('Élevée')), ('medium', _('Moyenne')), ('low', _('Faible')), ) AGENT_ACCESS_CHOICES = ( ('normal', _('Normal')), ('difficult', _('Difficile')), ('impossible', _('Impossible')), ) POINT_OF_INTEREST_CHOICES = ( ('bus_stop', _('Arrêt Bus/Tram')), ('park', _('Parc')), ('school', _('École')), ('commerce', _('Commerce')), ('monument', _('Monument')), ('pedestrian_crossing', _('Passage piéton')), ('place', _('Place')), ('none', _('Aucun')), ('other', _('Autre')), ) class CleanLitterBinObservation(models.Model): """Field observation for a litter bin: ground conditions and accessibility.""" litter_bin = models.ForeignKey( CleanLitterBin, on_delete=models.CASCADE, related_name="observations", verbose_name=_("Litter bin"), ) observation_datetime = models.DateTimeField(null=True, blank=True, verbose_name=_("Observation date/time")) observer_note = models.TextField(null=True, blank=True, verbose_name=_("Observer notes")) # --- Ground / sol --- sol_pente_trop_forte = models.BooleanField(default=False, verbose_name=_("Slope too steep")) sol_instable = models.BooleanField(default=False, verbose_name=_("Unstable ground")) sol_reflechissant = models.BooleanField(default=False, verbose_name=_("Reflective surface")) sol_glissant = models.BooleanField(default=False, verbose_name=_("Slippery surface")) sol_fentes = models.BooleanField(default=False, verbose_name=_("Cracks in ground")) sol_entrave = models.BooleanField(default=False, verbose_name=_("Ground obstacle")) sol_dalles_podo = models.BooleanField(default=False, verbose_name=_("Tactile paving (dalles podotactiles)")) sol_pente_transversale = models.FloatField(null=True, blank=True, verbose_name=_("Transversal slope (%)")) sol_pente_longitudinale = models.FloatField(null=True, blank=True, verbose_name=_("Longitudinal slope (%)")) sol_remarque = models.TextField(null=True, blank=True, verbose_name=_("Ground remarks")) # --- Accessibility / accessibilité --- acc_type_quartier = models.CharField( max_length=50, choices=NEIGHBOURHOOD_TYPE_CHOICES, null=True, blank=True, verbose_name=_("Neighbourhood type"), ) acc_densite_flux_pieton = models.CharField( max_length=50, choices=PEDESTRIAN_DENSITY_CHOICES, null=True, blank=True, verbose_name=_("Pedestrian flow density"), ) # Stored as a JSON list of strings (e.g. ["bus_stop", "park"]) acc_point_interet = models.JSONField( default=list, blank=True, verbose_name=_("Points of interest"), ) acc_approche_2m = models.BooleanField(default=False, verbose_name=_("Clear 2 m approach")) acc_voirie_a_traverser = models.BooleanField(default=False, verbose_name=_("Road to cross")) acc_poubelle_isolee = models.BooleanField(default=False, verbose_name=_("Isolated bin")) acc_gene_flux_routier = models.BooleanField(default=False, verbose_name=_("Obstructs road traffic")) acc_gene_flux_pieton = models.BooleanField(default=False, verbose_name=_("Obstructs pedestrian flow")) acc_chemin_logique = models.BooleanField(default=False, verbose_name=_("Logical path (itinéraire logique)")) acc_contraste = models.BooleanField(default=False, verbose_name=_("Visual contrast")) acc_indications_tri = models.BooleanField(default=False, verbose_name=_("Sorting indicators present")) acc_agent_vehicule = models.CharField( max_length=20, choices=AGENT_ACCESS_CHOICES, null=True, blank=True, verbose_name=_("Agent access – vehicle"), ) acc_agent_pied = models.CharField( max_length=20, choices=AGENT_ACCESS_CHOICES, null=True, blank=True, verbose_name=_("Agent access – on foot"), ) acc_point_interet_autre = models.CharField( max_length=255, null=True, blank=True, verbose_name=_("Other point of interest"), ) # Integer relevance/obstruction codes stored as JSON list (e.g. [1, 3]) acc_pertinence_placement = models.JSONField( default=list, blank=True, verbose_name=_("Placement relevance codes"), ) acc_entrave = models.JSONField( default=list, blank=True, verbose_name=_("Obstruction codes"), ) acc_point_interet_remarque = models.TextField(null=True, blank=True, verbose_name=_("Point of interest remarks")) acc_remarque = models.TextField(null=True, blank=True, verbose_name=_("Accessibility remarks")) class Meta: verbose_name = _("Litter bin observation") verbose_name_plural = _("Litter bin observations") ordering = ["-observation_datetime"] def __str__(self): return f"Observation {self.observation_datetime} – {self.litter_bin}"