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