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