feat: add support for dynamic intervention subtype filtering and validation

This commit is contained in:
kdeterme 2026-09-17 16:27:33 +02:00
parent 396d26414c
commit 368a167b9c
9 changed files with 387 additions and 6 deletions

View file

@ -471,7 +471,7 @@ def get_allowed_update_fields(user, intervention):
# Type d'intervention, Osiris, PMO, Assets # Type d'intervention, Osiris, PMO, Assets
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'inspector']): if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'inspector']):
allowed_fields.update(['type', 'osiris', 'pmo_number']) allowed_fields.update(['type', 'sub_type', 'osiris', 'pmo_number'])
if intervention.status_order < STATUS_ORDERS.get('validated'): if intervention.status_order < STATUS_ORDERS.get('validated'):
allowed_fields.add('category') allowed_fields.add('category')

View file

@ -10,6 +10,8 @@
let isEditMode = false; let isEditMode = false;
let originalValues = {}; let originalValues = {};
let originalContractOptions = null; // Store original contract options for cancel let originalContractOptions = null; // Store original contract options for cancel
let originalSubtypeOptions = null; // Store original subtype options for cancel
let originalSubtypeVisible = null; // Store original subtype visibility for cancel
let isSavingInProgress = false; let isSavingInProgress = false;
let isAutoSavingAndNavigating = false; let isAutoSavingAndNavigating = false;
let isSubmittingAutoSavedForm = false; let isSubmittingAutoSavedForm = false;
@ -29,6 +31,67 @@
// Setup dynamic contract loading when provider changes // Setup dynamic contract loading when provider changes
setupProviderContractLink(); setupProviderContractLink();
// Setup dynamic subtype loading/visibility when type changes
setupTypeSubtypeLink();
/**
* Setup the dynamic link between type and subtype selects.
* When the type changes, filter the subtype options and show/hide the subtype field.
*/
function setupTypeSubtypeLink() {
const typeField = document.querySelector('.editable-field[data-field="type"]');
const subtypeField = document.querySelector('.editable-field[data-field="sub_type"]');
if (!typeField || !subtypeField) return;
const typeSelect = typeField.querySelector('.field-edit select');
const subtypeSelect = subtypeField.querySelector('.field-edit select');
if (!typeSelect || !subtypeSelect) return;
let subtypes = [];
try {
if (subtypeField.dataset.subtypes) {
subtypes = JSON.parse(subtypeField.dataset.subtypes);
} else if (typeof interventionSubtypesData !== 'undefined') {
subtypes = interventionSubtypesData;
}
} catch (e) {
console.error('Error parsing subtypes:', e);
}
function updateSubtype(selectedType, keepValue) {
const available = subtypes.filter(st => !st.types || st.types.length === 0 || st.types.includes(selectedType));
const hasSubtypes = available.length > 0;
if (!hasSubtypes) {
subtypeField.classList.add('d-none');
subtypeSelect.value = '';
} else {
subtypeField.classList.remove('d-none');
const currentVal = subtypeSelect.value;
subtypeSelect.innerHTML = '<option value="">-</option>';
let valueStillValid = false;
available.forEach(st => {
const opt = document.createElement('option');
opt.value = String(st.id);
opt.textContent = st.name;
if (keepValue && String(st.id) === currentVal) {
opt.selected = true;
valueStillValid = true;
}
subtypeSelect.appendChild(opt);
});
if (keepValue && !valueStillValid) {
subtypeSelect.value = '';
}
}
}
typeSelect.addEventListener('change', function() {
updateSubtype(this.value, false);
});
}
/** /**
* Setup the dynamic link between provider and contract selects. * Setup the dynamic link between provider and contract selects.
@ -130,6 +193,16 @@
} }
} }
// Store original subtype options and visibility
const subtypeField = document.querySelector('.editable-field[data-field="sub_type"]');
if (subtypeField) {
const subtypeSelect = subtypeField.querySelector('.field-edit select');
if (subtypeSelect) {
originalSubtypeOptions = subtypeSelect.innerHTML;
}
originalSubtypeVisible = !subtypeField.classList.contains('d-none');
}
// Store original values // Store original values
originalValues = {}; originalValues = {};
editableFields.forEach(field => { editableFields.forEach(field => {
@ -196,6 +269,22 @@
} }
} }
// Restore original subtype options and visibility
if (originalSubtypeOptions !== null) {
const subtypeField = document.querySelector('.editable-field[data-field="sub_type"]');
if (subtypeField) {
const subtypeSelect = subtypeField.querySelector('.field-edit select');
if (subtypeSelect) {
subtypeSelect.innerHTML = originalSubtypeOptions;
}
if (originalSubtypeVisible) {
subtypeField.classList.remove('d-none');
} else {
subtypeField.classList.add('d-none');
}
}
}
// Restore original values // Restore original values
editableFields.forEach(field => { editableFields.forEach(field => {
const canEdit = field.dataset.canEdit === 'true'; const canEdit = field.dataset.canEdit === 'true';
@ -234,6 +323,8 @@
originalValues = {}; originalValues = {};
originalContractOptions = null; originalContractOptions = null;
originalSubtypeOptions = null;
originalSubtypeVisible = null;
} }
async function saveChanges(options = {}) { async function saveChanges(options = {}) {

View file

@ -335,6 +335,11 @@
{{ form.type }} {{ form.type }}
{{ form.type.errors }} {{ form.type.errors }}
</div> </div>
<div class="mb-3 d-none" id="sub_type_field_wrapper">
<label for="id_sub_type" class="form-label">{% translate "Sous-type d'intervention" %}</label>
{{ form.sub_type }}
{{ form.sub_type.errors }}
</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View file

@ -14,6 +14,7 @@
const thematic = '{{ thematic.code }}'; const thematic = '{{ thematic.code }}';
const symptomsData = {{ symptoms_data|safe }}; const symptomsData = {{ symptoms_data|safe }};
const sourceCategoriesData = {{ source_categories_data|safe }}; const sourceCategoriesData = {{ source_categories_data|safe }};
const subtypesData = {{ subtypes_data|default:"[]"|safe }};
const autocomplete_street_url = "{% url 'common:autocomplete_street' %}"; const autocomplete_street_url = "{% url 'common:autocomplete_street' %}";
var autocomplete_locations_url = "{% url 'assets:locations_autocomplete' %}?thematic={{ thematic.code }}{% if inspection_config %}&prioritize={% if inspection_config.default_category %}{{ inspection_config.default_category.code|lower }}{% else %}giep{% endif %}{% endif %}"; var autocomplete_locations_url = "{% url 'assets:locations_autocomplete' %}?thematic={{ thematic.code }}{% if inspection_config %}&prioritize={% if inspection_config.default_category %}{{ inspection_config.default_category.code|lower }}{% else %}giep{% endif %}{% endif %}";
var regional_roads_url = '{% url "common:get_regional_roads_geojson" %}'; var regional_roads_url = '{% url "common:get_regional_roads_geojson" %}';
@ -54,6 +55,50 @@
{% endif %} {% endif %}
</script> </script>
<!-- Dynamic subtype filtering for intervention add form -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const typeSelect = document.getElementById('id_type');
const subTypeSelect = document.getElementById('id_sub_type');
const subTypeWrapper = document.getElementById('sub_type_field_wrapper');
if (!typeSelect || !subTypeSelect || !subTypeWrapper) return;
function updateSubTypeVisibility() {
const selectedType = typeSelect.value;
const available = (typeof subtypesData !== 'undefined' ? subtypesData : []).filter(st =>
!st.types || st.types.length === 0 || st.types.includes(selectedType)
);
if (available.length === 0) {
subTypeWrapper.classList.add('d-none');
subTypeSelect.value = '';
} else {
subTypeWrapper.classList.remove('d-none');
const currentVal = subTypeSelect.value;
subTypeSelect.innerHTML = '<option value="">---------</option>';
let valueStillValid = false;
available.forEach(st => {
const opt = document.createElement('option');
opt.value = String(st.id);
opt.textContent = st.name;
if (String(st.id) === currentVal) {
opt.selected = true;
valueStillValid = true;
}
subTypeSelect.appendChild(opt);
});
if (!valueStillValid && currentVal) {
subTypeSelect.value = '';
}
}
}
typeSelect.addEventListener('change', updateSubTypeVisibility);
updateSubTypeVisibility();
});
</script>
<!-- intervention_add.js (split) --> <!-- intervention_add.js (split) -->
<script src="{% static 'interventions/add/map.js' %}"></script> <script src="{% static 'interventions/add/map.js' %}"></script>
<script src="{% static 'interventions/add/source.js' %}"></script> <script src="{% static 'interventions/add/source.js' %}"></script>

View file

@ -498,6 +498,31 @@
</select> </select>
</div> </div>
</div> </div>
<!-- Sous-type d'intervention -->
<div class="d-flex align-items-center editable-field {% if not has_subtypes_for_current_type %}d-none{% endif %}"
id="intervention-subtype-field"
data-field="sub_type"
data-subtypes="{{ subtypes_data_json|escape }}"
data-can-edit="{% if 'sub_type' in allowed_update_fields %}true{% else %}false{% endif %}">
<button class="btn btn-sm border-0" style="cursor: default;"><i class="bi bi-tag"></i></button>
<span class="label mx-1">{% translate "Sous-type" %}:</span>
<div class="field-display">
<span class="field-value">{{ intervention.sub_type.get_name|default:"-" }}</span>
</div>
<div class="field-edit d-none" style="flex: 1;">
<select class="form-select form-select-sm" id="intervention-subtype-select">
<option value="">-</option>
{% for st in sub_types %}
{% if not st.intervention_types or intervention.type in st.intervention_types %}
<option value="{{ st.id }}" {% if st.id == intervention.sub_type_id %}selected{% endif %}>
{{ st.get_name }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<!-- Catégorie --> <!-- Catégorie -->
<div class="d-flex align-items-center editable-field" data-field="category" data-can-edit="{% if 'category' in allowed_update_fields %}true{% else %}false{% endif %}"> <div class="d-flex align-items-center editable-field" data-field="category" data-can-edit="{% if 'category' in allowed_update_fields %}true{% else %}false{% endif %}">
<button class="btn btn-sm border-0" style="cursor: default;"><i class="bi bi-tags"></i></button> <button class="btn btn-sm border-0" style="cursor: default;"><i class="bi bi-tags"></i></button>

View file

@ -23,6 +23,7 @@
const recommendedNextStatus = '{{ recommended_next_status }}'; const recommendedNextStatus = '{{ recommended_next_status }}';
const interventionId = '{{ intervention.id }}'; const interventionId = '{{ intervention.id }}';
const interventionThematicId = {% if intervention.thematic %}{{ intervention.thematic.id }}{% else %}null{% endif %}; const interventionThematicId = {% if intervention.thematic %}{{ intervention.thematic.id }}{% else %}null{% endif %};
const interventionSubtypesData = {{ subtypes_data_json|default:"[]"|safe }};
const allowed_transitions = {{ allowed_transitions|safe }}; const allowed_transitions = {{ allowed_transitions|safe }};
const canViewPrices = {{ can_view_prices|yesno:"true,false" }}; const canViewPrices = {{ can_view_prices|yesno:"true,false" }};
var contractId = {% if contract_id %} {{ contract_id }} {% else %} null {% endif %}; var contractId = {% if contract_id %} {{ contract_id }} {% else %} null {% endif %};

View file

@ -645,6 +645,165 @@ class InterventionSubTypeTests(TestCase):
display_univ = admin_instance.types_display(self.subtype_universal) display_univ = admin_instance.types_display(self.subtype_universal)
self.assertEqual(str(display_univ), "Tous") self.assertEqual(str(display_univ), "Tous")
def test_update_intervention_field_subtype(self):
from interventions.models import Intervention
from interventions.views.helpers import _update_intervention_field
intervention = Intervention.objects.create(
title='Test SubType Update',
type='repair',
thematic=self.thematic,
status='in_preparation',
)
# 1. Setting compatible subtype succeeds
success, err = _update_intervention_field(intervention, 'sub_type', str(self.subtype_multi.id))
self.assertTrue(success)
self.assertIsNone(err)
self.assertEqual(intervention.sub_type, self.subtype_multi)
# 2. Setting universal subtype succeeds
success, err = _update_intervention_field(intervention, 'sub_type', str(self.subtype_universal.id))
self.assertTrue(success)
self.assertEqual(intervention.sub_type, self.subtype_universal)
# 3. Setting incompatible subtype fails
success, err = _update_intervention_field(intervention, 'sub_type', str(self.subtype_build_only.id))
self.assertFalse(success)
self.assertIn("non compatible", err)
# Old value unchanged
self.assertEqual(intervention.sub_type, self.subtype_universal)
# 4. Setting empty string clears subtype
success, err = _update_intervention_field(intervention, 'sub_type', "")
self.assertTrue(success)
self.assertIsNone(intervention.sub_type)
def test_type_change_resets_incompatible_subtype(self):
from interventions.models import Intervention
from interventions.views.helpers import _update_intervention_field
intervention = Intervention.objects.create(
title='Test SubType Incompatible Reset',
type='repair',
sub_type=self.subtype_multi,
thematic=self.thematic,
status='in_preparation',
)
self.assertEqual(intervention.sub_type, self.subtype_multi)
# Change type to 'build' (incompatible with subtype_multi ['repair', 'fixing'])
success, err = _update_intervention_field(intervention, 'type', 'build')
self.assertTrue(success)
self.assertEqual(intervention.type, 'build')
self.assertIsNone(intervention.sub_type)
def test_permissions_includes_sub_type(self):
from interventions.models import Intervention
from interventions.permissions import get_allowed_update_fields
from common.models import UserConfig, Role
admin_role, _ = Role.objects.get_or_create(name='admin')
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
user_config.roles.add(admin_role)
user_config.is_intern = True
user_config.save()
intervention = Intervention.objects.create(
title='Test Perms',
type='repair',
thematic=self.thematic,
status='in_preparation',
)
allowed_fields = get_allowed_update_fields(self.user, intervention)
self.assertIn('type', allowed_fields)
self.assertIn('sub_type', allowed_fields)
def test_detail_view_subtypes_context(self):
from interventions.models import Intervention
from common.models import UserConfig, Role
import json
admin_role, _ = Role.objects.get_or_create(name='admin')
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
user_config.roles.add(admin_role)
user_config.is_intern = True
user_config.save()
# 1. Intervention with type 'repair' (has matching subtypes: sub_multi, sub_univ)
interv_repair = Intervention.objects.create(
title='Detail View Subtype Test',
type='repair',
thematic=self.thematic,
status='in_preparation',
created_by=self.user,
)
self.client.force_login(self.user)
response = self.client.get(reverse('interventions:intervention_detail', args=[interv_repair.id]))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context['has_subtypes_for_current_type'])
subtypes_in_json = json.loads(response.context['subtypes_data_json'])
self.assertTrue(any(s['id'] == self.subtype_multi.id for s in subtypes_in_json))
# 2. Deactivate universal subtype to test type without subtypes
self.subtype_universal.is_active = False
self.subtype_universal.save()
interv_cleaning = Intervention.objects.create(
title='Detail View No Subtype Test',
type='cleaning',
thematic=self.thematic,
status='in_preparation',
created_by=self.user,
)
response_cleaning = self.client.get(reverse('interventions:intervention_detail', args=[interv_cleaning.id]))
self.assertEqual(response_cleaning.status_code, 200)
self.assertFalse(response_cleaning.context['has_subtypes_for_current_type'])
def test_update_intervention_endpoint_subtype(self):
from interventions.models import Intervention
from common.models import UserConfig, Role
import json
admin_role, _ = Role.objects.get_or_create(name='admin')
user_config, _ = UserConfig.objects.get_or_create(user=self.user)
user_config.roles.add(admin_role)
user_config.is_intern = True
user_config.save()
intervention = Intervention.objects.create(
title='API Update Subtype Test',
type='repair',
thematic=self.thematic,
status='in_preparation',
created_by=self.user,
)
self.client.force_login(self.user)
url = reverse('interventions:update_intervention', args=[intervention.id])
# Batch update sub_type
response = self.client.post(
url,
data=json.dumps({'sub_type': str(self.subtype_multi.id)}),
content_type='application/json',
)
self.assertEqual(response.status_code, 200)
intervention.refresh_from_db()
self.assertEqual(intervention.sub_type, self.subtype_multi)
# Batch update type and sub_type together: switch type to 'build' and sub_type to sub_build
response = self.client.post(
url,
data=json.dumps({'type': 'build', 'sub_type': str(self.subtype_build_only.id)}),
content_type='application/json',
)
self.assertEqual(response.status_code, 200)
intervention.refresh_from_db()
self.assertEqual(intervention.type, 'build')
self.assertEqual(intervention.sub_type, self.subtype_build_only)

View file

@ -54,7 +54,7 @@ from common.private_files.helpers import build_private_url
from common.utils import get_short_name from common.utils import get_short_name
from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation, from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
InterventionLink, InterventionLink, InterventionSubType,
ContractPauseReasonAssignment, ContractPauseReasonAssignment,
Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine, Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine,
InterventionOccupation, InterventionOccupation,
@ -632,6 +632,20 @@ def intervention_detail(request, intervention_id):
sequenced_data = get_sequenced_intervention_documents(intervention, notes_qs=notes) sequenced_data = get_sequenced_intervention_documents(intervention, notes_qs=notes)
sub_types_qs = InterventionSubType.objects.filter(is_active=True, thematic=intervention.thematic).order_by('order_number', 'name_fr')
subtypes_data = [
{
'id': st.id,
'name': st.get_name(),
'types': st.intervention_types,
}
for st in sub_types_qs
]
has_subtypes_for_current_type = any(
not st.intervention_types or intervention.type in st.intervention_types
for st in sub_types_qs
)
context = { context = {
"intervention": intervention, "intervention": intervention,
"status_choices" : [{"key": key, "label": label} for key, label in STATUS_CHOICES.items()], "status_choices" : [{"key": key, "label": label} for key, label in STATUS_CHOICES.items()],
@ -663,6 +677,9 @@ def intervention_detail(request, intervention_id):
'contracts': contracts, 'contracts': contracts,
'maintain_choices': MAINTAIN_CHOICES, 'maintain_choices': MAINTAIN_CHOICES,
'type_choices': get_type_choices(intervention.thematic), 'type_choices': get_type_choices(intervention.thematic),
'sub_types': sub_types_qs,
'subtypes_data_json': _json_mod.dumps(subtypes_data),
'has_subtypes_for_current_type': has_subtypes_for_current_type,
'origin_type_choices': ORIGIN_TYPE_CHOICES, 'origin_type_choices': ORIGIN_TYPE_CHOICES,
'priority_choices': Intervention._meta.get_field('priority').choices, 'priority_choices': Intervention._meta.get_field('priority').choices,
'symptoms': Symptom.objects.filter(is_active=True, thematic=intervention.thematic).order_by('name_fr'), 'symptoms': Symptom.objects.filter(is_active=True, thematic=intervention.thematic).order_by('name_fr'),
@ -1694,6 +1711,16 @@ def interventions_add(request, thematic_code):
for sc in SourceCategory.objects.filter(is_active=True) for sc in SourceCategory.objects.filter(is_active=True)
] ]
subtypes_qs = InterventionSubType.objects.filter(thematic=thematic, is_active=True).order_by('order_number', 'name_fr')
subtypes_data = [
{
"id": st.id,
"name": st.get_name(),
"types": st.intervention_types,
}
for st in subtypes_qs
]
if request.method == 'POST': if request.method == 'POST':
maintain_type = request.POST.get('maintain_type', 'corrective') maintain_type = request.POST.get('maintain_type', 'corrective')
form = InterventionForm(request.POST, request.FILES, thematic=thematic, user=request.user) form = InterventionForm(request.POST, request.FILES, thematic=thematic, user=request.user)
@ -2017,7 +2044,7 @@ def interventions_add(request, thematic_code):
for key in [ for key in [
'title', 'lat', 'lon', 'address', 'asset_category', 'status', 'title', 'lat', 'lon', 'address', 'asset_category', 'status',
'source_category', 'source_ref', 'source_detail', 'init_description', 'source_category', 'source_ref', 'source_detail', 'init_description',
'type', 'origin_type', 'priority' 'type', 'sub_type', 'origin_type', 'priority'
]: ]:
val = request.GET.get(key) val = request.GET.get(key)
if val not in (None, ""): if val not in (None, ""):
@ -2093,6 +2120,7 @@ def interventions_add(request, thematic_code):
'thematic': thematic, 'thematic': thematic,
'symptoms_data': json.dumps(symptoms_data), 'symptoms_data': json.dumps(symptoms_data),
'source_categories_data': json.dumps(source_categories_data), 'source_categories_data': json.dumps(source_categories_data),
'subtypes_data': json.dumps(subtypes_data),
'maintain_type': maintain_type, 'maintain_type': maintain_type,
'observation': observation, 'observation': observation,
'observation_photos': observation_photos, 'observation_photos': observation_photos,
@ -2654,14 +2682,18 @@ def update_intervention(request, intervention_id):
# Handle notes separately (special case) # Handle notes separately (special case)
notes_data = data.pop('notes', None) notes_data = data.pop('notes', None)
# Sort fields to process assigned_provider before contract # Sort fields to process assigned_provider before contract, and type before sub_type
def _field_priority(item): def _field_priority(item):
f_name = item[0] f_name = item[0]
if f_name == 'assigned_provider': if f_name == 'assigned_provider':
return 0 return 0
if f_name == 'contract': if f_name == 'contract':
return 1 return 1
if f_name == 'type':
return 2 return 2
if f_name == 'sub_type':
return 3
return 4
for field, new_value in sorted(data.items(), key=_field_priority): for field, new_value in sorted(data.items(), key=_field_priority):
if field not in allowed_update_fields: if field not in allowed_update_fields:

View file

@ -54,7 +54,7 @@ from common.private_files.helpers import build_private_url
from common.utils import get_short_name from common.utils import get_short_name
from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation, from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation,
InterventionLink, InterventionLink, InterventionSubType,
ContractPauseReasonAssignment, ContractPauseReasonAssignment,
Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine, Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine,
InterventionOccupation, InterventionOccupation,
@ -2604,9 +2604,32 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
allowed_types = dict(get_type_choices(intervention.thematic)).keys() allowed_types = dict(get_type_choices(intervention.thematic)).keys()
if new_value in allowed_types: if new_value in allowed_types:
intervention.type = new_value intervention.type = new_value
if intervention.sub_type and intervention.sub_type.intervention_types:
if new_value not in intervention.sub_type.intervention_types:
intervention.sub_type = None
return True, None return True, None
return False, _('Type not found') return False, _('Type not found')
elif field == "sub_type":
if new_value in ("", None, "null"):
intervention.sub_type = None
return True, None
try:
sub_type_id = int(new_value)
sub_type = InterventionSubType.objects.filter(
id=sub_type_id,
is_active=True,
thematic=intervention.thematic,
).first()
if not sub_type:
return False, _('Sous-type introuvable')
if sub_type.intervention_types and intervention.type not in sub_type.intervention_types:
return False, _("Sous-type non compatible avec le type d'intervention")
intervention.sub_type = sub_type
return True, None
except (ValueError, TypeError):
return False, _('Sous-type invalide')
elif field == "origin_type": elif field == "origin_type":
if new_value in dict(ORIGIN_TYPE_CHOICES).keys(): if new_value in dict(ORIGIN_TYPE_CHOICES).keys():
intervention.origin_type = new_value intervention.origin_type = new_value