feat: implement parking spot models and generic asset inspection framework
This commit is contained in:
parent
70c3ff3dea
commit
dad5acc4fb
8 changed files with 833 additions and 2 deletions
|
|
@ -681,4 +681,48 @@ class OutdoorAssetAdmin(admin.GISModelAdmin):
|
|||
search_fields = ("code", "name_fr", "name_nl", "serial_number", "certifying_body")
|
||||
raw_id_fields = ("site", "location", "parent_asset")
|
||||
|
||||
admin.site.register(OutdoorLocationDocument, AssetAdmin)
|
||||
admin.site.register(OutdoorLocationDocument, AssetAdmin)
|
||||
|
||||
from .models import ParkingLocation, ParkingSpot, AssetInspection, ParkingSpotInspection
|
||||
|
||||
@admin.register(ParkingLocation)
|
||||
class ParkingLocationAdmin(AssetGISAdmin):
|
||||
list_display = ("code", "name_fr", "name_nl", "address")
|
||||
search_fields = ("code", "name_fr", "name_nl", "address")
|
||||
|
||||
@admin.register(ParkingSpot)
|
||||
class ParkingSpotAdmin(AssetGISAdmin):
|
||||
list_display = (
|
||||
"code", "name_fr", "spot_type", "stand_type",
|
||||
"standard_stand_count", "cargo_stand_count", "total_stand_count",
|
||||
"has_scooter_drop_zone", "is_compliant", "inspection_status", "status"
|
||||
)
|
||||
list_filter = (
|
||||
"spot_type", "stand_type", "has_scooter_drop_zone",
|
||||
"is_compliant", "inspection_status", "marking_quality", "is_paid", "status"
|
||||
)
|
||||
search_fields = ("code", "name_fr", "name_nl", "note")
|
||||
filter_horizontal = ("poles", "panels")
|
||||
raw_id_fields = ("location", "street")
|
||||
|
||||
@admin.register(AssetInspection)
|
||||
class AssetInspectionAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"id", "asset", "inspector", "inspection_date",
|
||||
"result_status", "apply_changes_to_asset"
|
||||
)
|
||||
list_filter = ("result_status", "apply_changes_to_asset", "inspection_date")
|
||||
search_fields = ("notes", "inspector__username", "inspector__first_name", "inspector__last_name")
|
||||
raw_id_fields = ("inspector",)
|
||||
|
||||
@admin.register(ParkingSpotInspection)
|
||||
class ParkingSpotInspectionAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"id", "asset", "inspector", "inspection_date",
|
||||
"result_status", "verified_standard_stand_count",
|
||||
"verified_cargo_stand_count", "verified_has_scooter_drop_zone",
|
||||
"apply_changes_to_asset"
|
||||
)
|
||||
list_filter = ("result_status", "apply_changes_to_asset", "inspection_date")
|
||||
search_fields = ("notes", "inspector__username", "inspector__first_name", "inspector__last_name")
|
||||
raw_id_fields = ("inspector",)
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-07 07:54
|
||||
|
||||
import assets.models.core
|
||||
import django.contrib.gis.db.models.fields
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0007_outdoorlocation_outdoorasset_outdoorlocationdocument'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AssetInspection',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('asset_object_id', models.PositiveIntegerField(verbose_name="ID de l'asset")),
|
||||
('inspection_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name="Date d'inspection")),
|
||||
('result_status', models.CharField(choices=[('compliant', 'Conforme'), ('non_compliant', 'Non conforme / Anomalie'), ('updated', 'Données corrigées'), ('damaged', 'Endommagé'), ('missing', 'Absent / Retiré')], default='compliant', max_length=50, verbose_name='Résultat du contrôle')),
|
||||
('verified_standard_stand_count', models.IntegerField(blank=True, null=True, verbose_name="Nombre d'arceaux classiques constaté")),
|
||||
('verified_cargo_stand_count', models.IntegerField(blank=True, null=True, verbose_name="Nombre d'arceaux cargo constaté")),
|
||||
('verified_stand_type', models.CharField(blank=True, max_length=50, null=True, verbose_name="Type d'arceau constaté")),
|
||||
('verified_has_scooter_drop_zone', models.BooleanField(blank=True, null=True, verbose_name='Drop zone trottinette constatée')),
|
||||
('verified_marking_quality', models.CharField(blank=True, max_length=50, null=True, verbose_name='Qualité du marquage constatée')),
|
||||
('verified_obsolescence', models.CharField(blank=True, max_length=50, null=True, verbose_name="État d'obsolescence constaté")),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Remarques / Constats')),
|
||||
('apply_changes_to_asset', models.BooleanField(default=True, verbose_name="Appliquer directement les modifications à l'asset")),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('asset_content_type', models.ForeignKey(limit_choices_to={'app_label': 'assets'}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name="Type d'asset")),
|
||||
('inspector', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='asset_inspections', to=settings.AUTH_USER_MODEL, verbose_name='Inspecteur / Vérificateur')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': "Inspection / Vérification d'asset",
|
||||
'verbose_name_plural': "Inspections / Vérifications d'assets",
|
||||
'ordering': ['-inspection_date'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParkingLocation',
|
||||
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)),
|
||||
('lon', models.FloatField(blank=True, null=True)),
|
||||
('lat', models.FloatField(blank=True, null=True)),
|
||||
('geojson', models.TextField(blank=True, null=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('geom', django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise cartographique')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('address', models.CharField(blank=True, max_length=500, null=True, verbose_name='Adresse')),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_location_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Zone de stationnement',
|
||||
'verbose_name_plural': 'Zones de stationnement',
|
||||
'ordering': ['code', 'name_fr'],
|
||||
},
|
||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParkingSpot',
|
||||
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)),
|
||||
('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)),
|
||||
('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')),
|
||||
('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.PolygonField(blank=True, null=True, srid=3812, verbose_name='Emprise polygone')),
|
||||
('spot_type', models.CharField(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')], default='bicycle', max_length=50, verbose_name="Type d'emplacement")),
|
||||
('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(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')], default='normal', max_length=50, verbose_name="Type d'arceau principal")),
|
||||
('has_scooter_drop_zone', models.BooleanField(default=False, verbose_name='Drop zone trottinettes / EDPM')),
|
||||
('scooter_capacity', models.IntegerField(blank=True, null=True, verbose_name='Capacité estimée trottinettes')),
|
||||
('capacity', models.IntegerField(blank=True, null=True, verbose_name='Capacité générale (nbr véhicules)')),
|
||||
('is_paid', models.BooleanField(default=False, verbose_name='Payant')),
|
||||
('time_limit_minutes', models.IntegerField(blank=True, null=True, verbose_name='Durée maximale (minutes)')),
|
||||
('marking_quality', models.CharField(choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('damaged', 'Dégradé'), ('missing', 'Absent / Effacé')], default='good', max_length=50, verbose_name='Qualité du marquage au sol')),
|
||||
('is_temporary', models.BooleanField(default=False, verbose_name='Temporaire (chantier/événement)')),
|
||||
('is_compliant', models.BooleanField(default=True, verbose_name='Conforme aux spécifications')),
|
||||
('non_compliance_reason', models.TextField(blank=True, null=True, verbose_name='Motif de non-conformité')),
|
||||
('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='parking_spots', to='assets.parkinglocation', verbose_name='Zone / Localisation parente')),
|
||||
('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')),
|
||||
('panels', models.ManyToManyField(blank=True, related_name='parking_spots_attached_new', to='assets.signpanel', verbose_name='Panneaux de signalisation associés')),
|
||||
('poles', models.ManyToManyField(blank=True, related_name='parking_spots_attached_new', to='assets.signpole', verbose_name='Poteaux de signalisation associés')),
|
||||
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.parkingspot', verbose_name='Remplacé par')),
|
||||
('street', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parking_spots_new', to='assets.signstreet', verbose_name='Rue')),
|
||||
('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': 'Emplacement de stationnement',
|
||||
'verbose_name_plural': 'Emplacements de stationnement',
|
||||
'ordering': ['code', 'name_fr'],
|
||||
},
|
||||
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
# Generated by Django 6.0.7 on 2026-08-07 08:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0008_assetinspection_parkinglocation_parkingspot'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_has_scooter_drop_zone',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_stand_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_marking_quality',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_cargo_stand_count',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='assetinspection',
|
||||
name='verified_standard_stand_count',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='assetinspection',
|
||||
options={'ordering': ['-inspection_date'], 'verbose_name': "Inspection d'asset (Générique)", 'verbose_name_plural': "Inspections d'assets (Génériques)"},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artworkfountain',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='artworkmonument',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='buildingasset',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cleanglasscontainer',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cleanlitterbin',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='controlcentergeoasset',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itsgeoasset',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturebench',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturegreensurface',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturerwiasset',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='natureshrub',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='naturetree',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='outdoorasset',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='parkingspot',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightingelectricalcabinet',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightinglightfixture',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='publiclightingpole',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadconcreteblockset',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roaddrain',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadfurniture',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadsidewalk',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='roadway',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signpanel',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signparkingspot',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signpole',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='structuregeoasset',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightaccessory',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcable',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcontroller',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightcontrollerhardware',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightdetector',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightelectricalcabinet',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightelectricalcabinetcontent',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightlantern',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightnetworkhardware',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightpole',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightprogramming',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightradar',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightradarpole',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trafficlightvoltagehardware',
|
||||
name='inspection_status',
|
||||
field=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"),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParkingSpotInspection',
|
||||
fields=[
|
||||
('assetinspection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.assetinspection')),
|
||||
('verified_standard_stand_count', models.IntegerField(blank=True, null=True, verbose_name="Nombre d'arceaux classiques constaté")),
|
||||
('verified_cargo_stand_count', models.IntegerField(blank=True, null=True, verbose_name="Nombre d'arceaux cargo constaté")),
|
||||
('verified_stand_type', models.CharField(blank=True, max_length=50, null=True, verbose_name="Type d'arceau constaté")),
|
||||
('verified_has_scooter_drop_zone', models.BooleanField(blank=True, null=True, verbose_name='Drop zone trottinette constatée')),
|
||||
('verified_marking_quality', models.CharField(blank=True, max_length=50, null=True, verbose_name='Qualité du marquage constatée')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': "Inspection d'emplacement de stationnement",
|
||||
'verbose_name_plural': "Inspections d'emplacements de stationnement",
|
||||
},
|
||||
bases=('assets.assetinspection',),
|
||||
),
|
||||
]
|
||||
|
|
@ -11,4 +11,6 @@ from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSi
|
|||
from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract
|
||||
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
|
||||
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
|
||||
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
||||
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
||||
from .parking import ParkingLocation, ParkingSpot
|
||||
from .inspection import AssetInspection, ParkingSpotInspection
|
||||
|
|
@ -249,6 +249,14 @@ ASSET_VALIDATION_STATUS_CHOICES = {
|
|||
'rejected': _('Rejeté'),
|
||||
}
|
||||
|
||||
ASSET_INSPECTION_STATUS_CHOICES = {
|
||||
'to_inspect': _('À inspecter'),
|
||||
'compliant': _('Conforme'),
|
||||
'non_compliant': _('Non conforme'),
|
||||
'updated': _('Mis à jour'),
|
||||
'unknown': _('Non inspecté'),
|
||||
}
|
||||
|
||||
# Abstract base model for assets
|
||||
class AbstractAsset(models.Model):
|
||||
code = models.CharField(max_length=50)
|
||||
|
|
@ -261,6 +269,7 @@ class AbstractAsset(models.Model):
|
|||
serial_number = models.CharField(max_length=200,null=True,blank=True)
|
||||
warranty_duration = models.IntegerField(null=True,blank=True, verbose_name="Duration of the warranty (months)")
|
||||
last_inspection_date = models.DateTimeField(null=True,blank=True)
|
||||
inspection_status = models.CharField(max_length=50, choices=ASSET_INSPECTION_STATUS_CHOICES, default='unknown', null=True, blank=True, verbose_name="Statut d'inspection")
|
||||
last_preventive_date = models.DateTimeField(null=True,blank=True)
|
||||
last_corrective_date = models.DateTimeField(null=True,blank=True)
|
||||
last_survey_date = models.DateTimeField(null=True,blank=True)
|
||||
|
|
@ -274,6 +283,7 @@ class AbstractAsset(models.Model):
|
|||
updated_at = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='%(app_label)s_%(class)s_updated_by', verbose_name="Mis à jour par")
|
||||
|
||||
|
||||
def get_replacement_chain(self):
|
||||
"""Get the complete replacement chain starting from this asset."""
|
||||
chain = [self]
|
||||
|
|
|
|||
172
streetup/assets/models/inspection.py
Normal file
172
streetup/assets/models/inspection.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
INSPECTION_RESULT_CHOICES = (
|
||||
('compliant', _('Conforme')),
|
||||
('non_compliant', _('Non conforme / Anomalie')),
|
||||
('updated', _('Données corrigées')),
|
||||
('damaged', _('Endommagé')),
|
||||
('missing', _('Absent / Retiré')),
|
||||
)
|
||||
|
||||
|
||||
class AssetInspection(models.Model):
|
||||
"""
|
||||
Modèle d'enregistrement générique d'une vérification/inspection sur le terrain d'un asset.
|
||||
Contient uniquement les données communes à tous les assets.
|
||||
"""
|
||||
asset_content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("Type d'asset"),
|
||||
limit_choices_to={'app_label': 'assets'}
|
||||
)
|
||||
asset_object_id = models.PositiveIntegerField(verbose_name=_("ID de l'asset"))
|
||||
asset = GenericForeignKey('asset_content_type', 'asset_object_id')
|
||||
|
||||
inspector = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='asset_inspections',
|
||||
verbose_name=_("Inspecteur / Vérificateur")
|
||||
)
|
||||
inspection_date = models.DateTimeField(default=timezone.now, verbose_name=_("Date d'inspection"))
|
||||
|
||||
result_status = models.CharField(
|
||||
max_length=50,
|
||||
choices=INSPECTION_RESULT_CHOICES,
|
||||
default='compliant',
|
||||
verbose_name=_("Résultat du contrôle")
|
||||
)
|
||||
|
||||
verified_obsolescence = models.CharField(
|
||||
max_length=50,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("État d'obsolescence constaté")
|
||||
)
|
||||
|
||||
notes = models.TextField(null=True, blank=True, verbose_name=_("Remarques / Constats"))
|
||||
apply_changes_to_asset = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Appliquer directement les modifications à l'asset")
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Inspection d'asset (Générique)")
|
||||
verbose_name_plural = _("Inspections d'assets (Génériques)")
|
||||
ordering = ['-inspection_date']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if self.apply_changes_to_asset and self.asset:
|
||||
target_asset = self.asset
|
||||
modified = False
|
||||
|
||||
if hasattr(target_asset, 'last_inspection_date'):
|
||||
target_asset.last_inspection_date = self.inspection_date
|
||||
modified = True
|
||||
|
||||
if hasattr(target_asset, 'inspection_status'):
|
||||
target_asset.inspection_status = self.result_status
|
||||
modified = True
|
||||
|
||||
if hasattr(target_asset, 'validation_status'):
|
||||
target_asset.validation_status = 'validated'
|
||||
modified = True
|
||||
|
||||
if hasattr(target_asset, 'is_compliant'):
|
||||
if self.result_status in ('non_compliant', 'damaged', 'missing'):
|
||||
target_asset.is_compliant = False
|
||||
if hasattr(target_asset, 'non_compliance_reason') and self.notes:
|
||||
target_asset.non_compliance_reason = self.notes
|
||||
else:
|
||||
target_asset.is_compliant = True
|
||||
if hasattr(target_asset, 'non_compliance_reason'):
|
||||
target_asset.non_compliance_reason = None
|
||||
modified = True
|
||||
|
||||
if self.verified_obsolescence and hasattr(target_asset, 'obsolescence'):
|
||||
target_asset.obsolescence = self.verified_obsolescence
|
||||
modified = True
|
||||
|
||||
if modified:
|
||||
target_asset.save()
|
||||
|
||||
def __str__(self):
|
||||
asset_str = str(self.asset) if self.asset else f"Asset #{self.asset_object_id}"
|
||||
date_str = self.inspection_date.strftime('%d/%m/%Y') if self.inspection_date else ''
|
||||
return f"Inspection {asset_str} ({date_str}) - {self.get_result_status_display()}"
|
||||
|
||||
|
||||
class ParkingSpotInspection(AssetInspection):
|
||||
"""
|
||||
Spécialisation de l'inspection pour les emplacements de stationnement (vélos, trottinettes, livraison, PMR).
|
||||
"""
|
||||
verified_standard_stand_count = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Nombre d'arceaux classiques constaté")
|
||||
)
|
||||
verified_cargo_stand_count = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Nombre d'arceaux cargo constaté")
|
||||
)
|
||||
verified_stand_type = models.CharField(
|
||||
max_length=50,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Type d'arceau constaté")
|
||||
)
|
||||
verified_has_scooter_drop_zone = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Drop zone trottinette constatée")
|
||||
)
|
||||
verified_marking_quality = models.CharField(
|
||||
max_length=50,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Qualité du marquage constatée")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Inspection d'emplacement de stationnement")
|
||||
verbose_name_plural = _("Inspections d'emplacements de stationnement")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if self.apply_changes_to_asset and self.asset and self.asset.__class__.__name__ == 'ParkingSpot':
|
||||
target_asset = self.asset
|
||||
modified = False
|
||||
|
||||
if self.verified_standard_stand_count is not None:
|
||||
target_asset.standard_stand_count = self.verified_standard_stand_count
|
||||
modified = True
|
||||
if self.verified_cargo_stand_count is not None:
|
||||
target_asset.cargo_stand_count = self.verified_cargo_stand_count
|
||||
modified = True
|
||||
if self.verified_stand_type is not None:
|
||||
target_asset.stand_type = self.verified_stand_type
|
||||
modified = True
|
||||
if self.verified_has_scooter_drop_zone is not None:
|
||||
target_asset.has_scooter_drop_zone = self.verified_has_scooter_drop_zone
|
||||
modified = True
|
||||
if self.verified_marking_quality is not None:
|
||||
target_asset.marking_quality = self.verified_marking_quality
|
||||
modified = True
|
||||
|
||||
if modified:
|
||||
target_asset.save()
|
||||
143
streetup/assets/models/parking.py
Normal file
143
streetup/assets/models/parking.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
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}"
|
||||
81
streetup/assets/tests_parking_inspections.py
Normal file
81
streetup/assets/tests_parking_inspections.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.gis.geos import Polygon, MultiPolygon
|
||||
from django.utils import timezone
|
||||
from assets.models import ParkingLocation, ParkingSpot, AssetInspection, ParkingSpotInspection, SignPole, SignPanel
|
||||
|
||||
|
||||
class ParkingSpotAndInspectionTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="inspector_user", password="password123")
|
||||
|
||||
poly = Polygon(((648000, 670000), (648010, 670000), (648010, 670010), (648000, 670010), (648000, 670000)), srid=3812)
|
||||
multi_poly = MultiPolygon(poly, srid=3812)
|
||||
|
||||
self.parking_loc = ParkingLocation.objects.create(
|
||||
code="PARK-LOC-01",
|
||||
name_fr="Zone vélo Grand Place",
|
||||
geom=multi_poly
|
||||
)
|
||||
|
||||
self.spot = ParkingSpot.objects.create(
|
||||
code="SPOT-VELO-01",
|
||||
name_fr="Stationnement vélo Grand Place",
|
||||
location=self.parking_loc,
|
||||
spot_type="bicycle",
|
||||
standard_stand_count=3,
|
||||
cargo_stand_count=2,
|
||||
stand_type="mixed",
|
||||
has_scooter_drop_zone=True,
|
||||
scooter_capacity=10,
|
||||
geom=poly
|
||||
)
|
||||
|
||||
def test_parking_spot_creation_and_stand_counts(self):
|
||||
self.assertEqual(self.spot.standard_stand_count, 3)
|
||||
self.assertEqual(self.spot.cargo_stand_count, 2)
|
||||
self.assertEqual(self.spot.total_stand_count, 5)
|
||||
self.assertTrue(self.spot.has_scooter_drop_zone)
|
||||
self.assertTrue(self.spot.is_compliant)
|
||||
self.assertEqual(self.spot.inspection_status, "unknown")
|
||||
self.assertIsNotNone(self.spot.geojson)
|
||||
|
||||
def test_parking_spot_inspection_updates_asset(self):
|
||||
# Using specialized ParkingSpotInspection model
|
||||
inspection = ParkingSpotInspection.objects.create(
|
||||
asset=self.spot,
|
||||
inspector=self.user,
|
||||
inspection_date=timezone.now(),
|
||||
result_status="updated",
|
||||
verified_standard_stand_count=4,
|
||||
verified_cargo_stand_count=3,
|
||||
verified_has_scooter_drop_zone=True,
|
||||
verified_marking_quality="good",
|
||||
apply_changes_to_asset=True,
|
||||
notes="Ajout récent de 1 arceau classique et 1 arceau cargo"
|
||||
)
|
||||
|
||||
self.spot.refresh_from_db()
|
||||
self.assertEqual(self.spot.standard_stand_count, 4)
|
||||
self.assertEqual(self.spot.cargo_stand_count, 3)
|
||||
self.assertEqual(self.spot.total_stand_count, 7)
|
||||
self.assertEqual(self.spot.validation_status, "validated")
|
||||
self.assertEqual(self.spot.inspection_status, "updated")
|
||||
self.assertIsNotNone(self.spot.last_inspection_date)
|
||||
self.assertTrue(self.spot.is_compliant)
|
||||
|
||||
def test_generic_asset_inspection_non_compliance(self):
|
||||
# Generic inspection works for any asset
|
||||
inspection = AssetInspection.objects.create(
|
||||
asset=self.spot,
|
||||
inspector=self.user,
|
||||
inspection_date=timezone.now(),
|
||||
result_status="non_compliant",
|
||||
notes="Arceau cargo tordu suite à un choc de livraison",
|
||||
apply_changes_to_asset=True
|
||||
)
|
||||
|
||||
self.spot.refresh_from_db()
|
||||
self.assertFalse(self.spot.is_compliant)
|
||||
self.assertEqual(self.spot.inspection_status, "non_compliant")
|
||||
self.assertEqual(self.spot.non_compliance_reason, "Arceau cargo tordu suite à un choc de livraison")
|
||||
Loading…
Reference in a new issue