feat: support multi-commune contracts, base asset categories, and filtered municipality access
This commit is contained in:
parent
e68e6ccefe
commit
73738b6757
13 changed files with 1438 additions and 18 deletions
125
loko/assets/management/commands/load_base_asset_categories.py
Normal file
125
loko/assets/management/commands/load_base_asset_categories.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
"""
|
||||||
|
Management command to load baseline asset categories (from loko-bm) and
|
||||||
|
associate existing asset models and assets with their corresponding categories.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from common.models import Thematic
|
||||||
|
from assets.models import (
|
||||||
|
AssetCategory,
|
||||||
|
CleanLitterBin, CleanLitterBinModel,
|
||||||
|
CleanGlassContainer, CleanGlassContainerModel,
|
||||||
|
RoadDrain, RoadDrainModel,
|
||||||
|
Roadway, RoadwayModel,
|
||||||
|
RoadSidewalk, RoadSidewalkModel,
|
||||||
|
RoadFurniture, RoadFurnitureModel,
|
||||||
|
StructureAssetModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Charge les catégories d'assets de base (définies dans loko-bm) et lie les modèles existants."
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--file',
|
||||||
|
type=str,
|
||||||
|
help="Chemin vers le fichier JSON des catégories (défaut: common/fixtures/asset_categories.json)",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# 1. Localiser le fichier JSON
|
||||||
|
json_path = options.get('file')
|
||||||
|
candidates = []
|
||||||
|
if not json_path:
|
||||||
|
base_dir = getattr(settings, 'BASE_DIR', None)
|
||||||
|
candidates = [
|
||||||
|
os.path.join(base_dir, 'common', 'fixtures', 'asset_categories.json') if base_dir else None,
|
||||||
|
os.path.join(base_dir, 'loko', 'common', 'fixtures', 'asset_categories.json') if base_dir else None,
|
||||||
|
'/app/common/fixtures/asset_categories.json',
|
||||||
|
'/app/loko/common/fixtures/asset_categories.json',
|
||||||
|
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'common', 'fixtures', 'asset_categories.json'),
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if c and os.path.exists(c):
|
||||||
|
json_path = os.path.abspath(c)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not json_path or not os.path.exists(json_path):
|
||||||
|
self.stderr.write(self.style.ERROR(f"Fichier asset_categories.json introuvable. Candidats testés: {candidates}"))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stdout.write(f"Chargement des catégories depuis : {json_path}")
|
||||||
|
with open(json_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
created_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
|
||||||
|
# Cache thématiques
|
||||||
|
thematics_by_code = {t.code: t for t in Thematic.objects.all()}
|
||||||
|
|
||||||
|
# 2. Création / Mise à jour des catégories
|
||||||
|
for item in data:
|
||||||
|
code = item['code']
|
||||||
|
name_fr = item['name_fr']
|
||||||
|
name_nl = item.get('name_nl') or name_fr
|
||||||
|
thematic_code = item.get('thematic_code')
|
||||||
|
thematic = thematics_by_code.get(thematic_code)
|
||||||
|
|
||||||
|
cat, created = AssetCategory.objects.update_or_create(
|
||||||
|
code=code,
|
||||||
|
defaults={
|
||||||
|
'name_fr': name_fr,
|
||||||
|
'name_nl': name_nl,
|
||||||
|
'thematic': thematic,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
created_count += 1
|
||||||
|
else:
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
|
# Modèles Django autorisés (ContentType)
|
||||||
|
allowed_models = item.get('allowed_models', [])
|
||||||
|
if allowed_models:
|
||||||
|
cts = ContentType.objects.filter(model__in=allowed_models)
|
||||||
|
cat.allowed_models.set(cts)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f"✅ Catégories d'assets : {created_count} créées, {updated_count} mises à jour (Total: {len(data)})."
|
||||||
|
))
|
||||||
|
|
||||||
|
# 3. Association automatique des modèles d'assets existants aux catégories correspondantes
|
||||||
|
self.link_models_to_categories()
|
||||||
|
|
||||||
|
def link_models_to_categories(self):
|
||||||
|
links = [
|
||||||
|
('CleanLitterBinModel', CleanLitterBinModel, 'CL_BASKET'),
|
||||||
|
('CleanLitterBin', CleanLitterBin, 'CL_BASKET'),
|
||||||
|
('CleanGlassContainerModel', CleanGlassContainerModel, 'CL_BUBBLE'),
|
||||||
|
('CleanGlassContainer', CleanGlassContainer, 'CL_BUBBLE'),
|
||||||
|
('RoadDrainModel', RoadDrainModel, 'RD_DRAIN'),
|
||||||
|
('RoadDrain', RoadDrain, 'RD_DRAIN'),
|
||||||
|
('RoadwayModel', RoadwayModel, 'RD_ROADWAY'),
|
||||||
|
('Roadway', Roadway, 'RD_ROADWAY'),
|
||||||
|
('RoadSidewalkModel', RoadSidewalkModel, 'RD_SIDEWALK'),
|
||||||
|
('RoadSidewalk', RoadSidewalk, 'RD_SIDEWALK'),
|
||||||
|
('RoadFurnitureModel', RoadFurnitureModel, 'RD_STREET_FURNITURE'),
|
||||||
|
('RoadFurniture', RoadFurniture, 'RD_STREET_FURNITURE'),
|
||||||
|
('StructureAssetModel', StructureAssetModel, 'TU_CIVIL_ENGINEERING'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, model_cls, cat_code in links:
|
||||||
|
cat = AssetCategory.objects.filter(code=cat_code).first()
|
||||||
|
if not cat:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
updated = model_cls.objects.filter(category__isnull=True).update(category=cat)
|
||||||
|
if updated:
|
||||||
|
self.stdout.write(f" • {name} : {updated} enregistrement(s) lié(s) à la catégorie {cat.name_fr} ({cat_code}).")
|
||||||
|
except Exception as e:
|
||||||
|
self.stderr.write(f" ⚠️ Erreur liaison {name} -> {cat_code}: {e}")
|
||||||
|
|
@ -796,3 +796,92 @@ def can_edit_exceptional_transport(user):
|
||||||
and getattr(user_config, 'exceptional_transport_can_edit', False)
|
and getattr(user_config, 'exceptional_transport_can_edit', False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_accessible_municipalities(user):
|
||||||
|
"""
|
||||||
|
Retourne le queryset des communes (Municipality) auxquelles l'utilisateur a accès.
|
||||||
|
- Si superuser ou (utilisateur interne sans limitation aux contrats) :
|
||||||
|
retourne toutes les communes (Municipality.objects.all()).
|
||||||
|
- Si utilisateur externe ou avec limit_assets_to_contracts=True :
|
||||||
|
retourne uniquement les communes associées à ses contrats accessibles
|
||||||
|
(via Contract.municipalities ou localisations liées).
|
||||||
|
"""
|
||||||
|
from common.models import Municipality, UserConfig, UserContractAccess
|
||||||
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return Municipality.objects.none()
|
||||||
|
|
||||||
|
if getattr(user, 'is_superuser', False):
|
||||||
|
lang = get_language() or 'fr'
|
||||||
|
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||||
|
return Municipality.objects.all().order_by(order_field)
|
||||||
|
|
||||||
|
user_config = getattr(user, 'config', None)
|
||||||
|
if not user_config:
|
||||||
|
try:
|
||||||
|
user_config = UserConfig.objects.get(user=user)
|
||||||
|
except UserConfig.DoesNotExist:
|
||||||
|
return Municipality.objects.none()
|
||||||
|
|
||||||
|
if user_config.has_role('admin') or user_config.has_role('top_manager'):
|
||||||
|
lang = get_language() or 'fr'
|
||||||
|
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||||
|
return Municipality.objects.all().order_by(order_field)
|
||||||
|
|
||||||
|
is_external = not user_config.is_intern
|
||||||
|
must_limit = is_external or user_config.limit_assets_to_contracts
|
||||||
|
|
||||||
|
if not must_limit:
|
||||||
|
lang = get_language() or 'fr'
|
||||||
|
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||||
|
return Municipality.objects.all().order_by(order_field)
|
||||||
|
|
||||||
|
# Utilisateur restreint par contrats :
|
||||||
|
accessible_contract_ids = set(
|
||||||
|
UserContractAccess.objects.filter(
|
||||||
|
user_config=user_config,
|
||||||
|
can_view_assets=True
|
||||||
|
).values_list('contract_id', flat=True)
|
||||||
|
)
|
||||||
|
if user_config.default_contract_id:
|
||||||
|
accessible_contract_ids.add(user_config.default_contract_id)
|
||||||
|
|
||||||
|
if not accessible_contract_ids:
|
||||||
|
return Municipality.objects.none()
|
||||||
|
|
||||||
|
# Communes associées via ManyToMany Contract.municipalities
|
||||||
|
municipality_ids = set(
|
||||||
|
Municipality.objects.filter(
|
||||||
|
contracts__id__in=accessible_contract_ids
|
||||||
|
).values_list('id', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Communes associées via les localisations liées aux contrats
|
||||||
|
try:
|
||||||
|
from assets.models import CleanLocationContract
|
||||||
|
clean_muni_ids = CleanLocationContract.objects.filter(
|
||||||
|
contract_id__in=accessible_contract_ids,
|
||||||
|
location__municipality__isnull=False
|
||||||
|
).values_list('location__municipality_id', flat=True)
|
||||||
|
municipality_ids.update(clean_muni_ids)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from assets.models import TrafficLightContract
|
||||||
|
tl_muni_ids = TrafficLightContract.objects.filter(
|
||||||
|
contract_id__in=accessible_contract_ids,
|
||||||
|
intersection__municipality__isnull=False
|
||||||
|
).values_list('intersection__municipality_id', flat=True)
|
||||||
|
municipality_ids.update(tl_muni_ids)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not municipality_ids:
|
||||||
|
return Municipality.objects.none()
|
||||||
|
|
||||||
|
lang = get_language() or 'fr'
|
||||||
|
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||||
|
return Municipality.objects.filter(id__in=municipality_ids).order_by(order_field)
|
||||||
|
|
|
||||||
|
|
@ -2313,6 +2313,185 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
||||||
if os.path.exists(tmp_path):
|
if os.path.exists(tmp_path):
|
||||||
os.remove(tmp_path)
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
def test_integrate_cross_plan_update_all(self):
|
||||||
|
import openpyxl
|
||||||
|
import tempfile
|
||||||
|
from django.contrib.gis.geos import Point as DjangoPoint
|
||||||
|
from assets.models import TrafficLightPole, TrafficLightPoleModel, TrafficLightLantern
|
||||||
|
|
||||||
|
pole_model = TrafficLightPoleModel.objects.first()
|
||||||
|
|
||||||
|
# 1. Existing pole B01 in DB (will be updated)
|
||||||
|
existing_pole = TrafficLightPole.objects.create(
|
||||||
|
intersection=self.intersection,
|
||||||
|
code="SWB01_B01",
|
||||||
|
name_fr="Ancien B01",
|
||||||
|
geom=DjangoPoint(149250.0, 170550.0, srid=3812),
|
||||||
|
lon=4.35,
|
||||||
|
lat=50.84,
|
||||||
|
model=pole_model,
|
||||||
|
status="active"
|
||||||
|
)
|
||||||
|
# 2. Existing untouched pole B99 in DB (should remain active after update_all)
|
||||||
|
untouched_pole = TrafficLightPole.objects.create(
|
||||||
|
intersection=self.intersection,
|
||||||
|
code="SWB01_B99",
|
||||||
|
name_fr="Poteau Untouched",
|
||||||
|
model=pole_model,
|
||||||
|
status="active"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Create cross plan workbook containing B01 (with new coordinates) and B02 (new pole)
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "kruisjesplan"
|
||||||
|
ws.cell(row=1, column=1, value="Sleutel:")
|
||||||
|
ws.cell(row=1, column=2, value="SWB01")
|
||||||
|
ws.cell(row=5, column=1, value="Câble 01 SVAVB")
|
||||||
|
ws.cell(row=6, column=3, value="richting")
|
||||||
|
ws.cell(row=6, column=4, value="kringen")
|
||||||
|
ws.cell(row=7, column=5, value="B01")
|
||||||
|
ws.cell(row=7, column=6, value="B02")
|
||||||
|
ws.cell(row=8, column=1, value="com")
|
||||||
|
ws.cell(row=8, column=2, value="1")
|
||||||
|
ws.cell(row=8, column=3, value="A")
|
||||||
|
ws.cell(row=8, column=5, value="X")
|
||||||
|
ws.cell(row=8, column=6, value="X")
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
wb.save(tmp_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
|
||||||
|
with open(tmp_path, 'rb') as f:
|
||||||
|
uploaded_file = SimpleUploadedFile("update_plan.xlsx", f.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
|
||||||
|
url_import = reverse("assets:import_cross_plan", args=[self.intersection.id])
|
||||||
|
response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Call integrate with global_action="update_all"
|
||||||
|
url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id])
|
||||||
|
response_integrate = self.client.post(url_integrate, {
|
||||||
|
"cross_plan_filename": response.context["cross_plan_filename"],
|
||||||
|
"global_action": "update_all",
|
||||||
|
"name_pole_SWB01_B01": "B01 Mis à jour",
|
||||||
|
"x_3812_pole_SWB01_B01": "149260.0",
|
||||||
|
"y_3812_pole_SWB01_B01": "170560.0",
|
||||||
|
"lon_pole_SWB01_B01": "4.351",
|
||||||
|
"lat_pole_SWB01_B01": "50.841",
|
||||||
|
"name_pole_SWB01_B02": "Nouveau B02",
|
||||||
|
"x_3812_pole_SWB01_B02": "149300.0",
|
||||||
|
"y_3812_pole_SWB01_B02": "170600.0",
|
||||||
|
})
|
||||||
|
self.assertEqual(response_integrate.status_code, 302)
|
||||||
|
|
||||||
|
# Verify:
|
||||||
|
# - existing_pole should still have status="active", ID unchanged, name updated, geometry updated
|
||||||
|
existing_pole.refresh_from_db()
|
||||||
|
self.assertEqual(existing_pole.status, "active")
|
||||||
|
self.assertEqual(existing_pole.name_fr, "B01 Mis à jour")
|
||||||
|
self.assertEqual(existing_pole.geom.x, 149260.0)
|
||||||
|
self.assertEqual(existing_pole.geom.y, 170560.0)
|
||||||
|
|
||||||
|
# - untouched_pole should still be active
|
||||||
|
untouched_pole.refresh_from_db()
|
||||||
|
self.assertEqual(untouched_pole.status, "active")
|
||||||
|
|
||||||
|
# - new pole B02 should be created active
|
||||||
|
new_pole = TrafficLightPole.objects.filter(intersection=self.intersection, code="SWB01_B02", status="active").first()
|
||||||
|
self.assertIsNotNone(new_pole)
|
||||||
|
self.assertEqual(new_pole.name_fr, "Nouveau B02")
|
||||||
|
self.assertEqual(new_pole.geom.x, 149300.0)
|
||||||
|
|
||||||
|
# - lantern on B01 and lantern on B02 should be active
|
||||||
|
l1 = TrafficLightLantern.objects.filter(pole=existing_pole, status="active").first()
|
||||||
|
self.assertIsNotNone(l1)
|
||||||
|
l2 = TrafficLightLantern.objects.filter(pole=new_pole, status="active").first()
|
||||||
|
self.assertIsNotNone(l2)
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
def test_import_cross_plan_dxf_geometry_diff(self):
|
||||||
|
import openpyxl
|
||||||
|
import tempfile
|
||||||
|
from django.contrib.gis.geos import Point as DjangoPoint
|
||||||
|
from unittest.mock import patch
|
||||||
|
from assets.models import TrafficLightPole, TrafficLightPoleModel
|
||||||
|
|
||||||
|
pole_model = TrafficLightPoleModel.objects.first()
|
||||||
|
|
||||||
|
# Existing pole B01 with initial coordinates in DB
|
||||||
|
existing_pole = TrafficLightPole.objects.create(
|
||||||
|
intersection=self.intersection,
|
||||||
|
code="SWB01_B01",
|
||||||
|
geom=DjangoPoint(149200.0, 170500.0, srid=3812),
|
||||||
|
lon=4.350,
|
||||||
|
lat=50.840,
|
||||||
|
model=pole_model,
|
||||||
|
status="active"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cross plan workbook with pole B01
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "kruisjesplan"
|
||||||
|
ws.cell(row=1, column=1, value="Sleutel:")
|
||||||
|
ws.cell(row=1, column=2, value="SWB01")
|
||||||
|
ws.cell(row=5, column=1, value="Câble 01 SVAVB")
|
||||||
|
ws.cell(row=6, column=3, value="richting")
|
||||||
|
ws.cell(row=6, column=4, value="kringen")
|
||||||
|
ws.cell(row=7, column=5, value="B01")
|
||||||
|
ws.cell(row=8, column=3, value="A")
|
||||||
|
ws.cell(row=8, column=5, value="X")
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
wb.save(tmp_path)
|
||||||
|
|
||||||
|
# Mock DXF poles returned with a 2.5m shift (x + 2.0, y + 1.5 -> distance = 2.5m)
|
||||||
|
mock_dxf_poles = [{
|
||||||
|
'label': 'B01',
|
||||||
|
'lon': 4.35003,
|
||||||
|
'lat': 50.84002,
|
||||||
|
'x_3812': 149202.0,
|
||||||
|
'y_3812': 170501.5
|
||||||
|
}]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
|
||||||
|
with open(tmp_path, 'rb') as f:
|
||||||
|
uploaded_file = SimpleUploadedFile("dxf_diff_plan.xlsx", f.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
|
||||||
|
with patch('assets.views.trafficlights.resolve_dxf_file_path', return_value='/dummy/path.dxf'), \
|
||||||
|
patch('assets.views.trafficlights.parse_dxf_poles', return_value=mock_dxf_poles):
|
||||||
|
url_import = reverse("assets:import_cross_plan", args=[self.intersection.id])
|
||||||
|
response = self.client.post(url_import, {
|
||||||
|
"cross_plan_file_upload": uploaded_file,
|
||||||
|
"selected_dxf": "test.dxf"
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
preview_poles = response.context["poles"]
|
||||||
|
b01 = next(p for p in preview_poles if p["code"] == "SWB01_B01")
|
||||||
|
self.assertTrue(b01["geom_modified"])
|
||||||
|
self.assertEqual(b01["distance_diff"], 2.5)
|
||||||
|
self.assertEqual(b01["status"], "modified")
|
||||||
|
self.assertEqual(b01["db_x_3812"], 149200.0)
|
||||||
|
self.assertEqual(b01["new_x_3812"], 149202.0)
|
||||||
|
self.assertTrue(any("Position modifiée (DXF)" in d for d in b01["details_diff"]))
|
||||||
|
finally:
|
||||||
|
import os
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
os.remove(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
class AssetDefaultPositionTest(TestCase):
|
class AssetDefaultPositionTest(TestCase):
|
||||||
def test_assign_default_position_hierarchy(self):
|
def test_assign_default_position_hierarchy(self):
|
||||||
|
|
@ -4140,3 +4319,84 @@ class ExportAssetsTest(TestCase):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccessibleMunicipalitiesTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from common.models import Municipality, UserConfig, UserContractAccess, Thematic, UserThematics
|
||||||
|
from contracts.models import Company, Contract
|
||||||
|
self.muni_a = Municipality.objects.create(name_fr="Commune A", name_nl="Gemeente A", fid="1001")
|
||||||
|
self.muni_b = Municipality.objects.create(name_fr="Commune B", name_nl="Gemeente B", fid="1002")
|
||||||
|
self.muni_c = Municipality.objects.create(name_fr="Commune C", name_nl="Gemeente C", fid="1003")
|
||||||
|
|
||||||
|
self.superuser = User.objects.create_superuser(username="admin_super", password="pass123")
|
||||||
|
self.user_intern = User.objects.create_user(username="user_intern", password="pass123")
|
||||||
|
UserConfig.objects.create(user=self.user_intern, is_intern=True, limit_assets_to_contracts=False)
|
||||||
|
|
||||||
|
self.user_ext = User.objects.create_user(username="user_ext", password="pass123")
|
||||||
|
self.ext_config = UserConfig.objects.create(user=self.user_ext, is_intern=False, limit_assets_to_contracts=True)
|
||||||
|
|
||||||
|
self.company = Company.objects.create(name="Test Company")
|
||||||
|
self.contract = Contract.objects.create(contract_number="CTR-001", company=self.company, start_date="2026-01-01", end_date="2026-12-31")
|
||||||
|
self.contract.municipalities.add(self.muni_a, self.muni_b)
|
||||||
|
|
||||||
|
UserContractAccess.objects.create(
|
||||||
|
user_config=self.ext_config,
|
||||||
|
contract=self.contract,
|
||||||
|
can_view_assets=True
|
||||||
|
)
|
||||||
|
|
||||||
|
thematic_roads, _ = Thematic.objects.get_or_create(code="roads", defaults={"name_fr": "Voiries"})
|
||||||
|
UserThematics.objects.create(user_config=self.ext_config, thematic=thematic_roads, can_view_assets=True)
|
||||||
|
|
||||||
|
def test_superuser_sees_all_municipalities(self):
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
munis = list(get_user_accessible_municipalities(self.superuser))
|
||||||
|
self.assertEqual(len(munis), 3)
|
||||||
|
|
||||||
|
def test_intern_unrestricted_sees_all_municipalities(self):
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
munis = list(get_user_accessible_municipalities(self.user_intern))
|
||||||
|
self.assertEqual(len(munis), 3)
|
||||||
|
|
||||||
|
def test_restricted_user_sees_only_contracted_municipalities(self):
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
munis = list(get_user_accessible_municipalities(self.user_ext))
|
||||||
|
muni_ids = {m.id for m in munis}
|
||||||
|
self.assertIn(self.muni_a.id, muni_ids)
|
||||||
|
self.assertIn(self.muni_b.id, muni_ids)
|
||||||
|
self.assertNotIn(self.muni_c.id, muni_ids)
|
||||||
|
|
||||||
|
def test_restricted_user_without_contracts_sees_none(self):
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
user_empty = User.objects.create_user(username="user_empty", password="pass123")
|
||||||
|
UserConfig.objects.create(user=user_empty, is_intern=False, limit_assets_to_contracts=True)
|
||||||
|
munis = list(get_user_accessible_municipalities(user_empty))
|
||||||
|
self.assertEqual(len(munis), 0)
|
||||||
|
|
||||||
|
def test_build_conditional_filter_config_restricted(self):
|
||||||
|
from assets.views.core import build_conditional_filter_config
|
||||||
|
thematic_roads = Thematic.objects.get(code="roads")
|
||||||
|
cfg = build_conditional_filter_config([thematic_roads], user=self.user_ext)
|
||||||
|
roads_filters = cfg.get('roads', [])
|
||||||
|
muni_filter = next((f for f in roads_filters if f['key'] == 'municipality'), None)
|
||||||
|
self.assertIsNotNone(muni_filter)
|
||||||
|
opt_values = {opt['value'] for opt in muni_filter['options']}
|
||||||
|
self.assertIn(str(self.muni_a.id), opt_values)
|
||||||
|
self.assertIn(str(self.muni_b.id), opt_values)
|
||||||
|
self.assertNotIn(str(self.muni_c.id), opt_values)
|
||||||
|
|
||||||
|
def test_roads_streets_geojson_restriction(self):
|
||||||
|
from assets.models.roads import RoadStreet
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
street_a = RoadStreet.objects.create(code="ST-A", name_fr="Rue A", municipality=self.muni_a)
|
||||||
|
street_c = RoadStreet.objects.create(code="ST-C", name_fr="Rue C", municipality=self.muni_c)
|
||||||
|
|
||||||
|
self.client.force_login(self.user_ext)
|
||||||
|
response = self.client.get(reverse('assets:roads_streets_geojson'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
feature_ids = [f['id'] for f in data.get('features', [])]
|
||||||
|
self.assertIn(street_a.id, feature_ids)
|
||||||
|
self.assertNotIn(street_c.id, feature_ids)
|
||||||
|
|
|
||||||
|
|
@ -65,12 +65,8 @@ def artworks_index(request):
|
||||||
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||||
}
|
}
|
||||||
|
|
||||||
municipality_order_field = 'name_nl' if lang == 'nl' else 'name_fr'
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
municipalities = (
|
municipalities = get_user_accessible_municipalities(request.user)
|
||||||
Municipality.objects
|
|
||||||
.all()
|
|
||||||
.order_by(municipality_order_field)
|
|
||||||
)
|
|
||||||
municipality_choices = [(m.id, m.name) for m in municipalities]
|
municipality_choices = [(m.id, m.name) for m in municipalities]
|
||||||
|
|
||||||
# Status choices for filters
|
# Status choices for filters
|
||||||
|
|
|
||||||
|
|
@ -573,8 +573,8 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
# Shared lists used by multiple thematics
|
# Shared lists used by multiple thematics
|
||||||
from common.models import Municipality
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
municipalities = Municipality.objects.all().order_by('name_fr')
|
municipalities = get_user_accessible_municipalities(user)
|
||||||
municipality_options = [
|
municipality_options = [
|
||||||
{'value': str(m.id), 'label': m.name}
|
{'value': str(m.id), 'label': m.name}
|
||||||
for m in municipalities
|
for m in municipalities
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,8 @@ def publiclighting_index(request):
|
||||||
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||||
}
|
}
|
||||||
|
|
||||||
municipalities = list(Municipality.objects.all())
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
municipalities = list(get_user_accessible_municipalities(request.user))
|
||||||
municipalities.sort(key=lambda m: m.name.lower())
|
municipalities.sort(key=lambda m: m.name.lower())
|
||||||
municipality_choices = [(m.id, m.name) for m in municipalities]
|
municipality_choices = [(m.id, m.name) for m in municipalities]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,8 @@ def roads_index(request):
|
||||||
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||||
}
|
}
|
||||||
|
|
||||||
municipalities = list(Municipality.objects.all())
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
municipalities = list(get_user_accessible_municipalities(request.user))
|
||||||
municipalities.sort(key=lambda m: m.name.lower())
|
municipalities.sort(key=lambda m: m.name.lower())
|
||||||
municipality_choices = [(m.id, m.name) for m in municipalities]
|
municipality_choices = [(m.id, m.name) for m in municipalities]
|
||||||
|
|
||||||
|
|
@ -142,11 +143,26 @@ def roads_streets_geojson(request):
|
||||||
municipalities = request.GET.getlist('municipality')
|
municipalities = request.GET.getlist('municipality')
|
||||||
filter_geom = None
|
filter_geom = None
|
||||||
|
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
user = request.user
|
||||||
|
user_config = getattr(user, 'config', None)
|
||||||
|
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
||||||
|
is_external = (user_config is None) or (not user_config.is_intern)
|
||||||
|
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
||||||
|
|
||||||
# --------- Point d'entrée du queryset
|
# --------- Point d'entrée du queryset
|
||||||
qs = RoadStreet.objects.all()
|
qs = RoadStreet.objects.all()
|
||||||
|
|
||||||
# --------- Filtre par communes
|
# --------- Filtre par communes et restriction contractuelle
|
||||||
|
if not is_admin and must_limit:
|
||||||
|
accessible_munis = get_user_accessible_municipalities(user)
|
||||||
|
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
|
||||||
if municipalities:
|
if municipalities:
|
||||||
|
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
|
||||||
|
qs = qs.filter(municipality_id__in=valid_munis)
|
||||||
|
else:
|
||||||
|
qs = qs.filter(municipality_id__in=accessible_muni_ids)
|
||||||
|
elif municipalities:
|
||||||
qs = qs.filter(municipality_id__in=municipalities)
|
qs = qs.filter(municipality_id__in=municipalities)
|
||||||
|
|
||||||
# --------- Filtre géométrique
|
# --------- Filtre géométrique
|
||||||
|
|
@ -350,7 +366,29 @@ def roads_assets_geojson(request):
|
||||||
drains_qs = drains_qs.filter(street__id__in=roads_location_ids)
|
drains_qs = drains_qs.filter(street__id__in=roads_location_ids)
|
||||||
concrete_block_sets_qs = concrete_block_sets_qs.none()
|
concrete_block_sets_qs = concrete_block_sets_qs.none()
|
||||||
|
|
||||||
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
user = request.user
|
||||||
|
user_config = getattr(user, 'config', None)
|
||||||
|
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
||||||
|
is_external = (user_config is None) or (not user_config.is_intern)
|
||||||
|
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
||||||
|
|
||||||
|
if not is_admin and must_limit:
|
||||||
|
accessible_munis = get_user_accessible_municipalities(user)
|
||||||
|
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
|
||||||
if municipalities:
|
if municipalities:
|
||||||
|
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
|
||||||
|
roadways_qs = roadways_qs.filter(street__municipality_id__in=valid_munis)
|
||||||
|
sidewalks_qs = sidewalks_qs.filter(street__municipality_id__in=valid_munis)
|
||||||
|
furnitures_qs = furnitures_qs.filter(street__municipality_id__in=valid_munis)
|
||||||
|
drains_qs = drains_qs.filter(street__municipality_id__in=valid_munis)
|
||||||
|
else:
|
||||||
|
roadways_qs = roadways_qs.filter(street__municipality_id__in=accessible_muni_ids)
|
||||||
|
sidewalks_qs = sidewalks_qs.filter(street__municipality_id__in=accessible_muni_ids)
|
||||||
|
furnitures_qs = furnitures_qs.filter(street__municipality_id__in=accessible_muni_ids)
|
||||||
|
drains_qs = drains_qs.filter(street__municipality_id__in=accessible_muni_ids)
|
||||||
|
concrete_block_sets_qs = concrete_block_sets_qs.none()
|
||||||
|
elif municipalities:
|
||||||
roadways_qs = roadways_qs.filter(street__municipality_id__in=municipalities)
|
roadways_qs = roadways_qs.filter(street__municipality_id__in=municipalities)
|
||||||
sidewalks_qs = sidewalks_qs.filter(street__municipality_id__in=municipalities)
|
sidewalks_qs = sidewalks_qs.filter(street__municipality_id__in=municipalities)
|
||||||
furnitures_qs = furnitures_qs.filter(street__municipality_id__in=municipalities)
|
furnitures_qs = furnitures_qs.filter(street__municipality_id__in=municipalities)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ def sign_index(request):
|
||||||
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||||
}
|
}
|
||||||
|
|
||||||
municipalities = list(Municipality.objects.all())
|
from assets.permissions import get_user_accessible_municipalities
|
||||||
|
municipalities = list(get_user_accessible_municipalities(request.user))
|
||||||
municipalities.sort(key=lambda m: m.name.lower())
|
municipalities.sort(key=lambda m: m.name.lower())
|
||||||
municipality_choices = [(m.id, m.name) for m in municipalities]
|
municipality_choices = [(m.id, m.name) for m in municipalities]
|
||||||
|
|
||||||
|
|
|
||||||
828
loko/common/fixtures/asset_categories.json
Normal file
828
loko/common/fixtures/asset_categories.json
Normal file
|
|
@ -0,0 +1,828 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "TL_ARMOIRE",
|
||||||
|
"name_fr": "Armoire Tricolore",
|
||||||
|
"name_nl": "Armoire Tricolore",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightelectricalcabinet"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_BORNIER",
|
||||||
|
"name_fr": "Bornier Tricolore",
|
||||||
|
"name_nl": "Bornier Tricolore",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightaccessory"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_CABLE",
|
||||||
|
"name_fr": "Cable de Carrefour",
|
||||||
|
"name_nl": "Cable de Carrefour",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightcable"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_CARREFOUR",
|
||||||
|
"name_fr": "Carrefour Tricolore",
|
||||||
|
"name_nl": "Carrefour Tricolore",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_CONE",
|
||||||
|
"name_fr": "Cône Balisage",
|
||||||
|
"name_nl": "Cône Balisage",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_DETEC_OTHER",
|
||||||
|
"name_fr": "Détecteurs autres",
|
||||||
|
"name_nl": "Andere detectoren",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightdetector"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_DETEC_BP",
|
||||||
|
"name_fr": "Détection Bouton Poussoir",
|
||||||
|
"name_nl": "Détection Bouton Poussoir",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightdetector"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_DETEC_RADAR",
|
||||||
|
"name_fr": "Détection Radar",
|
||||||
|
"name_nl": "Détection Radar",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightdetector"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_DIVERS",
|
||||||
|
"name_fr": "Equipement divers",
|
||||||
|
"name_nl": "Equipement divers",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightaccessory"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_LANELIGHT",
|
||||||
|
"name_fr": "Lanelight Balisage",
|
||||||
|
"name_nl": "Lanelight Balisage",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_LANTERNE",
|
||||||
|
"name_fr": "Lanterne Tricolore",
|
||||||
|
"name_nl": "Lanterne Tricolore",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightlantern"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_NETWERK",
|
||||||
|
"name_fr": "Réseau",
|
||||||
|
"name_nl": "Réseau",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_PANNEAU",
|
||||||
|
"name_fr": "Panneau signalisation",
|
||||||
|
"name_nl": "Panneau signalisation",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_POTEAU",
|
||||||
|
"name_fr": "Poteau Tricolore",
|
||||||
|
"name_nl": "Poteau Tricolore",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightpole"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_RADAR_PREVENTIF",
|
||||||
|
"name_fr": "Radar Préventif",
|
||||||
|
"name_nl": "Radar Préventif",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightradar"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_REGELAAR",
|
||||||
|
"name_fr": "Contrôleur de trafic",
|
||||||
|
"name_nl": "Traffic regelaar",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightcontroller"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_SPANNING",
|
||||||
|
"name_fr": "Transformateur électrique",
|
||||||
|
"name_nl": "Transformateur électrique",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "LI_poles",
|
||||||
|
"name_fr": "Poteaux d'éclairage",
|
||||||
|
"name_nl": "Verlichtingsmasten",
|
||||||
|
"thematic_code": "publiclighting",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"publiclightingpole"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_BAL",
|
||||||
|
"name_fr": "Balisage",
|
||||||
|
"name_nl": "Balisage",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_BAR",
|
||||||
|
"name_fr": "Barrière",
|
||||||
|
"name_nl": "Barrière",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_BDI",
|
||||||
|
"name_fr": "Detection et protection incendie",
|
||||||
|
"name_nl": "Detection et protection incendie",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_BTS",
|
||||||
|
"name_fr": "Basse tension",
|
||||||
|
"name_nl": "Basse tension",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_CAB",
|
||||||
|
"name_fr": "Cables",
|
||||||
|
"name_nl": "Cables",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_CAC",
|
||||||
|
"name_fr": "Contrôle d'accès",
|
||||||
|
"name_nl": "Contrôle d'accès",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_CPT",
|
||||||
|
"name_fr": "Comptage vehicules",
|
||||||
|
"name_nl": "Comptage vehicules",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_CTV",
|
||||||
|
"name_fr": "Videosurveillance",
|
||||||
|
"name_nl": "Videosurveillance",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_ECL",
|
||||||
|
"name_fr": "Eclairage",
|
||||||
|
"name_nl": "Eclairage",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_GAB",
|
||||||
|
"name_fr": "Detection hors gabarit",
|
||||||
|
"name_nl": "Detection hors gabarit",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_GTC",
|
||||||
|
"name_fr": "Telegestion",
|
||||||
|
"name_nl": "Telegestion",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_HTS",
|
||||||
|
"name_fr": "Haute tension",
|
||||||
|
"name_nl": "Haute tension",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_LOC",
|
||||||
|
"name_fr": "Locaux techniques",
|
||||||
|
"name_nl": "Locaux techniques",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_MET",
|
||||||
|
"name_fr": "Métalleries",
|
||||||
|
"name_nl": "Métalleries",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_NBK",
|
||||||
|
"name_fr": "Groupes no break",
|
||||||
|
"name_nl": "Groupes no break",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_PMP",
|
||||||
|
"name_fr": "Pompage",
|
||||||
|
"name_nl": "Pompage",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_PMV",
|
||||||
|
"name_fr": "Panneaux à Messages Variables",
|
||||||
|
"name_nl": "Panneaux à Messages Variables",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_RAD",
|
||||||
|
"name_fr": "Retransmission radio",
|
||||||
|
"name_nl": "Retransmission radio",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_SIG",
|
||||||
|
"name_fr": "Signalisation routiere",
|
||||||
|
"name_nl": "Signalisation routiere",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_SON",
|
||||||
|
"name_fr": "Sonorisation",
|
||||||
|
"name_nl": "Sonorisation",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_TEL",
|
||||||
|
"name_fr": "Telephonie",
|
||||||
|
"name_nl": "Telephonie",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_VEN",
|
||||||
|
"name_fr": "Ventilation",
|
||||||
|
"name_nl": "Ventilation",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CC_BUILDING",
|
||||||
|
"name_fr": "Bâtiment",
|
||||||
|
"name_nl": "Gebouw",
|
||||||
|
"thematic_code": "controlcenters",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CC_MAINTENANCE",
|
||||||
|
"name_fr": "Maintenance",
|
||||||
|
"name_nl": "Onderhoud",
|
||||||
|
"thematic_code": "controlcenters",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CC_OFFICE_EQMNT",
|
||||||
|
"name_fr": "Bureautique",
|
||||||
|
"name_nl": "Bureautique",
|
||||||
|
"thematic_code": "controlcenters",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CC_SYSTEM",
|
||||||
|
"name_fr": "Système",
|
||||||
|
"name_nl": "Systeem",
|
||||||
|
"thematic_code": "controlcenters",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CC_TELEPHONY",
|
||||||
|
"name_fr": "Téléphonie",
|
||||||
|
"name_nl": "Telefonie",
|
||||||
|
"thematic_code": "controlcenters",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CC_VIDEO",
|
||||||
|
"name_fr": "Vidéo",
|
||||||
|
"name_nl": "Video",
|
||||||
|
"thematic_code": "controlcenters",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "LI_lightfixtures",
|
||||||
|
"name_fr": "Luminaires",
|
||||||
|
"name_nl": "Verlichtingsarmaturen",
|
||||||
|
"thematic_code": "publiclighting",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"publiclightinglightfixture"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "LI_electrical_cabinets",
|
||||||
|
"name_fr": "Armoires électriques",
|
||||||
|
"name_nl": "Elektrische kasten",
|
||||||
|
"thematic_code": "publiclighting",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"publiclightingelectricalcabinet"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "LI_cables",
|
||||||
|
"name_fr": "Câbles",
|
||||||
|
"name_nl": "Kabels",
|
||||||
|
"thematic_code": "publiclighting",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ITS_CAMERA",
|
||||||
|
"name_fr": "Caméras",
|
||||||
|
"name_nl": "Cameras",
|
||||||
|
"thematic_code": "its",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ITS_COUNTING",
|
||||||
|
"name_fr": "Comptages",
|
||||||
|
"name_nl": "Tellingen",
|
||||||
|
"thematic_code": "its",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ITS_LIDAR",
|
||||||
|
"name_fr": "Lidar",
|
||||||
|
"name_nl": "Lidar",
|
||||||
|
"thematic_code": "its",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ITS_NETWORK",
|
||||||
|
"name_fr": "Réseau",
|
||||||
|
"name_nl": "Netwerk",
|
||||||
|
"thematic_code": "its",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ITS_PMV",
|
||||||
|
"name_fr": "PMV",
|
||||||
|
"name_nl": "PMV",
|
||||||
|
"thematic_code": "its",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ITS_SYSTEM",
|
||||||
|
"name_fr": "Systèmes",
|
||||||
|
"name_nl": "Systemen",
|
||||||
|
"thematic_code": "its",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "NAT_GREEN_AREAS",
|
||||||
|
"name_fr": "Espaces verts",
|
||||||
|
"name_nl": "Groene ruimtes",
|
||||||
|
"thematic_code": "nature",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"naturegreensurface"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "NAT_TREES",
|
||||||
|
"name_fr": "Arbres",
|
||||||
|
"name_nl": "Bomen",
|
||||||
|
"thematic_code": "nature",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"naturetree"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "RD_ROADWAY",
|
||||||
|
"name_fr": "Chaussée",
|
||||||
|
"name_nl": "Rijbaan",
|
||||||
|
"thematic_code": "roads",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "RD_SIDEWALK",
|
||||||
|
"name_fr": "Trottoir",
|
||||||
|
"name_nl": "Voetpad",
|
||||||
|
"thematic_code": "roads",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "RD_CYCLEROAD",
|
||||||
|
"name_fr": "Piste cyclable",
|
||||||
|
"name_nl": "Fietspad",
|
||||||
|
"thematic_code": "roads",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_ROADWAY",
|
||||||
|
"name_fr": "Chaussée en tunnel",
|
||||||
|
"name_nl": "Tunnelweg",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_CIVIL_ENGINEERING",
|
||||||
|
"name_fr": "Génie civil",
|
||||||
|
"name_nl": "Burgerlijke bouwkunde",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TU_HYDRANT",
|
||||||
|
"name_fr": "Hydrant",
|
||||||
|
"name_nl": "Hydrant",
|
||||||
|
"thematic_code": "structures",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"structuregeoasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "LI_CONE",
|
||||||
|
"name_fr": "Cône",
|
||||||
|
"name_nl": "Kegel",
|
||||||
|
"thematic_code": "publiclighting",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ART_FONTAIN",
|
||||||
|
"name_fr": "Fontaines",
|
||||||
|
"name_nl": "Fonteinen",
|
||||||
|
"thematic_code": "artworks",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"artworkfountain"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ART_MONUMENT",
|
||||||
|
"name_fr": "Monument",
|
||||||
|
"name_nl": "Monument",
|
||||||
|
"thematic_code": "artworks",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"artworkmonument"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "RD_DRAIN",
|
||||||
|
"name_fr": "Avaloir",
|
||||||
|
"name_nl": "Rioolput",
|
||||||
|
"thematic_code": "roads",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "RD_MANHOLE",
|
||||||
|
"name_fr": "Taque",
|
||||||
|
"name_nl": "Riooldeksel",
|
||||||
|
"thematic_code": "roads",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "SI_POLE",
|
||||||
|
"name_fr": "Poteau",
|
||||||
|
"name_nl": "Paal",
|
||||||
|
"thematic_code": "sign",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "SI_PANEL",
|
||||||
|
"name_fr": "Panneau",
|
||||||
|
"name_nl": "Paneel",
|
||||||
|
"thematic_code": "sign",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "SI_BARRIER",
|
||||||
|
"name_fr": "Barrière",
|
||||||
|
"name_nl": "Barrière",
|
||||||
|
"thematic_code": "sign",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "SI_MARKING",
|
||||||
|
"name_fr": "Marquage",
|
||||||
|
"name_nl": "Markering",
|
||||||
|
"thematic_code": "sign",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "RD_STREET_FURNITURE",
|
||||||
|
"name_fr": "Mobilier urbain",
|
||||||
|
"name_nl": "Straatmeubilair",
|
||||||
|
"thematic_code": "roads",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_SIBELGA_METER",
|
||||||
|
"name_fr": "Compteur Sibelga",
|
||||||
|
"name_nl": "Sibelga-meter",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "SI_TAXI",
|
||||||
|
"name_fr": "Emplacement Taxi",
|
||||||
|
"name_nl": "Taxistandplaats",
|
||||||
|
"thematic_code": "sign",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_DETEC_VIDEO",
|
||||||
|
"name_fr": "Détection Vidéo",
|
||||||
|
"name_nl": "Videodetectie",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightdetector"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ITS_RADARS",
|
||||||
|
"name_fr": "Radars",
|
||||||
|
"name_nl": "Radars",
|
||||||
|
"thematic_code": "its",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "NAT_FURNITURES",
|
||||||
|
"name_fr": "Mobilier (Bacs à fleurs, ...)",
|
||||||
|
"name_nl": "Meubilair (bloembakken, ...)",
|
||||||
|
"thematic_code": "nature",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"naturebench",
|
||||||
|
"natureshrub"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CL_MATERIALS",
|
||||||
|
"name_fr": "Matériaux",
|
||||||
|
"name_nl": "Materialen",
|
||||||
|
"thematic_code": "clean",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CL_DRAIN",
|
||||||
|
"name_fr": "Avaloir",
|
||||||
|
"name_nl": "Rioolput",
|
||||||
|
"thematic_code": "clean",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CL_OTHER",
|
||||||
|
"name_fr": "Autre",
|
||||||
|
"name_nl": "Andere",
|
||||||
|
"thematic_code": "clean",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CL_ROADWAY",
|
||||||
|
"name_fr": "Chaussée",
|
||||||
|
"name_nl": "Rijbaan",
|
||||||
|
"thematic_code": "clean",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CL_BUBBLE",
|
||||||
|
"name_fr": "Bulle",
|
||||||
|
"name_nl": "Bubbel",
|
||||||
|
"thematic_code": "clean",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"cleanglasscontainer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CL_BASKET",
|
||||||
|
"name_fr": "Corbeille",
|
||||||
|
"name_nl": "Prullenbak",
|
||||||
|
"thematic_code": "clean",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"cleanlitterbin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ITS_POWER_SUPPLY",
|
||||||
|
"name_fr": "Alimentation électrique",
|
||||||
|
"name_nl": "Elektrische voeding",
|
||||||
|
"thematic_code": "its",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CL_SIDEWALK",
|
||||||
|
"name_fr": "Trottoir",
|
||||||
|
"name_nl": "Voetpad",
|
||||||
|
"thematic_code": "clean",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "CL_BIKELANE",
|
||||||
|
"name_fr": "Piste cyclable",
|
||||||
|
"name_nl": "Fietspad",
|
||||||
|
"thematic_code": "clean",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "NAT_RWI",
|
||||||
|
"name_fr": "Gestion intégrée des eaux pluviales (GIEP)",
|
||||||
|
"name_nl": "Geïntegreerd regenwaterbeheer (GRWB)",
|
||||||
|
"thematic_code": "water",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"naturerwiasset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_RADAR_POLE",
|
||||||
|
"name_fr": "Poteau radar",
|
||||||
|
"name_nl": "Radarpaal",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightradarpole"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "parking_spot",
|
||||||
|
"name_fr": "Emplacements de stationnement",
|
||||||
|
"name_nl": "Parkeerplaatsen",
|
||||||
|
"thematic_code": "parking",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bike_parking_spot",
|
||||||
|
"name_fr": "Emplacement vélos",
|
||||||
|
"name_nl": "Fietsenstalling",
|
||||||
|
"thematic_code": "parking",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"parkinglocation",
|
||||||
|
"parkingspot"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "TL_PROGRAMMING",
|
||||||
|
"name_fr": "Programmation",
|
||||||
|
"name_nl": "Programmatie",
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"parent_code": null,
|
||||||
|
"allowed_models": [
|
||||||
|
"trafficlightprogramming"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -126,17 +126,17 @@ class CompanyAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Contract)
|
@admin.register(Contract)
|
||||||
class ContractAdmin(admin.ModelAdmin):
|
class ContractAdmin(admin.ModelAdmin):
|
||||||
list_display = ('contract_number', 'description', 'company', 'is_blocked', 'is_active', 'start_date', 'end_date', 'contact_phone', 'send_email_notifications', 'send_daily_summary', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'thematics_list')
|
list_display = ('contract_number', 'description', 'company', 'municipalities_list', 'is_blocked', 'is_active', 'start_date', 'end_date', 'contact_phone', 'send_email_notifications', 'send_daily_summary', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'thematics_list')
|
||||||
search_fields = ('contract_number', 'company__name', 'description', 'contact_phone')
|
search_fields = ('contract_number', 'company__name', 'description', 'contact_phone')
|
||||||
list_filter = ('is_blocked', 'is_active', 'thematics', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'send_daily_summary')
|
list_filter = ('is_blocked', 'is_active', 'municipalities', 'thematics', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'send_daily_summary')
|
||||||
filter_horizontal = ('thematics',)
|
filter_horizontal = ('thematics', 'municipalities')
|
||||||
|
|
||||||
def get_inlines(self, request, obj=None):
|
def get_inlines(self, request, obj=None):
|
||||||
return [_get_pause_reason_inline(), _get_symptom_autoclose_inline(), _get_operation_template_posts_inline()] + list(super().get_inlines(request, obj))
|
return [_get_pause_reason_inline(), _get_symptom_autoclose_inline(), _get_operation_template_posts_inline()] + list(super().get_inlines(request, obj))
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'fields': ('company', 'contract_number', 'description', 'start_date', 'end_date', 'is_active', 'committed_amount', 'internal_link', 'executive_officer', 'contact_phone', 'signatory_name', 'signatory_role', 'thematics'),
|
'fields': ('company', 'municipalities', 'contract_number', 'description', 'start_date', 'end_date', 'is_active', 'committed_amount', 'internal_link', 'executive_officer', 'contact_phone', 'signatory_name', 'signatory_role', 'thematics'),
|
||||||
}),
|
}),
|
||||||
('Blocage budgétaire / Suspension', {
|
('Blocage budgétaire / Suspension', {
|
||||||
'fields': ('is_blocked', 'blocked_reason', 'blocked_at'),
|
'fields': ('is_blocked', 'blocked_reason', 'blocked_at'),
|
||||||
|
|
@ -164,6 +164,10 @@ class ContractAdmin(admin.ModelAdmin):
|
||||||
}
|
}
|
||||||
js = ('contracts/js/contract_admin.js',)
|
js = ('contracts/js/contract_admin.js',)
|
||||||
|
|
||||||
|
def municipalities_list(self, obj):
|
||||||
|
return ", ".join([m.name for m in obj.municipalities.all()])
|
||||||
|
municipalities_list.short_description = 'Communes'
|
||||||
|
|
||||||
def thematics_list(self, obj):
|
def thematics_list(self, obj):
|
||||||
return ", ".join([thematic.name for thematic in obj.thematics.all()])
|
return ", ".join([thematic.name for thematic in obj.thematics.all()])
|
||||||
thematics_list.short_description = 'Thématiques'
|
thematics_list.short_description = 'Thématiques'
|
||||||
|
|
|
||||||
20
loko/contracts/migrations/0010_contract_municipality.py
Normal file
20
loko/contracts/migrations/0010_contract_municipality.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-09-19 13:49
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('common', '0015_remove_obsolete_schools_appview'),
|
||||||
|
('contracts', '0009_contract_auto_assign_control_team'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contract',
|
||||||
|
name='municipality',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Commune couverte par ce contrat (permet le cadrage cartographique automatique).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contracts', to='common.municipality', verbose_name='Commune associée'),
|
||||||
|
),
|
||||||
|
]
|
||||||
46
loko/contracts/migrations/0011_contract_municipalities.py
Normal file
46
loko/contracts/migrations/0011_contract_municipalities.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Generated manually for multi-commune contracts
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def copy_municipality_to_municipalities(apps, schema_editor):
|
||||||
|
Contract = apps.get_model('contracts', 'Contract')
|
||||||
|
for contract in Contract.objects.all():
|
||||||
|
if getattr(contract, 'municipality_id', None):
|
||||||
|
contract.municipalities.add(contract.municipality_id)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_copy(apps, schema_editor):
|
||||||
|
Contract = apps.get_model('contracts', 'Contract')
|
||||||
|
for contract in Contract.objects.all():
|
||||||
|
first_m = contract.municipalities.first()
|
||||||
|
if first_m:
|
||||||
|
contract.municipality_id = first_m.id
|
||||||
|
contract.save(update_fields=['municipality_id'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('common', '0015_remove_obsolete_schools_appview'),
|
||||||
|
('contracts', '0010_contract_municipality'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contract',
|
||||||
|
name='municipalities',
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
help_text='Communes couvertes par ce contrat (permet le filtrage et le cadrage cartographique).',
|
||||||
|
related_name='contracts',
|
||||||
|
to='common.municipality',
|
||||||
|
verbose_name='Communes associées',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(copy_municipality_to_municipalities, reverse_code=reverse_copy),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='contract',
|
||||||
|
name='municipality',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -372,6 +372,18 @@ class Contract(models.Model):
|
||||||
verbose_name=_("Thématiques"),
|
verbose_name=_("Thématiques"),
|
||||||
help_text=_("Thématiques associées à ce contrat."),
|
help_text=_("Thématiques associées à ce contrat."),
|
||||||
)
|
)
|
||||||
|
municipalities = models.ManyToManyField(
|
||||||
|
'common.Municipality',
|
||||||
|
blank=True,
|
||||||
|
related_name="contracts",
|
||||||
|
verbose_name=_("Communes associées"),
|
||||||
|
help_text=_("Communes couvertes par ce contrat (permet le filtrage et le cadrage cartographique).")
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def municipality(self):
|
||||||
|
"""Retourne la première commune associée (compatibilité ascendante)."""
|
||||||
|
return self.municipalities.first()
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name=_("Actif"),
|
verbose_name=_("Actif"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue