1 # -*- encoding: utf-8 -*-
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
11 from ajax_select
.fields
import AutoCompleteSelectField
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
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
23 class BaseInlineFormSetWithInitial(BaseInlineFormSet
):
25 Cette classe permet de fournir l'option initial aux inlineformsets.
26 Elle devient désuette en django 1.4.
28 def __init__(self
, data
=None, files
=None, instance
=None,
29 save_as_new
=False, prefix
=None, queryset
=None, **kwargs
):
31 self
.initial_extra
= kwargs
.pop('initial', None)
33 from django
.db
.models
.fields
.related
import RelatedObject
35 self
.instance
= self
.fk
.rel
.to()
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()
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
)
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):
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
65 kwargs
['initial'] = self
.initial_extra
[i
-self
.initial_form_count()]
69 defaults
= {'auto_id': self
.auto_id
, 'prefix': self
.add_prefix(i
)}
71 defaults
['data'] = self
.data
72 defaults
['files'] = self
.files
75 defaults
['initial'] = self
.initial
[i
]
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
)
87 def _implantation_choices(obj
, request
):
89 employe
= groups
.get_employe_from_user(request
.user
)
90 q
= Q(**{'zone_administrative': employe
.implantation
.zone_administrative
})
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
:
97 return [('', '----------')] + \
98 [(i
.id, unicode(i
), )for i
in ref
.Implantation
.objects
.filter(q
)]
101 def _employe_choices(obj
, request
):
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
109 q_rh_region_service
= Q(
110 poste__implantation__zone_administrative
=(
111 employe
.implantation
.zone_administrative
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()
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
125 dossiers_regionaux_ids
= [
126 d
.id for d
in dae
.Dossier
.objects
.filter(q_dae_region_service
)
130 for d
in dae
.Dossier
.objects
132 .annotate(dernier_dossier
=Max('id'))
133 if d
['dernier_dossier'] in dossiers_regionaux_ids
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()]
140 dossiers_regionaux_ids
= [
141 d
.id for d
in rh
.Dossier
.objects
.filter(q_rh_region_service
)
145 for d
in rh
.Dossier
.objects
147 .annotate(dernier_dossier
=Max('id'))
148 if d
['dernier_dossier'] in dossiers_regionaux_ids
150 rhv1
= rh
.Employe
.objects \
151 .filter(id__in
=employes_ids
) \
152 .exclude(id__in
=id_copies
)
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
)
159 def option_label(employe
, extra
=""):
161 extra
= " [%s]" % extra
162 return "%s %s %s" % (employe
.nom
.upper(), employe
.prenom
.title(), extra
)
164 lbl_rh
= sorted([('rh-%s' % p
.id, option_label(p
, "existant dans rh")) for p
in rhv1
],
166 lbl_dae
= sorted([('dae-%s' % p
.id, option_label(p
)) for p
in dae_ | copies | employes_orphelins
],
168 return [('', 'Nouvel employé')] + lbl_rh
+ lbl_dae
171 def label_poste_display(poste
):
172 """Formate un visuel pour un poste dans une liste déroulante"""
175 annee
= poste
.date_debut
.year
178 label
= u
"%s (%s) %s - %s [%s]" % (
180 poste
.implantation
.nom_court
,
182 poste
.type_poste
.categorie_emploi
.nom
,
188 PostePieceFormSet
= inlineformset_factory(dae
.Poste
, dae
.PostePiece
,)
189 DossierPieceForm
= inlineformset_factory(dae
.Dossier
, dae
.DossierPiece
)
191 # Ce formset est utilisé dans le cas de la création de poste prépopulé avec les
193 FinancementFormSetInitial
= inlineformset_factory(
195 dae
.PosteFinancement
,
196 formset
=BaseInlineFormSetWithInitial
,
199 FinancementFormSet
= inlineformset_factory(
201 dae
.PosteFinancement
,
206 class DossierComparaisonForm(forms
.ModelForm
):
208 recherche
= AutoCompleteSelectField('dossiers', required
=False)
209 poste
= forms
.CharField(
210 max_length
=255, widget
=forms
.TextInput(attrs
={'size': '60'})
214 model
= dae
.DossierComparaison
215 exclude
= ('dossier',)
217 DossierComparaisonFormSet
= modelformset_factory(
218 dae
.DossierComparaison
, extra
=3, max_num
=3, form
=DossierComparaisonForm
222 class PosteComparaisonForm(forms
.ModelForm
):
224 recherche
= AutoCompleteSelectField('dae_postes', required
=False)
227 model
= dae
.PosteComparaison
230 # Ce formset est utilisé dans le cas de la création de poste prépopulé avec les
232 PosteComparaisonFormSetInitial
= inlineformset_factory(
234 dae
.PosteComparaison
,
237 form
=PosteComparaisonForm
,
238 formset
=BaseInlineFormSetWithInitial
,
240 PosteComparaisonFormSet
= inlineformset_factory(
242 dae
.PosteComparaison
,
245 form
=PosteComparaisonForm
,
249 class FlexibleRemunForm(forms
.ModelForm
):
251 montant_mensuel
= forms
.DecimalField(required
=False)
252 montant
= forms
.DecimalField(required
=True, label
='Montant annuel')
255 model
= dae
.Remuneration
257 def clean_devise(self
):
258 devise
= self
.cleaned_data
['devise']
259 if devise
.code
== 'EUR':
261 implantation
= ref
.Implantation
.objects
.get(
262 id=self
.data
['implantation']
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
)
273 RemunForm
= inlineformset_factory(
274 dae
.Dossier
, dae
.Remuneration
, extra
=5, form
=FlexibleRemunForm
278 class PosteForm(forms
.ModelForm
):
279 """ Formulaire des postes. """
281 # On ne propose que les services actifs
282 service
= forms
.ModelChoiceField(
283 queryset
=rh
.Service
.objects
.all(), required
=True
286 responsable
= AutoCompleteSelectField('responsables', required
=True)
287 #responsable = forms.ModelChoiceField(
288 # queryset=rh.Poste.objects.select_related(depth=1))
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)
294 valeur_point_min
= forms
.ModelChoiceField(
295 queryset
=rh
.ValeurPoint
.actuelles
.all(), required
=False
297 valeur_point_max
= forms
.ModelChoiceField(
298 queryset
=rh
.ValeurPoint
.actuelles
.all(), required
=False
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',
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'}),
333 def __init__(self
, *args
, **kwargs
):
334 """ Mise à jour dynamique du contenu du menu des postes.
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,
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).
345 request
= kwargs
.pop('request')
346 super(PosteForm
, self
).__init__(*args
, **kwargs
)
347 self
.fields
['poste'].choices
= self
._poste_choices(request
)
349 self
.fields
['implantation'].choices
= \
350 _implantation_choices(self
, request
)
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
358 def _poste_choices(self
, request
):
359 """ Menu déroulant pour les postes.
360 Constitué des postes de RH
362 postes_rh
= rh
.Poste
.objects
.ma_region_ou_service(request
.user
).all()
363 postes_rh
= postes_rh
.select_related(depth
=1)
365 return [('', 'Nouveau poste')] + \
366 sorted([('rh-%s' % p
.id, label_poste_display(p
)) for p
in
372 Validation conditionnelles de certains champs.
374 cleaned_data
= self
.cleaned_data
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 " \
380 self
._errors
["local"] = self
.error_class([msg
])
381 self
._errors
["expatrie"] = ''
382 raise forms
.ValidationError(msg
)
387 class ChoosePosteForm(forms
.Form
):
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)
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
)
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')) \
406 .order_by('-date_debut')
408 return [('', '----------')] + \
409 [('dae-%s' % p
.id, label_poste_display(p
)) for p
in postes_dae
]
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('-date_debut')
419 return [('', '----------')] + \
420 [('rh-%s' % p
.id, label_poste_display(p
)) for p
in postes_rh
]
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")
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" % reverse('poste', args
=(poste_rh_key
,)))
439 class EmployeForm(forms
.ModelForm
):
440 """ Formulaire des employés. """
443 fields
= ('employe', 'nom', 'prenom', 'genre')
445 # La liste des choix est laissée vide. Voir Poste.__init__ pour la raison.
446 employe
= forms
.ChoiceField(choices
=(), required
=False)
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
)
455 class DossierForm(forms
.ModelForm
):
456 """ Formulaire des dossiers. """
458 exclude
= ('etat', 'employe', 'poste', 'date_debut',)
460 widgets
= dict(statut_residence
=forms
.RadioSelect(),
461 contrat_date_debut
=admin_widgets
.AdminDateWidget(),
462 contrat_date_fin
=admin_widgets
.AdminDateWidget(),
468 class PosteWorkflowForm(WorkflowFormMixin
):
469 bouton_libelles
= POSTE_ETATS_BOUTONS
475 def __init__(self
, *args
, **kwargs
):
476 super(PosteWorkflowForm
, self
).__init__(*args
, **kwargs
)
477 self
.fields
['etat'].help_text
= WF_HELP_TEXT
480 class DossierWorkflowForm(WorkflowFormMixin
):
481 bouton_libelles
= POSTE_ETATS_BOUTONS
# meme workflow que poste...
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
493 super(DossierWorkflowForm
, self
).save()
494 poste
= self
.instance
.poste
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" %(
502 self
.data
.get('commentaire', ''),
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
510 # force l'état du poste
511 poste
.etat
= self
.instance
.etat
515 class ContratForm(forms
.ModelForm
):
518 fields
= ('type_contrat', 'fichier', )
522 class DAENumeriseeForm(forms
.ModelForm
):
526 fields
= ('dae_numerisee',)
529 class DAEFinaliseesSearchForm(forms
.Form
):
531 label
='Recherche', required
=False,
532 widget
=forms
.TextInput(attrs
={'size': 40})
534 importees
= forms
.ChoiceField(
535 label
='Importation', required
=False, choices
=(
537 ('oui', 'DAE importées seulement'),
538 ('non', 'DAE non-importées seulement'),