loko/streetup/interventions/views/forms.py

735 lines
No EOL
30 KiB
Python

from django import forms
from django.db import models
from django.utils.translation import gettext_lazy as _, get_language
from django.db.models import F, CharField, Case, When, Q, Value, IntegerField
from django.db.models.functions import Coalesce, Lower
import bleach
import json
import re
from interventions.models import Intervention, InterventionNote, InterventionDocument, Symptom, OperationTemplate, InterventionOccupation, SourceCategory, OperationStatusSet, MeasurementDefinition, TYPE_CHOICES
from assets.models import AssetCategory
from common.models import UserConfig, UserThematics
from contracts.models import Contract, ContractOrder, CompanyMember
from projects.models import Project
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
result = [single_file_clean(d, initial) for d in data]
else:
result = [single_file_clean(data, initial)]
return [item for item in result if item]
class InterventionForm(forms.ModelForm):
documents = MultipleFileField(required=False, label=_("Documents"))
project = forms.ModelChoiceField(required=False, label=_("Projet"), queryset = Project.get_active_projects())
def __init__(self, *args, thematic=None, user=None, **kwargs):
super().__init__(*args, **kwargs)
# Filtrer les querysets selon thématique
if thematic:
symptom_qs = Symptom.objects.filter(thematic=thematic, is_active=True)
self.fields['asset_category'].queryset = AssetCategory.objects.filter(thematic=thematic, parent=None)
self.fields['project'].queryset = Project.get_active_projects().filter(thematics=thematic)
else:
symptom_qs = Symptom.objects.all()
self.fields['asset_category'].queryset = AssetCategory.objects.filter(parent=None)
# Limiter les projets aux projets visibles par l'utilisateur
if user is not None:
from projects.permissions import filter_viewable_projects_for_user
allowed_projects = filter_viewable_projects_for_user(user)
self.fields['project'].queryset = self.fields['project'].queryset.filter(pk__in=allowed_projects)
lang = (get_language() or "fr").lower()
# Récupérer les symptômes et les trier en Python
# (le tri sur annotations complexes ne fonctionne pas bien avec Django/PostgreSQL)
symptoms_list = list(symptom_qs)
def get_display_name(s):
if lang.startswith("nl"):
return s.name_nl if s.name_nl else s.name_fr
return s.name_fr
def sort_key(s):
# Trier par: 1) has_order_number (0 si défini, 1 sinon), 2) order_number, 3) nom alphabétique
has_order = 0 if s.order_number is not None else 1
order_num = s.order_number if s.order_number is not None else 999999
name = (get_display_name(s) or "").lower()
return (has_order, order_num, name, s.pk)
symptoms_list.sort(key=sort_key)
# Créer un queryset ordonné par les IDs triés
ordered_ids = [s.pk for s in symptoms_list]
if ordered_ids:
from django.db.models import Case, When
preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(ordered_ids)])
symptom_qs = symptom_qs.filter(pk__in=ordered_ids).order_by(preserved_order)
self.fields['symptom'].queryset = symptom_qs
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
self.fields['project'].widget.attrs.update({'class': 'form-select'})
# Limit status choices to statuses up to 'pris en charge' (assigned)
allowed_statuses = ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned']
self.fields['status'].choices = [
(val, lbl) for val, lbl in self.fields['status'].choices if val in allowed_statuses
]
type_value = self.initial.get('type') or self.data.get('type')
maintain_type_value = self.initial.get('maintain_type') or self.data.get('maintain_type')
if type_value == 'inspection':
if not self.initial.get('status') and not self.data.get('status'):
self.fields['status'].initial = 'to_be_processed'
self.fields['status'].widget = forms.Select(attrs={'class': 'form-select'})
self.fields['status'].widget.choices = self.fields['status'].choices
else:
self.fields['status'].initial = 'in_preparation'
self.fields['status'].widget = forms.HiddenInput()
if maintain_type_value == 'ameliorative':
self.fields['origin_type'].initial = 'demand'
self.fields['origin_type'].widget = forms.HiddenInput()
if maintain_type_value == 'preventive':
self.fields['priority'].required = False
self.fields['origin_type'].required = False
self.fields['priority'].widget = forms.HiddenInput()
self.fields['origin_type'].widget = forms.HiddenInput()
if type_value != 'inspection':
self.fields['status'].required = False
self.fields['status'].widget = forms.HiddenInput()
self.fields['maintain_type'].widget = forms.HiddenInput()
# --- Bonus UX : si un 'symptom' est déjà en initial (GET),
# pré-remplir asset_category / priority / origin_type (sauf ameliorative)
if not self.is_bound:
symptom_pk = self.initial.get('symptom')
if symptom_pk:
s = Symptom.objects.filter(pk=symptom_pk).prefetch_related('asset_categories').only(
'priority', 'origin_type'
).first()
if s:
asset_category_ids = list(s.asset_categories.values_list('id', flat=True))
# Ne pré-remplir que si le symptôme a exactement une catégorie associée
# Si plusieurs catégories, laisser vide pour forcer l'utilisateur à choisir
if len(asset_category_ids) == 1 and not self.fields['asset_category'].initial:
self.fields['asset_category'].initial = asset_category_ids[0]
if s.priority:
self.fields['priority'].initial = s.priority
if (maintain_type_value != 'ameliorative'
and s.origin_type):
self.fields['origin_type'].initial = s.origin_type
self.fields['title'].initial = s.get_name()
# Set default type to 'fixing' for corrective interventions when no type is provided
if maintain_type_value == 'corrective' and not self.initial.get('type'):
self.fields['type'].initial = 'fixing'
# If source category is OBSERVATION, make source_ref read-only (displayed but not editable)
try:
sc_val = self.initial.get('source_category') or (self.data.get('source_category') if self.is_bound else None)
if sc_val:
try:
sc_obj = SourceCategory.objects.get(pk=int(sc_val))
except Exception:
sc_obj = None
if sc_obj:
names = [getattr(sc_obj, a, '') or '' for a in ('name_fr', 'name_nl')]
if any((n.strip().lower() == 'observation') for n in names):
self.fields['source_ref'].widget.attrs['readonly'] = 'readonly'
# subtle visual cue
css = self.fields['source_ref'].widget.attrs.get('class', '')
self.fields['source_ref'].widget.attrs['class'] = (css + ' bg-light').strip()
self.fields['source_ref'].widget.attrs['title'] = _('Référence non modifiable')
except Exception:
pass
class Meta:
model = Intervention
fields = [
'code', 'title', 'status', 'priority', 'init_description',
'maintain_type', 'type', 'origin_type', 'begin_time', 'end_time',
'asset_category', 'symptom',
'source_category', 'source_detail', 'source_ref',
'lat', 'lon', 'geom', 'address',
'reason_for_correction', 'correction_details',
'documents', 'project'
]
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'init_description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'status': forms.Select(attrs={'class': 'form-select'}),
'maintain_type': forms.Select(attrs={'class': 'form-select'}),
'source_category': forms.Select(attrs={'class': 'form-select'}),
'source_detail': forms.TextInput(attrs={'class': 'form-control'}),
'source_ref': forms.TextInput(attrs={'class': 'form-control'}),
'asset_category': forms.Select(attrs={'class': 'form-select'}),
'symptom': forms.Select(attrs={'class': 'form-select'}),
'priority': forms.Select(attrs={'class': 'form-select'}),
'type': forms.Select(attrs={'class': 'form-select'}),
'origin_type': forms.Select(attrs={'class': 'form-select'}),
# 'project': forms.Select(attrs={'class': 'form-select'}),
'begin_time': forms.DateInput(attrs={
'class': 'form-control',
'type': 'datetime-local' # Allows to select a date and time
}),
'end_time': forms.DateInput(attrs={
'class': 'form-control',
'type': 'datetime-local'
}),
'lat': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any', 'placeholder': _('Latitude')}),
'lon': forms.NumberInput(attrs={'class': 'form-control', 'step': 'any', 'placeholder': _('Longitude')}),
'reason_for_correction': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'correction_details': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
def _is_observation_source(self, source_category):
"""Return True when the selected source category corresponds to OBSERVATION (FR/NL)."""
if not source_category:
return False
possible_names = [
getattr(source_category, attr, "") or ""
for attr in ("name_fr", "name_nl")
]
return any(name.strip().lower() == "observation" for name in possible_names if isinstance(name, str))
def clean(self):
cleaned_data = super().clean()
# Force status to 'in_preparation' for non-inspection types
type_value = cleaned_data.get('type')
if type_value != 'inspection':
cleaned_data['status'] = 'in_preparation'
# If a sub-category was selected, override asset_category with it
sub_cat_id = self.data.get('interv_sub_category_id')
if sub_cat_id:
try:
sub_cat = AssetCategory.objects.get(pk=sub_cat_id)
cleaned_data['asset_category'] = sub_cat
# Clear any field-level error on asset_category that may have been raised
self.errors.pop('asset_category', None)
except (AssetCategory.DoesNotExist, ValueError):
pass
maintain_type = cleaned_data.get('maintain_type')
if maintain_type == 'preventive':
if not cleaned_data.get('status'):
cleaned_data['status'] = 'to_be_processed'
if not cleaned_data.get('priority'):
cleaned_data['priority'] = '3'
if not cleaned_data.get('origin_type'):
cleaned_data['origin_type'] = 'demand'
source_category = cleaned_data.get('source_category')
documents = cleaned_data.get('documents') or []
# Ensure consistent list of uploaded files
documents = [doc for doc in documents if doc]
cleaned_data['documents'] = documents
self.fms_filename_mismatch = []
self.fms_filename_reference = None
if maintain_type == 'corrective':
asset_category = cleaned_data.get('asset_category')
if not asset_category:
self.add_error(
'asset_category',
_("Veuillez sélectionner une catégorie d'actif."),
)
symptom = cleaned_data.get('symptom')
if symptom and getattr(symptom, 'require_equipment_selection', False):
equipment_id = (self.data.get('equipment_id') or '').strip()
assets_raw = (self.data.get('assets') or '').strip()
has_equipment = bool(equipment_id)
if not has_equipment and assets_raw and assets_raw != '[]':
try:
parsed_assets = json.loads(assets_raw)
has_equipment = isinstance(parsed_assets, list) and len(parsed_assets) > 0
except (TypeError, ValueError):
has_equipment = True
if not has_equipment:
self.add_error(
None,
_("Un équipement est obligatoire pour ce symptôme."),
)
# OBSERVATION: require a simple source reference (we use the observation id by convention)
if maintain_type == 'corrective' and self._is_observation_source(source_category):
source_ref = (cleaned_data.get('source_ref') or "").strip()
if not source_ref:
self.add_error(
'source_ref',
_("Veuillez indiquer la référence de la source (ex.: l'identifiant de l'observation)."),
)
else:
# Be lenient but prefer a numeric id for consistency
if not re.fullmatch(r"\d+", source_ref):
self.add_error(
'source_ref',
_("La référence doit être un identifiant numérique (ID d'observation)."),
)
# Validation spécifique externe via les hooks
from common.hooks import hooks
cleaned_data = hooks.run_form_cleaners(self, cleaned_data)
return cleaned_data
class InterventionNoteForm(forms.ModelForm):
note_type = forms.ChoiceField(
label=_("Type de note"),
choices=InterventionNote.NoteType.choices,
initial=InterventionNote.NoteType.WORK_DONE,
widget=forms.Select(attrs={'class': 'form-select'}),
)
content = forms.CharField(
label=_("Contenu"),
widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
)
class Meta:
model = InterventionNote
fields = ['note_type', 'content']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
note_type_field = self.fields.get('note_type')
if note_type_field:
default_value = note_type_field.initial or InterventionNote.NoteType.WORK_DONE
css_class = note_type_field.widget.attrs.get('class', '')
classes = f"{css_class} form-select".strip().split()
note_type_field.widget.attrs['class'] = ' '.join(dict.fromkeys(classes))
note_type_field.widget.attrs['data-default-value'] = default_value
def clean_content(self):
content = self.cleaned_data.get('content', '')
# Utilisation de bleach pour nettoyer le contenu
allowed_tags = ['p', 'br', 'strong', 'em', 'ul', 'li', 'ol', 'a']
allowed_attrs = {'a': ['href', 'title', 'target']}
return bleach.clean(content, tags=allowed_tags, attributes=allowed_attrs)
class InterventionDocumentForm(forms.ModelForm):
documents = MultipleFileField(required=True, label=_("Documents"))
class Meta:
model = InterventionDocument
fields = ['documents']
def __init__(self, *args, **kwargs):
super(InterventionDocumentForm, self).__init__(*args, **kwargs)
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
class InterventionOccupationForm(forms.ModelForm):
member = forms.ModelChoiceField(
label=_('Technicien'),
queryset=CompanyMember.objects.none(),
widget=forms.Select(attrs={'class': 'form-select'}),
)
minutes = forms.IntegerField(
label=_('Durée (minutes)'),
min_value=1,
widget=forms.NumberInput(attrs={'class': 'form-control', 'min': 1, 'step': 1}),
)
def __init__(self, *args, members_queryset=None, user=None, **kwargs):
super().__init__(*args, **kwargs)
if members_queryset is None:
members_queryset = CompanyMember.objects.none()
self.fields['member'].queryset = members_queryset
if (not self.is_bound
and not self.instance.pk
and user is not None):
default_member = members_queryset.filter(user=user).first()
if default_member:
self.fields['member'].initial = default_member.pk
class Meta:
model = InterventionOccupation
fields = ['member', 'minutes']
class PreventiveInterventionForm(forms.Form):
title = forms.CharField(
label=_("Titre des interventions"),
required=True,
max_length=200,
widget=forms.TextInput(attrs={
"class": "form-control",
"placeholder": ""
})
)
status = forms.ChoiceField(
label=_("Statut"),
choices=[
('in_preparation', _('En préparation')),
('to_be_approved', _('À approuver')),
('to_be_planned', _('À planifier')),
],
widget=forms.Select(attrs={"class": "form-select"})
)
type = forms.ChoiceField(
label=_("Type d'intervention"),
choices=TYPE_CHOICES,
initial='care',
widget=forms.Select(attrs={"class": "form-select"})
)
contract = forms.ModelChoiceField(
label=_("Contrat"),
queryset=Contract.objects.none(),
widget=forms.Select(attrs={"class": "form-select"})
)
order = forms.ModelChoiceField(
queryset=ContractOrder.objects.none(),
label=_("Commande"),
required=False,
widget=forms.Select(attrs={"class": "form-select", "id": "id_order", "disabled": "disabled"})
)
operation_templates = forms.ModelMultipleChoiceField(
label=_("Templates d'opérations"),
queryset=OperationTemplate.objects.none(),
widget=forms.CheckboxSelectMultiple(),
required=False,
help_text=_("Les opérations génériques seront ajoutées une fois par intervention. Les opérations liées aux équipements seront répétées pour chaque asset sélectionné.")
)
expected_begin_time = forms.DateTimeField(
label=_("Début prévisionnel"),
widget=forms.DateTimeInput(attrs={
"type": "datetime-local",
"class": "form-control"
})
)
expected_end_time = forms.DateTimeField(
label=_("Fin prévisionnelle"),
widget=forms.DateTimeInput(attrs={
"type": "datetime-local",
"class": "form-control"
})
)
recurrence_count = forms.IntegerField(
label=_("Nombre de répétitions"),
min_value=0,
initial=0,
widget=forms.NumberInput(attrs={
"class": "form-control"
})
)
recurrence_delta_months = forms.IntegerField(
label=_("Délai entre les répétitions (mois)"),
min_value=1,
required=False,
widget=forms.NumberInput(attrs={
"class": "form-control"
})
)
create_order_per_recurrence = forms.BooleanField(
label=_("Créer une commande par répétition"),
required=False,
initial=False,
help_text=_("Si coché, une nouvelle commande sera créée pour chaque répétition. La commande sélectionnée sera renommée avec le suffixe '-1', et des commandes '-2', '-3', etc. seront créées automatiquement pour chaque répétition suivante."),
widget=forms.CheckboxInput(attrs={
"class": "form-check-input"
})
)
type_of_selected_elements = forms.ChoiceField(
label=_("Type d'éléments à sélectionner"),
choices=[
('locations', 'Localisations'),
('assets', 'Assets')
],
initial='locations',
widget=forms.RadioSelect(
attrs={
'class': 'form-check-input'
}
)
)
intervention_creation_mode = forms.ChoiceField(
label=_("Mode de création des interventions"),
choices=[
('one_per_element', _("Créer une intervention par élément (localisation ou asset)")),
('single_for_all', _("Créer une seule intervention pour tous les éléments"))
],
initial='one_per_element',
widget=forms.RadioSelect(
attrs={
'class': 'form-check-input'
}
),
help_text=_("Choisissez comment créer les interventions en fonction des éléments sélectionnés.")
)
def clean(self):
cleaned_data = super().clean()
count = cleaned_data.get("recurrence_count")
delta = cleaned_data.get("recurrence_delta_months")
if count and count > 0 and not delta:
self.add_error("recurrence_delta_months", _("Ce champ est requis si vous avez indiqué une répétition."))
def __init__(self, *args, user=None, thematic=None, **kwargs):
super().__init__(*args, **kwargs)
type_value = self.initial.get('type') or (self.data.get('type') if self.data else None) or 'care'
if type_value == 'inspection':
self.fields['status'].choices = [
('in_preparation', _('En préparation')),
('to_be_approved', _('À approuver')),
('to_be_planned', _('À planifier')),
('to_be_processed', _('À traiter')),
('assigned', _('Pris en charge')),
]
if user is not None:
try:
user_config = UserConfig.objects.get(user=user)
# Récupérer les thématiques pour lesquelles l'utilisateur peut éditer des interventions
user_thematics_ids = UserThematics.objects.filter(
user_config=user_config,
can_edit_interventions=True
).values_list('thematic_id', flat=True)
# Filtrer les contrats :
# 1. Contrats accessibles à l'utilisateur
# 2. Contrats associés à au moins une thématique que l'utilisateur peut éditer
# OU contrats sans thématiques (pour la rétrocompatibilité)
base_contracts = user_config.contracts.filter(is_active=True)
# Si une thématique spécifique est fournie, filtrer aussi par cette thématique
if thematic:
# Contrats liés à cette thématique spécifique
contracts_qs = base_contracts.filter(
Q(thematics=thematic)
).distinct()
else:
# Contrats liés aux thématiques de l'utilisateur
contracts_qs = base_contracts.filter(
Q(thematics__id__in=user_thematics_ids)
).distinct()
self.fields['contract'].queryset = contracts_qs
except UserConfig.DoesNotExist:
self.fields['contract'].queryset = Contract.objects.none()
# Récupérer contract_id proprement (initial ou data)
contract_id = None
# 1) Si on a un initial pour contract, il peut être un objet ou un id
initial_contract = self.initial.get("contract")
if initial_contract:
if hasattr(initial_contract, "id"):
contract_id = initial_contract.id
else:
try:
contract_id = int(initial_contract)
except (ValueError, TypeError):
contract_id = None
# 2) Si POST, la data peut définir/écraser le contrat
raw_contract = self.data.get("contract")
if raw_contract:
try:
contract_id = int(raw_contract)
except (ValueError, TypeError):
pass
if contract_id:
self.fields["order"].queryset = ContractOrder.objects.filter(contract_id=contract_id)
# On enlève le disabled si présent, mais on ne touche pas à la valeur
self.fields["order"].widget.attrs.pop("disabled", None)
else:
# Pas de contrat connu → pas de commandes proposées
self.fields["order"].queryset = ContractOrder.objects.none()
# Tri alphabétique des templates d'opération (OperationTemplate n'a qu'un champ 'name')
if thematic:
templates_qs = OperationTemplate.objects.filter(thematic=thematic).order_by(Lower('name'))
else:
templates_qs = OperationTemplate.objects.all().order_by(Lower('name'))
self.fields['operation_templates'].queryset = templates_qs
class AssetsLinkForm(forms.Form):
assets = forms.JSONField(required=False)
def clean_assets(self):
data = self.cleaned_data.get("assets") or []
if not isinstance(data, list):
raise forms.ValidationError(_("assets doit être une liste."))
norm = []
for i, a in enumerate(data):
if not isinstance(a, dict):
raise forms.ValidationError(
_("Item #%(index)s doit être un objet JSON."),
code="invalid_json",
params={"index": i+1}
)
try:
model = str(a["model"]).lower().strip()
object_id = int(a["object_id"])
norm.append({"model": model, "object_id": object_id})
except Exception:
raise forms.ValidationError(
_("Item #%(index)s invalide"),
params={"index": i+1}
)
return norm
class LinkExistingInterventionsForm(forms.Form):
codes = forms.CharField(
label=_("Codes d'intervention"),
required=True,
help_text=_("Séparez les codes par des espaces, des virgules ou des retours à la ligne."),
widget=forms.Textarea(attrs={"rows": 1}),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["codes"].widget.attrs.update({
"class": "form-control form-control-sm",
"placeholder": _("Ex.: I000123 I000456"),
})
def clean_codes(self):
raw_value = self.cleaned_data.get("codes", "")
tokens = re.split(r"[\s,;]+", raw_value)
normalized = []
for token in tokens:
token = token.strip().upper()
if not token:
continue
if token not in normalized:
normalized.append(token)
if not normalized:
raise forms.ValidationError(_("Veuillez indiquer au moins un code d'intervention."))
return normalized
class CustomOperationForm(forms.Form):
"""Form for adding custom operations to an intervention"""
OPERATION_TYPE_CHOICES = [
('measurement', _('Mesure')),
('status', _('Choix de statut')),
]
operation_type = forms.ChoiceField(
choices=OPERATION_TYPE_CHOICES,
label=_("Type d'opération"),
widget=forms.RadioSelect,
initial='status'
)
name_fr = forms.CharField(
label=_("Nom (FR)"),
max_length=255,
required=False, # Not individually required, but at least one name is required
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Nom de l\'opération en français')})
)
name_nl = forms.CharField(
label=_("Nom (NL)"),
max_length=255,
required=False, # Not individually required, but at least one name is required
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Naam van de operatie in het Nederlands')})
)
# For status-based operations
status_set = forms.ModelChoiceField(
queryset=OperationStatusSet.objects.all(),
label=_("Ensemble de statuts"),
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
# For measurement operations
measurement_definition = forms.ModelChoiceField(
queryset=MeasurementDefinition.objects.all(),
label=_("Définition de mesure"),
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
template_group = forms.CharField(
label=_("Groupe (optionnel)"),
max_length=255,
required=False,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Ex: Opérations personnalisées')})
)
def clean(self):
cleaned_data = super().clean()
operation_type = cleaned_data.get('operation_type')
# Check that at least one name (FR or NL) is provided
name_fr = cleaned_data.get('name_fr', '').strip()
name_nl = cleaned_data.get('name_nl', '').strip()
if not name_fr and not name_nl:
raise forms.ValidationError(_("Vous devez fournir au moins un nom (FR ou NL)."))
# If only one name is provided, copy it to the other
if name_fr and not name_nl:
cleaned_data['name_nl'] = name_fr
elif name_nl and not name_fr:
cleaned_data['name_fr'] = name_nl
if operation_type == 'status':
if not cleaned_data.get('status_set'):
raise forms.ValidationError(_("Vous devez sélectionner un ensemble de statuts pour une opération de type 'Choix'."))
elif operation_type == 'measurement':
if not cleaned_data.get('measurement_definition'):
raise forms.ValidationError(_("Vous devez sélectionner une définition de mesure pour une opération de type 'Mesure'."))
return cleaned_data