1 # -*- encoding: utf-8 -*-
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 (
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
17 from ajax_select
.fields
import AutoCompleteSelectField
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
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
30 class BaseInlineFormSetWithInitial(BaseInlineFormSet
):
32 Cette classe permet de fournir l'option initial aux inlineformsets.
33 Elle devient désuette en django 1.4.
35 def __init__(self
, data
=None, files
=None, instance
=None,
36 save_as_new
=False, prefix
=None, queryset
=None, **kwargs
):
38 self
.initial_extra
= kwargs
.pop('initial', None)
40 from django
.db
.models
.fields
.related
import RelatedObject
42 self
.instance
= self
.fk
.rel
.to()
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()
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
)
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):
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
72 kwargs
['initial'] = self
.initial_extra
[i
-self
.initial_form_count()]
76 defaults
= {'auto_id': self
.auto_id
, 'prefix': self
.add_prefix(i
)}
78 defaults
['data'] = self
.data
79 defaults
['files'] = self
.files
82 defaults
['initial'] = self
.initial
[i
]
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
)
94 def _implantation_choices(obj
, request
):
96 employe
= groups
.get_employe_from_user(request
.user
)
97 q
= Q(**{'zone_administrative': employe
.implantation
.zone_administrative
})
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
:
104 return [('', '----------')] + \
105 [(i
.id, unicode(i
), )for i
in ref
.Implantation
.objects
.filter(q
)]
108 def _employe_choices(obj
, request
):
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
116 q_rh_region_service
= Q(
117 poste__implantation__zone_administrative
=(
118 employe
.implantation
.zone_administrative
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()
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
132 dossiers_regionaux_ids
= [
133 d
.id for d
in dae
.Dossier
.objects
.filter(q_dae_region_service
)
137 for d
in dae
.Dossier
.objects
139 .annotate(dernier_dossier
=Max('id'))
140 if d
['dernier_dossier'] in dossiers_regionaux_ids
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()]
147 dossiers_regionaux_ids
= [
148 d
.id for d
in rh
.Dossier
.objects
.filter(q_rh_region_service
)
152 for d
in rh
.Dossier
.objects
154 .annotate(dernier_dossier
=Max('id'))
155 if d
['dernier_dossier'] in dossiers_regionaux_ids
157 rhv1
= rh
.Employe
.objects \
158 .filter(id__in
=employes_ids
) \
159 .exclude(id__in
=id_copies
)
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
)
166 def option_label(employe
, extra
=""):
168 extra
= " [%s]" % extra
169 return "%s %s %s" % (employe
.nom
.upper(), employe
.prenom
.title(), extra
)
171 lbl_rh
= sorted([('rh-%s' % p
.id, option_label(p
, "existant dans rh")) for p
in rhv1
],
173 lbl_dae
= sorted([('dae-%s' % p
.id, option_label(p
)) for p
in dae_ | copies | employes_orphelins
],
175 return [('', 'Nouvel employé')] + lbl_rh
+ lbl_dae
178 def label_poste_display(poste
):
179 """Formate un visuel pour un poste dans une liste déroulante"""
182 annee
= poste
.date_debut
.year
185 label
= u
"%s (%s) %s [%s]" % (
187 poste
.implantation
.nom_court
,
189 #poste.type_poste.categorie_emploi.nom,
195 PostePieceFormSet
= inlineformset_factory(dae
.Poste
, dae
.PostePiece
,)
196 DossierPieceForm
= inlineformset_factory(dae
.Dossier
, dae
.DossierPiece
)
198 # Ce formset est utilisé dans le cas de la création de poste prépopulé avec les
200 FinancementFormSetInitial
= inlineformset_factory(
202 dae
.PosteFinancement
,
203 formset
=BaseInlineFormSetWithInitial
,
206 FinancementFormSet
= inlineformset_factory(
208 dae
.PosteFinancement
,
213 class DossierComparaisonForm(forms
.ModelForm
):
215 recherche
= AutoCompleteSelectField('dossiers', required
=False)
216 poste
= forms
.CharField(
217 max_length
=255, widget
=forms
.TextInput(attrs
={'size': '60'})
221 model
= dae
.DossierComparaison
222 exclude
= ('dossier',)
224 DossierComparaisonFormSet
= modelformset_factory(
225 dae
.DossierComparaison
, extra
=3, max_num
=3, form
=DossierComparaisonForm
229 class PosteComparaisonForm(forms
.ModelForm
):
231 recherche
= AutoCompleteSelectField('dae_postes', required
=False)
234 model
= dae
.PosteComparaison
237 # Ce formset est utilisé dans le cas de la création de poste prépopulé avec les
239 PosteComparaisonFormSetInitial
= inlineformset_factory(
241 dae
.PosteComparaison
,
244 form
=PosteComparaisonForm
,
245 formset
=BaseInlineFormSetWithInitial
,
247 PosteComparaisonFormSet
= inlineformset_factory(
249 dae
.PosteComparaison
,
252 form
=PosteComparaisonForm
,
256 class FlexibleRemunForm(forms
.ModelForm
):
258 montant_mensuel
= forms
.DecimalField(required
=False)
259 montant
= forms
.DecimalField(required
=True, label
='Montant annuel')
262 model
= dae
.Remuneration
264 def __init__(self
, *a
, **kw
):
265 super(FlexibleRemunForm
, self
).__init__(*a
, **kw
)
266 self
.fields
['type'].widget
= ReadOnlyChoiceWidget(choices
=self
.fields
['type'].choices
)
268 def clean_devise(self
):
269 devise
= self
.cleaned_data
['devise']
270 if devise
.code
== 'EUR':
272 implantation
= ref
.Implantation
.objects
.get(
273 id=self
.data
['implantation']
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
)
284 def has_changed(self
):
286 Modification de has_changed pour qu'il ignore les montant a 0
290 changed_data
= self
.changed_data
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'))
297 # Montant is set to 0 in javascript, ifnore 'montant' data if
300 # Generer le key tel qu'identifié dans self.data:
301 montant_key
= '-'.join((self
.prefix
, 'montant'))
303 if ('montant' in changed_data
and
304 self
.data
.get(montant_key
, '0') == '0'):
305 changed_data
.pop(changed_data
.index('montant'))
307 return bool(changed_data
)
310 class GroupedInlineFormset(BaseInlineFormSet
):
312 def set_groups(self
, group_accessor
, group_order
=[]):
314 group_accessor: A function that will get the key and name from
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
323 self
.groups
= OrderedDict()
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
] = {
334 temp_groups
[group_key
]['forms'].append(form
)
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
)
340 for key
in temp_groups
:
341 self
.groups
[key
] = temp_groups
[key
]
345 self
.group_list
= self
.groups
.values()
348 def remun_formset_factory(parent_model
,
350 form
=forms
.ModelForm
,
351 formset
=GroupedInlineFormset
,
358 formfield_callback
=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.
368 'formfield_callback': formfield_callback
,
371 'can_delete': can_delete
,
372 'can_order': can_order
,
377 FormSet
= modelformset_factory(model
, **kwargs
)
381 if 'type' in form
.initial
and form
.initial
['type']:
382 return (form
.initial
['type'].nature_remuneration
,
383 form
.initial
['type'].nature_remuneration
,
386 def __init__(inst
, *a
, **kw
):
387 super(FormSet
, inst
).__init__(*a
, **kw
)
390 for form
, tr
in zip(inst
.forms
, trs
):
396 inst
.set_groups(grouper
, group_order
)
398 FormSet
.__init__
= __init__
403 RemunForm
= remun_formset_factory(
406 form
=FlexibleRemunForm
,
416 class PosteForm(forms
.ModelForm
):
417 """ Formulaire des postes. """
419 # On ne propose que les services actifs
420 service
= forms
.ModelChoiceField(
421 queryset
=rh
.Service
.objects
.all(), required
=True
424 responsable
= AutoCompleteSelectField('responsables', required
=True)
425 #responsable = forms.ModelChoiceField(
426 # queryset=rh.Poste.objects.select_related(depth=1))
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)
432 valeur_point_min
= forms
.ModelChoiceField(
433 queryset
=rh
.ValeurPoint
.actuelles
.all(), required
=False
435 valeur_point_max
= forms
.ModelChoiceField(
436 queryset
=rh
.ValeurPoint
.actuelles
.all(), required
=False
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',
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'}),
471 def __init__(self
, *args
, **kwargs
):
472 """ Mise à jour dynamique du contenu du menu des postes.
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,
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).
483 request
= kwargs
.pop('request')
484 super(PosteForm
, self
).__init__(*args
, **kwargs
)
485 self
.fields
['poste'].choices
= self
._poste_choices(request
)
487 self
.fields
['implantation'].choices
= \
488 _implantation_choices(self
, request
)
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
496 def _poste_choices(self
, request
):
497 """ Menu déroulant pour les postes.
498 Constitué des postes de RH
500 postes_rh
= rh
.Poste
.objects
.ma_region_ou_service(request
.user
).all()
501 postes_rh
= postes_rh
.select_related(depth
=1)
503 return [('', 'Nouveau poste')] + \
504 sorted([('rh-%s' % p
.id, label_poste_display(p
)) for p
in
510 Validation conditionnelles de certains champs.
512 cleaned_data
= self
.cleaned_data
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 " \
518 self
._errors
["local"] = self
.error_class([msg
])
519 self
._errors
["expatrie"] = ''
520 raise forms
.ValidationError(msg
)
525 class ChoosePosteForm(forms
.Form
):
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)
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
)
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')) \
544 .order_by('implantation', '-date_debut', )
546 return [('', '----------')] + \
547 [('dae-%s' % p
.id, label_poste_display(p
)) for p
in postes_dae
]
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))
560 .order_by('implantation', '-date_debut', )
562 return [('', '----------')] + \
563 [('rh-%s' % p
.id, label_poste_display(p
)) for p
in postes_rh
]
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")
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
,)))
582 class EmployeForm(forms
.ModelForm
):
583 """ Formulaire des employés. """
586 fields
= ('employe', 'nom', 'prenom', 'genre')
588 # La liste des choix est laissée vide. Voir Poste.__init__ pour la raison.
589 employe
= forms
.ChoiceField(choices
=(), required
=False)
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
)
598 class DossierForm(forms
.ModelForm
):
599 """ Formulaire des dossiers. """
601 exclude
= ('etat', 'employe', 'poste', 'date_debut',)
603 widgets
= dict(statut_residence
=forms
.RadioSelect(),
604 contrat_date_debut
=admin_widgets
.AdminDateWidget(),
605 contrat_date_fin
=admin_widgets
.AdminDateWidget(),
611 class PosteWorkflowForm(WorkflowFormMixin
):
612 bouton_libelles
= POSTE_ETATS_BOUTONS
618 def __init__(self
, *args
, **kwargs
):
619 super(PosteWorkflowForm
, self
).__init__(*args
, **kwargs
)
620 self
.fields
['etat'].help_text
= WF_HELP_TEXT
623 class DossierWorkflowForm(WorkflowFormMixin
):
624 bouton_libelles
= POSTE_ETATS_BOUTONS
# meme workflow que poste...
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
636 super(DossierWorkflowForm
, self
).save()
637 poste
= self
.instance
.poste
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" %(
645 self
.data
.get('commentaire', ''),
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
653 # force l'état du poste
654 poste
.etat
= self
.instance
.etat
658 class ContratForm(forms
.ModelForm
):
661 fields
= ('type_contrat', 'fichier', )
665 class DAENumeriseeForm(forms
.ModelForm
):
669 fields
= ('dae_numerisee',)
672 class DAEFinaliseesSearchForm(forms
.Form
):
674 label
='Recherche', required
=False,
675 widget
=forms
.TextInput(attrs
={'size': 40})
677 importees
= forms
.ChoiceField(
678 label
='Importation', required
=False, choices
=(
680 ('oui', 'DAE importées seulement'),
681 ('non', 'DAE non-importées seulement'),