Changement de titre de "natures" de types de rémunérations, permettre de définir...
[auf_rh_dae.git] / project / dae / forms.py
1 # -*- encoding: utf-8 -*-
2
3 import datetime
4 from ordereddict import OrderedDict
5 from django import forms
6 from django.core.urlresolvers import reverse
7 from django.forms.models import BaseInlineFormSet
8 from django.forms.models import (
9 inlineformset_factory,
10 modelformset_factory,
11 _get_foreign_key,
12 )
13 from django.db.models import Q, Max, Count
14 from django.shortcuts import redirect
15 from django.contrib.admin import widgets as admin_widgets
16
17 from ajax_select.fields import AutoCompleteSelectField
18
19 from auf.django.references import models as ref
20 from auf.django.workflow.forms import WorkflowFormMixin
21 from auf.django.workflow.models import WorkflowCommentaire
22
23 from project import groups
24 from project.rh import models as rh
25 from project.dae import models as dae
26 from .widgets import ReadOnlyChoiceWidget
27 from project.dae.workflow import POSTE_ETATS_BOUTONS, POSTE_ETAT_FINALISE
28
29
30 class BaseInlineFormSetWithInitial(BaseInlineFormSet):
31 """
32 Cette classe permet de fournir l'option initial aux inlineformsets.
33 Elle devient désuette en django 1.4.
34 """
35 def __init__(self, data=None, files=None, instance=None,
36 save_as_new=False, prefix=None, queryset=None, **kwargs):
37
38 self.initial_extra = kwargs.pop('initial', None)
39
40 from django.db.models.fields.related import RelatedObject
41 if instance is None:
42 self.instance = self.fk.rel.to()
43 else:
44 self.instance = instance
45 self.save_as_new = save_as_new
46 # is there a better way to get the object descriptor?
47 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
48 if queryset is None:
49 queryset = self.model._default_manager
50 qs = queryset.filter(**{self.fk.name: self.instance})
51 super(BaseInlineFormSetWithInitial, self).__init__(data, files, prefix=prefix,
52 queryset=qs, **kwargs)
53
54 def _construct_form(self, i, **kwargs):
55 if self.is_bound and i < self.initial_form_count():
56 # Import goes here instead of module-level because importing
57 # django.db has side effects.
58 from django.db import connections
59 pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name)
60 pk = self.data[pk_key]
61 pk_field = self.model._meta.pk
62 pk = pk_field.get_db_prep_lookup('exact', pk,
63 connection=connections[self.get_queryset().db])
64 if isinstance(pk, list):
65 pk = pk[0]
66 kwargs['instance'] = self._existing_object(pk)
67 if i < self.initial_form_count() and not kwargs.get('instance'):
68 kwargs['instance'] = self.get_queryset()[i]
69 if i >= self.initial_form_count() and self.initial_extra:
70 # Set initial values for extra forms
71 try:
72 kwargs['initial'] = self.initial_extra[i-self.initial_form_count()]
73 except IndexError:
74 pass
75
76 defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
77 if self.is_bound:
78 defaults['data'] = self.data
79 defaults['files'] = self.files
80 if self.initial:
81 try:
82 defaults['initial'] = self.initial[i]
83 except IndexError:
84 pass
85 # Allow extra forms to be empty.
86 if i >= self.initial_form_count():
87 defaults['empty_permitted'] = True
88 defaults.update(kwargs)
89 form = self.form(**defaults)
90 self.add_fields(form, i)
91 return form
92
93
94 def _implantation_choices(obj, request):
95 # TRAITEMENT NORMAL
96 employe = groups.get_employe_from_user(request.user)
97 q = Q(**{'zone_administrative': employe.implantation.zone_administrative})
98
99 # TRAITEMENT DRH
100 user_groupes = [g.name for g in request.user.groups.all()]
101 if groups.DRH_NIVEAU_1 in user_groupes or \
102 groups.DRH_NIVEAU_2 in user_groupes:
103 q = Q()
104 return [('', '----------')] + \
105 [(i.id, unicode(i), )for i in ref.Implantation.objects.filter(q)]
106
107
108 def _employe_choices(obj, request):
109 # TRAITEMENT NORMAL
110 employe = groups.get_employe_from_user(request.user)
111 q_dae_region_service = Q(
112 poste__implantation__zone_administrative=(
113 employe.implantation.zone_administrative
114 )
115 )
116 q_rh_region_service = Q(
117 poste__implantation__zone_administrative=(
118 employe.implantation.zone_administrative
119 )
120 )
121 # TRAITEMENT DRH
122 user_groupes = [g.name for g in request.user.groups.all()]
123 if groups.DRH_NIVEAU_1 in user_groupes or \
124 groups.DRH_NIVEAU_2 in user_groupes:
125 q_dae_region_service = Q()
126 q_rh_region_service = Q()
127
128 # On filtre les employes avec les droits régionaux et on s'assure que
129 # c'est bien le dernier dossier en date pour sortir l'employe. On retient
130 # un employé qui travaille présentement dans la même région que le user
131 # connecté.
132 dossiers_regionaux_ids = [
133 d.id for d in dae.Dossier.objects.filter(q_dae_region_service)
134 ]
135 employes_ids = [
136 d['employe']
137 for d in dae.Dossier.objects
138 .values('employe')
139 .annotate(dernier_dossier=Max('id'))
140 if d['dernier_dossier'] in dossiers_regionaux_ids
141 ]
142 dae_employe = dae.Employe.objects.filter(id__in=employes_ids)
143 dae_ = dae_employe.filter(id_rh__isnull=True)
144 copies = dae_employe.filter(Q(id_rh__isnull=False))
145 id_copies = [p.id_rh_id for p in copies.all()]
146
147 dossiers_regionaux_ids = [
148 d.id for d in rh.Dossier.objects.filter(q_rh_region_service)
149 ]
150 employes_ids = [
151 d['employe']
152 for d in rh.Dossier.objects
153 .values('employe')
154 .annotate(dernier_dossier=Max('id'))
155 if d['dernier_dossier'] in dossiers_regionaux_ids
156 ]
157 rhv1 = rh.Employe.objects \
158 .filter(id__in=employes_ids) \
159 .exclude(id__in=id_copies)
160
161 # On ajoute les nouveaux Employés DAE qui ont été crées, mais qui n'ont
162 # pas de Dossier associés
163 employes_avec_dae = [d.employe_id for d in dae.Dossier.objects.all()]
164 employes_orphelins = dae.Employe.objects.exclude(id__in=employes_avec_dae)
165
166 def option_label(employe, extra=""):
167 if extra:
168 extra = " [%s]" % extra
169 return "%s %s %s" % (employe.nom.upper(), employe.prenom.title(), extra)
170
171 lbl_rh = sorted([('rh-%s' % p.id, option_label(p, "existant dans rh")) for p in rhv1],
172 key=lambda t: t[1])
173 lbl_dae = sorted([('dae-%s' % p.id, option_label(p)) for p in dae_ | copies | employes_orphelins],
174 key=lambda t: t[1])
175 return [('', 'Nouvel employé')] + lbl_rh + lbl_dae
176
177
178 def label_poste_display(poste):
179 """Formate un visuel pour un poste dans une liste déroulante"""
180 annee = ""
181 if poste.date_debut:
182 annee = poste.date_debut.year
183
184 nom = poste.nom
185 label = u"%s (%s) %s [%s]" % (
186 annee,
187 poste.implantation.nom_court,
188 nom,
189 #poste.type_poste.categorie_emploi.nom,
190 poste.id,
191 )
192 return label
193
194
195 PostePieceFormSet = inlineformset_factory(dae.Poste, dae.PostePiece,)
196 DossierPieceForm = inlineformset_factory(dae.Dossier, dae.DossierPiece)
197
198 # Ce formset est utilisé dans le cas de la création de poste prépopulé avec les
199 # données de RH
200 FinancementFormSetInitial = inlineformset_factory(
201 dae.Poste,
202 dae.PosteFinancement,
203 formset=BaseInlineFormSetWithInitial,
204 extra=2
205 )
206 FinancementFormSet = inlineformset_factory(
207 dae.Poste,
208 dae.PosteFinancement,
209 extra=2
210 )
211
212
213 class DossierComparaisonForm(forms.ModelForm):
214
215 recherche = AutoCompleteSelectField('dossiers', required=False)
216 poste = forms.CharField(
217 max_length=255, widget=forms.TextInput(attrs={'size': '60'})
218 )
219
220 class Meta:
221 model = dae.DossierComparaison
222 exclude = ('dossier',)
223
224 DossierComparaisonFormSet = modelformset_factory(
225 dae.DossierComparaison, extra=3, max_num=3, form=DossierComparaisonForm
226 )
227
228
229 class PosteComparaisonForm(forms.ModelForm):
230
231 recherche = AutoCompleteSelectField('dae_postes', required=False)
232
233 class Meta:
234 model = dae.PosteComparaison
235 exclude = ('poste',)
236
237 # Ce formset est utilisé dans le cas de la création de poste prépopulé avec les
238 # données de RH
239 PosteComparaisonFormSetInitial = inlineformset_factory(
240 dae.Poste,
241 dae.PosteComparaison,
242 extra=3,
243 max_num=3,
244 form=PosteComparaisonForm,
245 formset=BaseInlineFormSetWithInitial,
246 )
247 PosteComparaisonFormSet = inlineformset_factory(
248 dae.Poste,
249 dae.PosteComparaison,
250 extra=3,
251 max_num=3,
252 form=PosteComparaisonForm,
253 )
254
255
256 class FlexibleRemunForm(forms.ModelForm):
257
258 montant_mensuel = forms.DecimalField(required=False)
259 montant = forms.DecimalField(required=True, label='Montant annuel')
260
261 class Meta:
262 model = dae.Remuneration
263
264 def __init__(self, *a, **kw):
265 super(FlexibleRemunForm, self).__init__(*a, **kw)
266 self.fields['type'].widget = ReadOnlyChoiceWidget(choices=self.fields['type'].choices)
267
268 def clean_devise(self):
269 devise = self.cleaned_data['devise']
270 if devise.code == 'EUR':
271 return devise
272 implantation = ref.Implantation.objects.get(
273 id=self.data['implantation']
274 )
275 liste_taux = devise.tauxchange_set.order_by('-annee')
276 if len(liste_taux) == 0:
277 raise forms.ValidationError(
278 u"La devise %s n'a pas de taux pour l'implantation %s" %
279 (devise, implantation)
280 )
281 else:
282 return devise
283
284 def has_changed(self):
285 """
286 Modification de has_changed pour qu'il ignore les montant a 0
287 et les 'types'.
288 """
289
290 changed_data = self.changed_data
291
292 # Type is set in hidden fields, it shouldn't be changed by the
293 # user; ignore when checking if data has changed.
294 if 'type' in changed_data:
295 changed_data.pop(changed_data.index('type'))
296
297 # Montant is set to 0 in javascript, ifnore 'montant' data if
298 # its value is 0.
299
300 # Generer le key tel qu'identifié dans self.data:
301 montant_key = '-'.join((self.prefix, 'montant'))
302
303 if ('montant' in changed_data and
304 self.data.get(montant_key, '0') == '0'):
305 changed_data.pop(changed_data.index('montant'))
306
307 return bool(changed_data)
308
309
310 class GroupedInlineFormset(BaseInlineFormSet):
311
312 def set_groups(self, group_accessor, group_order=[]):
313 """
314 group_accessor: A function that will get the key and name from
315 each form.
316 group_order: list the group keys here in a list and
317 GroupedInlineFormset.groups will be ordered (ordereddict) by
318 the key sequence provided here. Any missing key from the
319 sequence will
320 """
321
322 # Build group list.
323 self.groups = OrderedDict()
324 temp_groups = {}
325 # self.groups_and_forms = []
326 for form in self.forms:
327 group_key, group_name = group_accessor(form)
328 if not temp_groups.has_key(group_key):
329 temp_groups[group_key] = {
330 'name': group_name,
331 'key': group_key,
332 'forms': [],
333 }
334 temp_groups[group_key]['forms'].append(form)
335
336 for order_key in group_order:
337 if temp_groups.has_key(order_key):
338 self.groups[order_key] = temp_groups.pop(order_key)
339
340 for key in temp_groups:
341 self.groups[key] = temp_groups[key]
342
343 del temp_groups
344
345 self.group_list = self.groups.values()
346
347
348 def remun_formset_factory(parent_model,
349 model,
350 form=forms.ModelForm,
351 formset=GroupedInlineFormset,
352 fk_name=None,
353 fields=None,
354 exclude=None,
355 can_order=False,
356 can_delete=True,
357 max_num=None,
358 formfield_callback=None,
359 group_order=None):
360 trs = rh.TypeRemuneration.objects.all()
361 extra = max_num = trs.count()
362 fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
363 # enforce a max_num=1 when the foreign key to the parent model is unique.
364 if fk.unique:
365 max_num = 1
366 kwargs = {
367 'form': form,
368 'formfield_callback': formfield_callback,
369 'formset': formset,
370 'extra': extra,
371 'can_delete': can_delete,
372 'can_order': can_order,
373 'fields': fields,
374 'exclude': exclude,
375 'max_num': max_num,
376 }
377 FormSet = modelformset_factory(model, **kwargs)
378 FormSet.fk = fk
379
380 def grouper(form):
381 if 'type' in form.initial and form.initial['type']:
382 return (form.initial['type'].nature_remuneration,
383 form.initial['type'].nature_remuneration,
384 )
385
386 def __init__(inst, *a, **kw):
387 super(FormSet, inst).__init__(*a, **kw)
388
389 # Set initial data.
390 for form, tr in zip(inst.forms, trs):
391 form.initial = {
392 'type': tr,
393 }
394
395 # Set form grouping.
396 inst.set_groups(grouper, group_order)
397
398 FormSet.__init__ = __init__
399
400 return FormSet
401
402
403 RemunForm = remun_formset_factory(
404 dae.Dossier,
405 dae.Remuneration,
406 form=FlexibleRemunForm,
407 group_order = [
408 u'Traitement',
409 u'Indemnité',
410 u'Charges',
411 u'Accessoire',
412 ]
413 )
414
415
416 class PosteForm(forms.ModelForm):
417 """ Formulaire des postes. """
418
419 # On ne propose que les services actifs
420 service = forms.ModelChoiceField(
421 queryset=rh.Service.objects.all(), required=True
422 )
423
424 responsable = AutoCompleteSelectField('responsables', required=True)
425 #responsable = forms.ModelChoiceField(
426 # queryset=rh.Poste.objects.select_related(depth=1))
427
428 # La liste des choix est laissée vide. Voir __init__ pour la raison.
429 poste = forms.ChoiceField(label="Nouveau poste ou évolution du poste",
430 choices=(), required=False)
431
432 valeur_point_min = forms.ModelChoiceField(
433 queryset=rh.ValeurPoint.actuelles.all(), required=False
434 )
435 valeur_point_max = forms.ModelChoiceField(
436 queryset=rh.ValeurPoint.actuelles.all(), required=False
437 )
438
439 class Meta:
440 model = dae.Poste
441 fields = ('type_intervention',
442 'poste', 'implantation', 'type_poste', 'service', 'nom',
443 'responsable', 'local', 'expatrie', 'mise_a_disposition',
444 'appel', 'date_debut', 'date_fin',
445 'regime_travail', 'regime_travail_nb_heure_semaine',
446 'classement_min', 'classement_max',
447 'valeur_point_min', 'valeur_point_max',
448 'devise_min', 'devise_max',
449 'salaire_min', 'salaire_max',
450 'indemn_expat_min', 'indemn_expat_max',
451 'indemn_fct_min', 'indemn_fct_max',
452 'charges_patronales_min', 'charges_patronales_max',
453 'autre_min', 'autre_max', 'devise_comparaison',
454 'comp_locale_min', 'comp_locale_max',
455 'comp_universite_min', 'comp_universite_max',
456 'comp_fonctionpub_min', 'comp_fonctionpub_max',
457 'comp_ong_min', 'comp_ong_max',
458 'comp_autre_min', 'comp_autre_max',
459 'justification',
460 )
461 widgets = dict(type_intervention=forms.RadioSelect(),
462 appel=forms.RadioSelect(),
463 nom=forms.TextInput(attrs={'size': 60},),
464 date_debut=admin_widgets.AdminDateWidget(),
465 date_fin=admin_widgets.AdminDateWidget(),
466 justification=forms.Textarea(attrs={'cols': 80},),
467 #devise_min=forms.Select(attrs={'disabled':'disabled'}),
468 #devise_max=forms.Select(attrs={'disabled':'disabled'}),
469 )
470
471 def __init__(self, *args, **kwargs):
472 """ Mise à jour dynamique du contenu du menu des postes.
473
474 Si on ne met le menu à jour de cette façon, à chaque instantiation du
475 formulaire, son contenu est mis en cache par le système et il ne
476 reflète pas les changements apportés par les ajouts, modifications,
477 etc...
478
479 Aussi, dans ce cas-ci, on ne peut pas utiliser un ModelChoiceField
480 car le "id" de chaque choix est spécial (voir _poste_choices).
481
482 """
483 request = kwargs.pop('request')
484 super(PosteForm, self).__init__(*args, **kwargs)
485 self.fields['poste'].choices = self._poste_choices(request)
486
487 self.fields['implantation'].choices = \
488 _implantation_choices(self, request)
489
490 # Quand le dae.Poste n'existe pas, on recherche dans les dossiers rhv1
491 if self.instance and self.instance.id is None:
492 dossiers = self.instance.get_dossiers()
493 if len(dossiers) > 0:
494 self.initial['service'] = dossiers[0].poste.service
495
496 def _poste_choices(self, request):
497 """ Menu déroulant pour les postes.
498 Constitué des postes de RH
499 """
500 postes_rh = rh.Poste.objects.ma_region_ou_service(request.user).all()
501 postes_rh = postes_rh.select_related(depth=1)
502
503 return [('', 'Nouveau poste')] + \
504 sorted([('rh-%s' % p.id, label_poste_display(p)) for p in
505 postes_rh],
506 key=lambda t: t[1])
507
508 def clean(self):
509 """
510 Validation conditionnelles de certains champs.
511 """
512 cleaned_data = self.cleaned_data
513
514 if cleaned_data.get("local") is False \
515 and cleaned_data.get("expatrie") is False:
516 msg = "Le poste doit au moins être ouvert localement " \
517 "ou aux expatriés"
518 self._errors["local"] = self.error_class([msg])
519 self._errors["expatrie"] = ''
520 raise forms.ValidationError(msg)
521
522 return cleaned_data
523
524
525 class ChoosePosteForm(forms.Form):
526 class Meta:
527 fields = ('poste',)
528
529 # La liste des choix est laissée vide. Voir PosteForm.__init__.
530 postes_dae = forms.ChoiceField(choices=(), required=False)
531 postes_rh = forms.ChoiceField(choices=(), required=False)
532
533 def __init__(self, request=None, *args, **kwargs):
534 super(ChoosePosteForm, self).__init__(*args, **kwargs)
535 self.fields['postes_dae'].choices = self._poste_dae_choices(request)
536 self.fields['postes_rh'].choices = self._poste_rh_choices(request)
537
538 def _poste_dae_choices(self, request):
539 """ Menu déroulant pour les postes."""
540 postes_dae = dae.Poste.objects.ma_region_ou_service(request.user) \
541 .exclude(etat__in=(POSTE_ETAT_FINALISE, )) \
542 .annotate(num_dae=Count('dae_dossiers')) \
543 .filter(num_dae=0) \
544 .order_by('implantation', '-date_debut', )
545
546 return [('', '----------')] + \
547 [('dae-%s' % p.id, label_poste_display(p)) for p in postes_dae]
548
549 def _poste_rh_choices(self, request):
550 """ Menu déroulant pour les postes."""
551 postes_dae = dae.Poste.objects.exclude(etat__in=(POSTE_ETAT_FINALISE, ))
552 today = datetime.date.today()
553 id_poste_dae_commences = [p.id_rh_id for p in postes_dae if p.id_rh is not None]
554 postes_rh = rh.Poste.objects.ma_region_ou_service(request.user) \
555 .exclude(id__in=id_poste_dae_commences) \
556 .filter(Q(date_debut__lte=today) &
557 (Q(date_fin__gte=today) |
558 Q(date_fin__isnull=True))
559 ) \
560 .order_by('implantation', '-date_debut', )
561
562 return [('', '----------')] + \
563 [('rh-%s' % p.id, label_poste_display(p)) for p in postes_rh]
564
565 def clean(self):
566 cleaned_data = super(ChoosePosteForm, self).clean()
567 postes_dae = cleaned_data.get("postes_dae")
568 postes_rh = cleaned_data.get("postes_rh")
569 if (postes_dae is u"" and postes_rh is u"") or \
570 (postes_dae is not u"" and postes_rh is not u""):
571 raise forms.ValidationError("Choisissez un poste DAE ou un poste RH")
572 return cleaned_data
573
574 def redirect(self):
575 poste_dae_key = self.cleaned_data.get("postes_dae")
576 if poste_dae_key is not u"":
577 return redirect(reverse('embauche', args=(poste_dae_key,)))
578 poste_rh_key = self.cleaned_data.get("postes_rh")
579 if poste_rh_key is not u"":
580 return redirect("%s?creer_dossier_dae='M'" % reverse('poste', args=(poste_rh_key,)))
581
582 class EmployeForm(forms.ModelForm):
583 """ Formulaire des employés. """
584 class Meta:
585 model = dae.Employe
586 fields = ('employe', 'nom', 'prenom', 'genre')
587
588 # La liste des choix est laissée vide. Voir Poste.__init__ pour la raison.
589 employe = forms.ChoiceField(choices=(), required=False)
590
591 def __init__(self, *args, **kwargs):
592 """ Mise à jour dynamique du contenu du menu des employés. """
593 request = kwargs.pop('request', None)
594 super(EmployeForm, self).__init__(*args, **kwargs)
595 self.fields['employe'].choices = _employe_choices(self, request)
596
597
598 class DossierForm(forms.ModelForm):
599 """ Formulaire des dossiers. """
600 class Meta:
601 exclude = ('etat', 'employe', 'poste', 'date_debut',)
602 model = dae.Dossier
603 widgets = dict(statut_residence=forms.RadioSelect(),
604 contrat_date_debut=admin_widgets.AdminDateWidget(),
605 contrat_date_fin=admin_widgets.AdminDateWidget(),
606 )
607
608 WF_HELP_TEXT = ""
609
610
611 class PosteWorkflowForm(WorkflowFormMixin):
612 bouton_libelles = POSTE_ETATS_BOUTONS
613
614 class Meta:
615 fields = ('etat', )
616 model = dae.Poste
617
618 def __init__(self, *args, **kwargs):
619 super(PosteWorkflowForm, self).__init__(*args, **kwargs)
620 self.fields['etat'].help_text = WF_HELP_TEXT
621
622
623 class DossierWorkflowForm(WorkflowFormMixin):
624 bouton_libelles = POSTE_ETATS_BOUTONS # meme workflow que poste...
625
626 class Meta:
627 fields = ('etat', )
628 model = dae.Dossier
629
630 def __init__(self, *args, **kwargs):
631 super(DossierWorkflowForm, self).__init__(*args, **kwargs)
632 self.fields['etat'].help_text = WF_HELP_TEXT
633 self._etat_initial = self.instance.etat
634
635 def save(self):
636 super(DossierWorkflowForm, self).save()
637 poste = self.instance.poste
638
639 # créer le commentaire automatique pour le poste associé
640 commentaire = WorkflowCommentaire()
641 commentaire.content_object = poste
642 texte = u"Validation automatique à travers le dossier [%s] de %s\n%s" %(
643 self.instance.id,
644 self.instance,
645 self.data.get('commentaire', ''),
646 )
647 commentaire.texte = texte
648 commentaire.etat_initial = self.instance._etat_courant
649 commentaire.etat_final = self.instance.etat
650 commentaire.owner = self.request.user
651 commentaire.save()
652
653 # force l'état du poste
654 poste.etat = self.instance.etat
655 poste.save()
656
657
658 class ContratForm(forms.ModelForm):
659
660 class Meta:
661 fields = ('type_contrat', 'fichier', )
662 model = dae.Contrat
663
664
665 class DAENumeriseeForm(forms.ModelForm):
666
667 class Meta:
668 model = dae.Dossier
669 fields = ('dae_numerisee',)
670
671
672 class DAEFinaliseesSearchForm(forms.Form):
673 q = forms.CharField(
674 label='Recherche', required=False,
675 widget=forms.TextInput(attrs={'size': 40})
676 )
677 importees = forms.ChoiceField(
678 label='Importation', required=False, choices=(
679 ('', ''),
680 ('oui', 'DAE importées seulement'),
681 ('non', 'DAE non-importées seulement'),
682 )
683 )