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