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