1 # -=- encoding: utf-8 -=-
4 from datetime
import date
5 from decimal
import Decimal
8 from auf
.django
.emploi
.models
import \
9 GENRE_CHOICES
, SITUATION_CHOICES
# devrait plutot être dans references
10 from auf
.django
.references
import models
as ref
11 from django
.contrib
.auth
.models
import User
12 from django
.core
.files
.storage
import FileSystemStorage
13 from django
.core
.exceptions
import MultipleObjectsReturned
14 from django
.db
import models
15 from django
.db
.models
import Q
16 from django
.db
.models
.signals
import post_save
, pre_save
17 from django
.conf
import settings
19 from project
.rh
.change_list
import \
20 RechercheTemporelle
, KEY_STATUT
, STATUT_ACTIF
, STATUT_INACTIF
, \
22 from project
import groups
23 from project
.rh
.managers
import (
27 DossierComparaisonManager
,
28 PosteComparaisonManager
,
35 TWOPLACES
= Decimal('0.01')
37 from project
.rh
.validators
import validate_date_passee
39 # import pour relocaliser le modèle selon la convention (models.py pour
41 from project
.rh
.historique
import ModificationTraite
44 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
45 REGIME_TRAVAIL_DEFAULT
= Decimal('100.00')
46 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
= Decimal('35.00')
47 REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
= \
48 "Saisir le nombre d'heure de travail à temps complet (100%), " \
49 "sans tenir compte du régime de travail"
52 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
53 base_url
=settings
.PRIVE_MEDIA_URL
)
56 class RemunIntegrityException(Exception):
59 def poste_piece_dispatch(instance
, filename
):
60 path
= "%s/poste/%s/%s" % (
61 instance
._meta
.app_label
, instance
.poste_id
, filename
66 def dossier_piece_dispatch(instance
, filename
):
67 path
= "%s/dossier/%s/%s" % (
68 instance
._meta
.app_label
, instance
.dossier_id
, filename
73 def employe_piece_dispatch(instance
, filename
):
74 path
= "%s/employe/%s/%s" % (
75 instance
._meta
.app_label
, instance
.employe_id
, filename
80 def contrat_dispatch(instance
, filename
):
81 path
= "%s/contrat/%s/%s" % (
82 instance
._meta
.app_label
, instance
.dossier_id
, filename
87 class DateActiviteMixin(models
.Model
):
89 Mixin pour mettre à jour l'activité d'un modèle
93 date_creation
= models
.DateTimeField(auto_now_add
=True,
94 null
=True, blank
=True,
95 verbose_name
=u
"Date de création",)
96 date_modification
= models
.DateTimeField(auto_now
=True,
97 null
=True, blank
=True,
98 verbose_name
=u
"Date de modification",)
101 class Archivable(models
.Model
):
102 archive
= models
.BooleanField(u
'archivé', default
=False)
104 objects
= ArchivableManager()
105 avec_archives
= models
.Manager()
111 class DevisableMixin(object):
113 def get_annee_pour_taux_devise(self
):
114 return datetime
.datetime
.now().year
116 def taux_devise(self
, devise
=None):
122 if devise
.code
== "EUR":
125 annee
= self
.get_annee_pour_taux_devise()
126 taux
= TauxChange
.objects
.filter(devise
=devise
, annee__lte
=annee
) \
130 def montant_euros_float(self
):
132 taux
= self
.taux_devise()
137 return float(self
.montant
) * float(taux
)
139 def montant_euros(self
):
140 return int(round(self
.montant_euros_float(), 2))
143 class Commentaire(models
.Model
):
144 texte
= models
.TextField()
145 owner
= models
.ForeignKey(
146 'auth.User', db_column
='owner', related_name
='+',
147 verbose_name
=u
"Commentaire de"
149 date_creation
= models
.DateTimeField(
150 u
'date', auto_now_add
=True, blank
=True, null
=True
155 ordering
= ['-date_creation']
157 def __unicode__(self
):
158 return u
'%s' % (self
.texte
)
163 POSTE_APPEL_CHOICES
= (
164 ('interne', 'Interne'),
165 ('externe', 'Externe'),
169 class Poste_( DateActiviteMixin
, models
.Model
,):
171 Un Poste est un emploi (job) à combler dans une implantation.
172 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
173 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
176 objects
= PosteManager()
179 nom
= models
.CharField(u
"Titre du poste", max_length
=255)
180 nom_feminin
= models
.CharField(
181 u
"Titre du poste (au féminin)", max_length
=255, null
=True
183 implantation
= models
.ForeignKey(
185 help_text
=u
"Taper le nom de l'implantation ou sa région",
186 db_column
='implantation', related_name
='+'
188 type_poste
= models
.ForeignKey(
189 'TypePoste', db_column
='type_poste',
190 help_text
=u
"Taper le nom du type de poste", related_name
='+',
191 null
=True, verbose_name
=u
"type de poste"
193 service
= models
.ForeignKey(
194 'Service', db_column
='service', related_name
='%(app_label)s_postes',
195 verbose_name
=u
"direction/service/pôle support", null
=True
197 responsable
= models
.ForeignKey(
198 'Poste', db_column
='responsable',
199 related_name
='+', null
=True,
200 help_text
=u
"Taper le nom du poste ou du type de poste",
201 verbose_name
=u
"Poste du responsable"
205 regime_travail
= models
.DecimalField(
206 u
"temps de travail", max_digits
=12, decimal_places
=2,
207 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
208 help_text
="% du temps complet"
210 regime_travail_nb_heure_semaine
= models
.DecimalField(
211 u
"nb. heures par semaine", max_digits
=12, decimal_places
=2,
212 null
=True, default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
213 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
217 local
= models
.NullBooleanField(
218 u
"local", default
=True, null
=True, blank
=True
220 expatrie
= models
.NullBooleanField(
221 u
"expatrié", default
=False, null
=True, blank
=True
223 mise_a_disposition
= models
.NullBooleanField(
224 u
"mise à disposition", null
=True, default
=False
226 appel
= models
.CharField(
227 u
"Appel à candidature", max_length
=10, null
=True,
228 choices
=POSTE_APPEL_CHOICES
, default
='interne'
232 classement_min
= models
.ForeignKey(
233 'Classement', db_column
='classement_min', related_name
='+',
234 null
=True, blank
=True
236 classement_max
= models
.ForeignKey(
237 'Classement', db_column
='classement_max', related_name
='+',
238 null
=True, blank
=True
240 valeur_point_min
= models
.ForeignKey(
242 help_text
=u
"Taper le code ou le nom de l'implantation",
243 db_column
='valeur_point_min', related_name
='+', null
=True,
246 valeur_point_max
= models
.ForeignKey(
248 help_text
=u
"Taper le code ou le nom de l'implantation",
249 db_column
='valeur_point_max', related_name
='+', null
=True,
252 devise_min
= models
.ForeignKey(
253 'Devise', db_column
='devise_min', null
=True, related_name
='+'
255 devise_max
= models
.ForeignKey(
256 'Devise', db_column
='devise_max', null
=True, related_name
='+'
258 salaire_min
= models
.DecimalField(
259 max_digits
=12, decimal_places
=2, default
=0,
261 salaire_max
= models
.DecimalField(
262 max_digits
=12, decimal_places
=2, default
=0,
264 indemn_min
= models
.DecimalField(
265 max_digits
=12, decimal_places
=2, default
=0,
267 indemn_max
= models
.DecimalField(
268 max_digits
=12, decimal_places
=2, default
=0,
270 autre_min
= models
.DecimalField(
271 max_digits
=12, decimal_places
=2, default
=0,
273 autre_max
= models
.DecimalField(
274 max_digits
=12, decimal_places
=2, default
=0,
277 # Comparatifs de rémunération
278 devise_comparaison
= models
.ForeignKey(
279 'Devise', null
=True, blank
=True, db_column
='devise_comparaison',
282 comp_locale_min
= models
.DecimalField(
283 max_digits
=12, decimal_places
=2, null
=True, blank
=True
285 comp_locale_max
= models
.DecimalField(
286 max_digits
=12, decimal_places
=2, null
=True, blank
=True
288 comp_universite_min
= models
.DecimalField(
289 max_digits
=12, decimal_places
=2, null
=True, blank
=True
291 comp_universite_max
= models
.DecimalField(
292 max_digits
=12, decimal_places
=2, null
=True, blank
=True
294 comp_fonctionpub_min
= models
.DecimalField(
295 max_digits
=12, decimal_places
=2, null
=True, blank
=True
297 comp_fonctionpub_max
= models
.DecimalField(
298 max_digits
=12, decimal_places
=2, null
=True, blank
=True
300 comp_ong_min
= models
.DecimalField(
301 max_digits
=12, decimal_places
=2, null
=True, blank
=True
303 comp_ong_max
= models
.DecimalField(
304 max_digits
=12, decimal_places
=2, null
=True, blank
=True
306 comp_autre_min
= models
.DecimalField(
307 max_digits
=12, decimal_places
=2, null
=True, blank
=True
309 comp_autre_max
= models
.DecimalField(
310 max_digits
=12, decimal_places
=2, null
=True, blank
=True
314 justification
= models
.TextField(null
=True, blank
=True)
317 date_debut
= models
.DateField(
318 u
"date de début", help_text
=HELP_TEXT_DATE
, null
=True, blank
=True,
321 date_fin
= models
.DateField(
322 u
"date de fin", help_text
=HELP_TEXT_DATE
, null
=True, blank
=True,
328 ordering
= ['implantation__nom', 'nom']
329 verbose_name
= u
"Poste"
330 verbose_name_plural
= u
"Postes"
333 def __unicode__(self
):
334 representation
= u
'%s - %s [%s]' % (
335 self
.implantation
, self
.nom
, self
.id
337 return representation
339 prefix_implantation
= "implantation__zone_administrative"
341 def get_zones_administratives(self
):
342 return [self
.implantation
.zone_administrative
]
344 def get_devise(self
):
345 vp
= ValeurPoint
.objects
.filter(
346 implantation
=self
.implantation
, devise__archive
=False
351 return Devise
.objects
.get(code
='EUR')
355 __doc__
= Poste_
.__doc__
357 # meta dématérialisation : pour permettre le filtrage
358 vacant
= models
.NullBooleanField(u
"vacant", null
=True, blank
=True)
362 if self
.occupe_par():
366 def occupe_par(self
):
368 Retourne la liste d'employé occupant ce poste.
369 Généralement, retourne une liste d'un élément.
370 Si poste inoccupé, retourne liste vide.
371 UTILISE pour mettre a jour le flag vacant
375 for d
in self
.rh_dossiers
.exclude(date_fin__lt
=date
.today())
378 reversion
.register(Poste
, format
='xml', follow
=[
379 'rh_financements', 'rh_pieces', 'rh_comparaisons_internes',
384 POSTE_FINANCEMENT_CHOICES
= (
385 ('A', 'A - Frais de personnel'),
386 ('B', 'B - Projet(s)-Titre(s)'),
391 class PosteFinancement_(models
.Model
):
393 Pour un Poste, structure d'informations décrivant comment on prévoit
396 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
397 pourcentage
= models
.DecimalField(
398 max_digits
=12, decimal_places
=2,
399 help_text
="ex.: 33.33 % (décimale avec point)"
401 commentaire
= models
.TextField(
402 help_text
="Spécifiez la source de financement."
409 def __unicode__(self
):
410 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
413 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
416 class PosteFinancement(PosteFinancement_
):
417 poste
= models
.ForeignKey(
418 Poste
, db_column
='poste', related_name
='rh_financements'
421 reversion
.register(PosteFinancement
, format
='xml')
424 class PostePiece_(models
.Model
):
426 Documents relatifs au Poste.
427 Ex.: Description de poste
429 nom
= models
.CharField(u
"Nom", max_length
=255)
430 fichier
= models
.FileField(
431 u
"Fichier", upload_to
=poste_piece_dispatch
, storage
=storage_prive
438 def __unicode__(self
):
439 return u
'%s' % (self
.nom
)
442 class PostePiece(PostePiece_
):
443 poste
= models
.ForeignKey(
444 Poste
, db_column
='poste', related_name
='rh_pieces'
447 reversion
.register(PostePiece
, format
='xml')
450 class PosteComparaison_(models
.Model
, DevisableMixin
):
452 De la même manière qu'un dossier, un poste peut-être comparé à un autre
455 objects
= PosteComparaisonManager()
457 implantation
= models
.ForeignKey(
458 ref
.Implantation
, null
=True, blank
=True, related_name
="+"
460 nom
= models
.CharField(u
"Poste", max_length
=255, null
=True, blank
=True)
461 montant
= models
.IntegerField(null
=True)
462 devise
= models
.ForeignKey(
463 "Devise", related_name
='+', null
=True, blank
=True
469 def __unicode__(self
):
473 class PosteComparaison(PosteComparaison_
):
474 poste
= models
.ForeignKey(
475 Poste
, related_name
='rh_comparaisons_internes'
478 reversion
.register(PosteComparaison
, format
='xml')
481 class PosteCommentaire(Commentaire
):
482 poste
= models
.ForeignKey(
483 Poste
, db_column
='poste', related_name
='commentaires'
486 reversion
.register(PosteCommentaire
, format
='xml')
490 class Employe(models
.Model
):
492 Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
493 Dossiers qu'il occupe ou a occupé de Postes.
495 Cette classe aurait pu avantageusement s'appeler Personne car la notion
496 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
499 objects
= EmployeManager()
502 nom
= models
.CharField(max_length
=255)
503 prenom
= models
.CharField(u
"prénom", max_length
=255)
504 nom_affichage
= models
.CharField(
505 u
"nom d'affichage", max_length
=255, null
=True, blank
=True
507 nationalite
= models
.ForeignKey(
508 ref
.Pays
, to_field
='code', db_column
='nationalite',
509 related_name
='employes_nationalite', verbose_name
=u
"nationalité",
510 blank
=True, null
=True
512 date_naissance
= models
.DateField(
513 u
"date de naissance", help_text
=HELP_TEXT_DATE
,
514 validators
=[validate_date_passee
], null
=True, blank
=True
516 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
519 situation_famille
= models
.CharField(
520 u
"situation familiale", max_length
=1, choices
=SITUATION_CHOICES
,
521 null
=True, blank
=True
523 date_entree
= models
.DateField(
524 u
"date d'entrée à l'AUF", help_text
=HELP_TEXT_DATE
, null
=True,
529 tel_domicile
= models
.CharField(
530 u
"tél. domicile", max_length
=255, null
=True, blank
=True
532 tel_cellulaire
= models
.CharField(
533 u
"tél. cellulaire", max_length
=255, null
=True, blank
=True
535 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
536 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
537 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
538 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
539 pays
= models
.ForeignKey(
540 ref
.Pays
, to_field
='code', db_column
='pays',
541 related_name
='employes', null
=True, blank
=True
543 courriel_perso
= models
.EmailField(
544 u
'adresse courriel personnelle', blank
=True
547 # meta dématérialisation : pour permettre le filtrage
548 nb_postes
= models
.IntegerField(u
"nombre de postes", null
=True, blank
=True)
551 ordering
= ['nom', 'prenom']
552 verbose_name
= u
"Employé"
553 verbose_name_plural
= u
"Employés"
555 def __unicode__(self
):
556 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
558 def get_latest_dossier_ordered_by_date_fin_and_principal(self
):
559 res
= self
.rh_dossiers
.order_by(
560 '-principal', 'date_fin')
562 # Retourne en le premier du queryset si la date de fin est None
563 # Sinon, retourne le plus récent selon la date de fin.
565 if first
.date_fin
== None:
568 return res
.order_by('-principal', '-date_fin')[0]
572 if self
.genre
.upper() == u
'M':
574 elif self
.genre
.upper() == u
'F':
580 Retourne l'URL du service retournant la photo de l'Employe.
581 Équivalent reverse url 'rh_photo' avec id en param.
583 from django
.core
.urlresolvers
import reverse
584 return reverse('rh_photo', kwargs
={'id': self
.id})
586 def dossiers_passes(self
):
587 params
= {KEY_STATUT
: STATUT_INACTIF
, }
588 search
= RechercheTemporelle(params
, Dossier
)
589 search
.purge_params(params
)
590 q
= search
.get_q_temporel(self
.rh_dossiers
)
591 return self
.rh_dossiers
.filter(q
)
593 def dossiers_futurs(self
):
594 params
= {KEY_STATUT
: STATUT_FUTUR
, }
595 search
= RechercheTemporelle(params
, Dossier
)
596 search
.purge_params(params
)
597 q
= search
.get_q_temporel(self
.rh_dossiers
)
598 return self
.rh_dossiers
.filter(q
)
600 def dossiers_encours(self
):
601 params
= {KEY_STATUT
: STATUT_ACTIF
, }
602 search
= RechercheTemporelle(params
, Dossier
)
603 search
.purge_params(params
)
604 q
= search
.get_q_temporel(self
.rh_dossiers
)
605 return self
.rh_dossiers
.filter(q
)
607 def dossier_principal_pour_annee(self
):
608 return self
.dossier_principal(pour_annee
=True)
610 def dossier_principal(self
, pour_annee
=False):
612 Retourne le dossier principal (ou le plus ancien si il y en a
615 Si pour_annee == True, retourne le ou les dossiers principaux
616 pour l'annee en cours, sinon, le ou les dossiers principaux
617 pour la journee en cours.
619 TODO: (Refactoring possible): Utiliser meme logique dans
620 dae/templatetags/dae.py
626 year_start
= date(year
, 1, 1)
627 year_end
= date(year
, 12, 31)
630 dossier
= self
.rh_dossiers
.filter(
631 (Q(date_debut__lte
=year_end
, date_fin__isnull
=True) |
632 Q(date_debut__isnull
=True, date_fin__gte
=year_start
) |
633 Q(date_debut__lte
=year_end
, date_fin__gte
=year_start
) |
634 Q(date_debut__isnull
=True, date_fin__isnull
=True)) &
635 Q(principal
=True)).order_by('date_debut')[0]
636 except IndexError, Dossier
.DoesNotExist
:
641 dossier
= self
.rh_dossiers
.filter(
642 (Q(date_debut__lte
=today
, date_fin__isnull
=True) |
643 Q(date_debut__isnull
=True, date_fin__gte
=today
) |
644 Q(date_debut__lte
=today
, date_fin__gte
=today
) |
645 Q(date_debut__isnull
=True, date_fin__isnull
=True)) &
646 Q(principal
=True)).order_by('date_debut')[0]
647 except IndexError, Dossier
.DoesNotExist
:
652 def postes_encours(self
):
653 postes_encours
= set()
654 for d
in self
.dossiers_encours():
655 postes_encours
.add(d
.poste
)
656 return postes_encours
658 def poste_principal(self
):
660 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
662 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
664 # DEPRECATED : on a maintenant Dossier.principal
665 poste
= Poste
.objects
.none()
667 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
672 prefix_implantation
= \
673 "rh_dossiers__poste__implantation__zone_administrative"
675 def get_zones_administratives(self
):
677 d
.poste
.implantation
.zone_administrative
678 for d
in self
.dossiers
.all()
681 reversion
.register(Employe
, format
='xml', follow
=[
682 'pieces', 'commentaires', 'ayantdroits'
686 class EmployePiece(models
.Model
):
688 Documents relatifs à un employé.
691 employe
= models
.ForeignKey(
692 'Employe', db_column
='employe', related_name
="pieces",
693 verbose_name
=u
"employé"
695 nom
= models
.CharField(max_length
=255)
696 fichier
= models
.FileField(
697 u
"fichier", upload_to
=employe_piece_dispatch
, storage
=storage_prive
702 verbose_name
= u
"Employé pièce"
703 verbose_name_plural
= u
"Employé pièces"
705 def __unicode__(self
):
706 return u
'%s' % (self
.nom
)
708 reversion
.register(EmployePiece
, format
='xml')
711 class EmployeCommentaire(Commentaire
):
712 employe
= models
.ForeignKey(
713 'Employe', db_column
='employe', related_name
='commentaires'
717 verbose_name
= u
"Employé commentaire"
718 verbose_name_plural
= u
"Employé commentaires"
720 reversion
.register(EmployeCommentaire
, format
='xml')
723 LIEN_PARENTE_CHOICES
= (
724 ('Conjoint', 'Conjoint'),
725 ('Conjointe', 'Conjointe'),
731 class AyantDroit(models
.Model
):
733 Personne en relation avec un Employe.
736 nom
= models
.CharField(max_length
=255)
737 prenom
= models
.CharField(u
"prénom", max_length
=255)
738 nom_affichage
= models
.CharField(
739 u
"nom d'affichage", max_length
=255, null
=True, blank
=True
741 nationalite
= models
.ForeignKey(
742 ref
.Pays
, to_field
='code', db_column
='nationalite',
743 related_name
='ayantdroits_nationalite',
744 verbose_name
=u
"nationalité", null
=True, blank
=True
746 date_naissance
= models
.DateField(
747 u
"Date de naissance", help_text
=HELP_TEXT_DATE
,
748 validators
=[validate_date_passee
], null
=True, blank
=True
750 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
753 employe
= models
.ForeignKey(
754 'Employe', db_column
='employe', related_name
='ayantdroits',
755 verbose_name
=u
"Employé"
757 lien_parente
= models
.CharField(
758 u
"lien de parenté", max_length
=10, choices
=LIEN_PARENTE_CHOICES
,
759 null
=True, blank
=True
764 verbose_name
= u
"Ayant droit"
765 verbose_name_plural
= u
"Ayants droit"
767 def __unicode__(self
):
768 return u
'%s %s' % (self
.nom
.upper(), self
.prenom
, )
770 prefix_implantation
= \
771 "employe__dossiers__poste__implantation__zone_administrative"
773 def get_zones_administratives(self
):
775 d
.poste
.implantation
.zone_administrative
776 for d
in self
.employe
.dossiers
.all()
779 reversion
.register(AyantDroit
, format
='xml', follow
=['commentaires'])
782 class AyantDroitCommentaire(Commentaire
):
783 ayant_droit
= models
.ForeignKey(
784 'AyantDroit', db_column
='ayant_droit', related_name
='commentaires'
787 reversion
.register(AyantDroitCommentaire
, format
='xml')
792 STATUT_RESIDENCE_CHOICES
= (
794 ('expat', 'Expatrié'),
797 COMPTE_COMPTA_CHOICES
= (
804 class Dossier_(DateActiviteMixin
, models
.Model
, DevisableMixin
,):
806 Le Dossier regroupe les informations relatives à l'occupation
807 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
810 Plusieurs Contrats peuvent être associés au Dossier.
811 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
812 lequel aucun Dossier n'existe est un poste vacant.
815 objects
= DossierManager()
818 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
819 organisme_bstg
= models
.ForeignKey(
820 'OrganismeBstg', db_column
='organisme_bstg', related_name
='+',
821 verbose_name
=u
"organisme",
823 u
"Si détaché (DET) ou mis à disposition (MAD), "
824 u
"préciser l'organisme."
825 ), null
=True, blank
=True
829 remplacement
= models
.BooleanField(default
=False)
830 remplacement_de
= models
.ForeignKey(
831 'self', related_name
='+', help_text
=u
"Taper le nom de l'employé",
832 null
=True, blank
=True
834 statut_residence
= models
.CharField(
835 u
"statut", max_length
=10, default
='local', null
=True,
836 choices
=STATUT_RESIDENCE_CHOICES
840 classement
= models
.ForeignKey(
841 'Classement', db_column
='classement', related_name
='+', null
=True,
844 regime_travail
= models
.DecimalField(
845 u
"régime de travail", max_digits
=12, null
=True, decimal_places
=2,
846 default
=REGIME_TRAVAIL_DEFAULT
, help_text
="% du temps complet"
848 regime_travail_nb_heure_semaine
= models
.DecimalField(
849 u
"nb. heures par semaine", max_digits
=12,
850 decimal_places
=2, null
=True,
851 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
852 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
855 # Occupation du Poste par cet Employe (anciennement "mandat")
856 date_debut
= models
.DateField(
857 u
"date de début d'occupation de poste", db_index
=True
859 date_fin
= models
.DateField(
860 u
"Date de fin d'occupation de poste", null
=True, blank
=True,
865 est_cadre
= models
.BooleanField(
871 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
872 verbose_name
=u
'Compte comptabilité',
873 choices
=COMPTE_COMPTA_CHOICES
)
874 compte_courriel
= models
.BooleanField()
878 ordering
= ['employe__nom', ]
879 verbose_name
= u
"Dossier"
880 verbose_name_plural
= "Dossiers"
882 def salaire_theorique(self
):
883 annee
= date
.today().year
884 coeff
= self
.classement
.coefficient
885 implantation
= self
.poste
.implantation
886 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
888 montant
= coeff
* point
.valeur
889 devise
= point
.devise
890 return {'montant': montant
, 'devise': devise
}
892 def __unicode__(self
):
893 poste
= self
.poste
.nom
894 if self
.employe
.genre
== 'F':
895 poste
= self
.poste
.nom_feminin
896 return u
'%s - %s' % (self
.employe
, poste
)
898 prefix_implantation
= "poste__implantation__zone_administrative"
900 def get_zones_administratives(self
):
901 return [self
.poste
.implantation
.zone_administrative
]
903 def remunerations(self
):
904 key
= "%s_remunerations" % self
._meta
.app_label
905 remunerations
= getattr(self
, key
)
906 return remunerations
.all().order_by('-date_debut')
908 def remunerations_en_cours(self
):
909 q
= Q(date_fin__exact
=None) |
Q(date_fin__gt
=datetime
.date
.today())
910 return self
.remunerations().all().filter(q
).order_by('date_debut')
912 def get_salaire(self
):
914 return [r
for r
in self
.remunerations().order_by('-date_debut')
915 if r
.type_id
== 1][0]
919 def get_salaire_euros(self
):
920 tx
= self
.taux_devise()
921 return (float)(tx
) * (float)(self
.salaire
)
923 def get_remunerations_brutes(self
):
927 4 Indemnité d'expatriation
928 5 Indemnité pour frais
929 6 Indemnité de logement
930 7 Indemnité de fonction
931 8 Indemnité de responsabilité
932 9 Indemnité de transport
933 10 Indemnité compensatrice
934 11 Indemnité de subsistance
935 12 Indemnité différentielle
936 13 Prime d'installation
939 16 Indemnité de départ
940 18 Prime de 13ième mois
943 ids
= [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
944 return [r
for r
in self
.remunerations_en_cours().all()
947 def get_charges_salariales(self
):
949 20 Charges salariales ?
952 return [r
for r
in self
.remunerations_en_cours().all()
955 def get_charges_patronales(self
):
957 17 Charges patronales
960 return [r
for r
in self
.remunerations_en_cours().all()
963 def get_remunerations_tierces(self
):
967 return [r
for r
in self
.remunerations_en_cours().all()
968 if r
.type_id
in (2,)]
972 def get_total_local_charges_salariales(self
):
973 devise
= self
.poste
.get_devise()
975 for r
in self
.get_charges_salariales():
976 if r
.devise
!= devise
:
978 total
+= float(r
.montant
)
981 def get_total_local_charges_patronales(self
):
982 devise
= self
.poste
.get_devise()
984 for r
in self
.get_charges_patronales():
985 if r
.devise
!= devise
:
987 total
+= float(r
.montant
)
990 def get_local_salaire_brut(self
):
992 somme des rémuérations brutes
994 devise
= self
.poste
.get_devise()
996 for r
in self
.get_remunerations_brutes():
997 if r
.devise
!= devise
:
999 total
+= float(r
.montant
)
1002 def get_local_salaire_net(self
):
1004 salaire brut - charges salariales
1006 devise
= self
.poste
.get_devise()
1008 for r
in self
.get_charges_salariales():
1009 if r
.devise
!= devise
:
1011 total_charges
+= float(r
.montant
)
1012 return self
.get_local_salaire_brut() - total_charges
1014 def get_local_couts_auf(self
):
1016 salaire net + charges patronales
1018 devise
= self
.poste
.get_devise()
1020 for r
in self
.get_charges_patronales():
1021 if r
.devise
!= devise
:
1023 total_charges
+= float(r
.montant
)
1024 return self
.get_local_salaire_net() + total_charges
1026 def get_total_local_remunerations_tierces(self
):
1027 devise
= self
.poste
.get_devise()
1029 for r
in self
.get_remunerations_tierces():
1030 if r
.devise
!= devise
:
1032 total
+= float(r
.montant
)
1037 def get_total_charges_salariales(self
):
1039 for r
in self
.get_charges_salariales():
1040 total
+= r
.montant_euros()
1043 def get_total_charges_patronales(self
):
1045 for r
in self
.get_charges_patronales():
1046 total
+= r
.montant_euros()
1049 def get_salaire_brut(self
):
1051 somme des rémuérations brutes
1054 for r
in self
.get_remunerations_brutes():
1055 total
+= r
.montant_euros()
1058 def get_salaire_net(self
):
1060 salaire brut - charges salariales
1063 for r
in self
.get_charges_salariales():
1064 total_charges
+= r
.montant_euros()
1065 return self
.get_salaire_brut() - total_charges
1067 def get_couts_auf(self
):
1069 salaire net + charges patronales
1072 for r
in self
.get_charges_patronales():
1073 total_charges
+= r
.montant_euros()
1074 return self
.get_salaire_net() + total_charges
1076 def get_total_remunerations_tierces(self
):
1078 for r
in self
.get_remunerations_tierces():
1079 total
+= r
.montant_euros()
1082 def premier_contrat(self
):
1083 """contrat avec plus petite date de début"""
1085 contrat
= self
.rh_contrats
.exclude(date_debut
=None) \
1086 .order_by('date_debut')[0]
1087 except IndexError, Contrat
.DoesNotExist
:
1091 def dernier_contrat(self
):
1092 """contrat avec plus grande date de fin"""
1094 contrat
= self
.rh_contrats
.exclude(date_debut
=None) \
1095 .order_by('-date_debut')[0]
1096 except IndexError, Contrat
.DoesNotExist
:
1101 today
= date
.today()
1102 return (self
.date_debut
is None or self
.date_debut
<= today
) \
1103 and (self
.date_fin
is None or self
.date_fin
>= today
) \
1104 and not (self
.date_fin
is None and self
.date_debut
is None)
1107 class Dossier(Dossier_
):
1108 __doc__
= Dossier_
.__doc__
1109 poste
= models
.ForeignKey(
1110 Poste
, db_column
='poste', related_name
='rh_dossiers',
1111 help_text
=u
"Taper le nom du poste ou du type de poste",
1113 employe
= models
.ForeignKey(
1114 'Employe', db_column
='employe',
1115 help_text
=u
"Taper le nom de l'employé",
1116 related_name
='rh_dossiers', verbose_name
=u
"employé"
1118 principal
= models
.BooleanField(
1119 u
"dossier principal", default
=True,
1121 u
"Ce dossier est pour le principal poste occupé par l'employé"
1126 reversion
.register(Dossier
, format
='xml', follow
=[
1127 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
1128 'rh_contrats', 'commentaires'
1132 class RHDossierClassementRecord(models
.Model
):
1133 classement
= models
.ForeignKey(
1135 related_name
='classement_records',
1137 dossier
= models
.ForeignKey(
1139 related_name
='classement_records',
1141 date_debut
= models
.DateField(
1143 help_text
=HELP_TEXT_DATE
,
1148 date_fin
= models
.DateField(
1150 help_text
=HELP_TEXT_DATE
,
1155 commentaire
= models
.CharField(
1162 def __unicode__(self
):
1163 return self
.classement
.__unicode__()
1166 verbose_name
= u
"Element d'historique de classement"
1167 verbose_name_plural
= u
"Historique de classement"
1170 def post_save_handler(cls
,
1177 today
= date
.today()
1178 previous_record
= None
1179 previous_classement
= None
1182 # Premièrement, pour les nouvelles instances:
1184 if not instance
.classement
:
1188 date_debut
=instance
.date_debut
,
1189 classement
=instance
.classement
,
1194 # Deuxièmement, pour les instances existantes:
1197 # 1. Est-ce que le classement a changé?
1198 # 2. Est-ce qu'une historique de classement existe déjà
1200 previous_record
= cls
.objects
.get(
1202 classement
=instance
.before_save
.classement
,
1205 except cls
.DoesNotExist
:
1206 if instance
.before_save
.classement
:
1207 # Il était censé avoir une historique de classement
1209 previous_record
= cls
.objects
.create(
1210 date_debut
=instance
.before_save
.date_debut
,
1211 classement
=instance
.before_save
.classement
,
1214 previous_classement
= instance
.before_save
.classement
1215 except MultipleObjectsReturned
:
1216 qs
= cls
.objects
.filter(
1218 classement
=instance
.before_save
.classement
,
1221 latest
= qs
.latest('date_debut')
1222 qs
.exclude(id=latest
.id).update(date_fin
=today
)
1223 previous_record
= latest
1224 previous_classement
= latest
.classement
1226 previous_classement
= previous_record
.classement
1229 instance
.classement
!=
1233 # Cas aucun changement:
1238 # Classement a changé
1240 previous_record
.date_fin
= today
1241 previous_record
.save()
1243 if instance
.classement
:
1246 classement
=instance
.classement
,
1251 class DossierPiece_(models
.Model
):
1253 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
1254 Ex.: Lettre de motivation.
1256 nom
= models
.CharField(max_length
=255)
1257 fichier
= models
.FileField(
1258 upload_to
=dossier_piece_dispatch
, storage
=storage_prive
1265 def __unicode__(self
):
1266 return u
'%s' % (self
.nom
)
1269 class DossierPiece(DossierPiece_
):
1270 dossier
= models
.ForeignKey(
1271 Dossier
, db_column
='dossier', related_name
='rh_dossierpieces'
1274 reversion
.register(DossierPiece
, format
='xml')
1276 class DossierCommentaire(Commentaire
):
1277 dossier
= models
.ForeignKey(
1278 Dossier
, db_column
='dossier', related_name
='commentaires'
1281 reversion
.register(DossierCommentaire
, format
='xml')
1284 class DossierComparaison_(models
.Model
, DevisableMixin
):
1286 Photo d'une comparaison salariale au moment de l'embauche.
1288 objects
= DossierComparaisonManager()
1290 implantation
= models
.ForeignKey(
1291 ref
.Implantation
, related_name
="+", null
=True, blank
=True
1293 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
1294 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
1295 montant
= models
.IntegerField(null
=True)
1296 devise
= models
.ForeignKey(
1297 'Devise', related_name
='+', null
=True, blank
=True
1303 def __unicode__(self
):
1304 return "%s (%s)" % (self
.poste
, self
.personne
)
1307 class DossierComparaison(DossierComparaison_
):
1308 dossier
= models
.ForeignKey(
1309 Dossier
, related_name
='rh_comparaisons'
1312 reversion
.register(DossierComparaison
, format
='xml')
1317 class RemunerationMixin(models
.Model
):
1320 type = models
.ForeignKey(
1321 'TypeRemuneration', db_column
='type', related_name
='+',
1322 verbose_name
=u
"type de rémunération"
1324 type_revalorisation
= models
.ForeignKey(
1325 'TypeRevalorisation', db_column
='type_revalorisation',
1326 related_name
='+', verbose_name
=u
"type de revalorisation",
1327 null
=True, blank
=True
1329 montant
= models
.DecimalField(
1330 null
=True, blank
=True, max_digits
=12, decimal_places
=2
1331 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1332 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+')
1334 # commentaire = precision
1335 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
1337 # date_debut = anciennement date_effectif
1338 date_debut
= models
.DateField(
1339 u
"date de début", null
=True, blank
=True, db_index
=True
1341 date_fin
= models
.DateField(
1342 u
"date de fin", null
=True, blank
=True, db_index
=True
1347 ordering
= ['type__nom', '-date_fin']
1349 def __unicode__(self
):
1350 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
1353 class Remuneration_(RemunerationMixin
, DevisableMixin
):
1355 Structure de rémunération (données budgétaires) en situation normale
1356 pour un Dossier. Si un Evenement existe, utiliser la structure de
1357 rémunération EvenementRemuneration de cet événement.
1359 objects
= RemunerationManager()
1362 def find_yearly_range(from_date
, to_date
, year
):
1363 today
= date
.today()
1364 year
= year
or date
.today().year
1365 year_start
= date(year
, 1, 1)
1366 year_end
= date(year
, 12, 31)
1368 def constrain_to_year(*dates
):
1370 S'assure que les dates soient dans le range year_start a
1373 return [min(max(year_start
, d
), year_end
)
1377 from_date
or year_start
, year_start
)
1379 to_date
or year_end
, year_end
)
1381 start_date
, end_date
= constrain_to_year(start_date
, end_date
)
1383 jours_annee
= (year_end
- year_start
).days
1384 jours_dates
= (end_date
- start_date
).days
1385 factor
= Decimal(str(jours_dates
)) / Decimal(str(jours_annee
))
1387 return start_date
, end_date
, factor
1390 def montant_ajuste_euros(self
, annee
=None):
1392 Le montant ajusté représente le montant annuel, ajusté sur la
1393 période de temps travaillée, multipliée par le ratio de temps
1394 travaillé (en rapport au temps plein).
1396 date_debut
, date_fin
, factor
= self
.find_yearly_range(
1402 montant_euros
= Decimal(str(self
.montant_euros_float()) or '0')
1404 if self
.type.nature_remuneration
!= u
'Accessoire':
1405 dossier
= getattr(self
, 'dossier', None)
1408 Dans le cas d'un DossierComparaisonRemuneration, il
1409 n'y a plus de reference au dossier.
1411 regime_travail
= REGIME_TRAVAIL_DEFAULT
1413 regime_travail
= self
.dossier
.regime_travail
1414 return (montant_euros
* factor
*
1415 regime_travail
/ 100)
1417 return montant_euros
1419 def montant_mois(self
):
1420 return round(self
.montant
/ 12, 2)
1422 def montant_avec_regime(self
):
1423 return round(self
.montant
* (self
.dossier
.regime_travail
/ 100), 2)
1425 def montant_euro_mois(self
):
1426 return round(self
.montant_euros() / 12, 2)
1428 def __unicode__(self
):
1430 devise
= self
.devise
.code
1433 return "%s %s" % (self
.montant
, devise
)
1437 verbose_name
= u
"Rémunération"
1438 verbose_name_plural
= u
"Rémunérations"
1441 class Remuneration(Remuneration_
):
1442 dossier
= models
.ForeignKey(
1443 Dossier
, db_column
='dossier', related_name
='rh_remunerations'
1446 reversion
.register(Remuneration
, format
='xml')
1451 class Contrat_(models
.Model
):
1453 Document juridique qui encadre la relation de travail d'un Employe
1454 pour un Poste particulier. Pour un Dossier (qui documente cette
1455 relation de travail) plusieurs contrats peuvent être associés.
1457 objects
= ContratManager()
1458 type_contrat
= models
.ForeignKey(
1459 'TypeContrat', db_column
='type_contrat',
1460 verbose_name
=u
'type de contrat', related_name
='+'
1462 date_debut
= models
.DateField(
1463 u
"date de début", db_index
=True
1465 date_fin
= models
.DateField(
1466 u
"date de fin", null
=True, blank
=True, db_index
=True
1468 fichier
= models
.FileField(
1469 upload_to
=contrat_dispatch
, storage
=storage_prive
, null
=True,
1475 ordering
= ['dossier__employe__nom']
1476 verbose_name
= u
"Contrat"
1477 verbose_name_plural
= u
"Contrats"
1479 def __unicode__(self
):
1480 return u
'%s - %s' % (self
.dossier
, self
.id)
1483 class Contrat(Contrat_
):
1484 dossier
= models
.ForeignKey(
1485 Dossier
, db_column
='dossier', related_name
='rh_contrats'
1488 reversion
.register(Contrat
, format
='xml')
1493 class CategorieEmploi(models
.Model
):
1495 Catégorie utilisée dans la gestion des Postes.
1496 Catégorie supérieure à TypePoste.
1498 nom
= models
.CharField(max_length
=255)
1502 verbose_name
= u
"catégorie d'emploi"
1503 verbose_name_plural
= u
"catégories d'emploi"
1505 def __unicode__(self
):
1508 reversion
.register(CategorieEmploi
, format
='xml')
1511 class FamilleProfessionnelle(models
.Model
):
1513 Famille professionnelle d'un poste.
1515 nom
= models
.CharField(max_length
=100)
1519 verbose_name
= u
'famille professionnelle'
1520 verbose_name_plural
= u
'familles professionnelles'
1522 def __unicode__(self
):
1525 reversion
.register(FamilleProfessionnelle
, format
='xml')
1528 class TypePoste(Archivable
):
1532 nom
= models
.CharField(max_length
=255)
1533 nom_feminin
= models
.CharField(u
"nom féminin", max_length
=255)
1534 is_responsable
= models
.BooleanField(
1535 u
"poste de responsabilité", default
=False
1537 categorie_emploi
= models
.ForeignKey(
1538 CategorieEmploi
, db_column
='categorie_emploi', related_name
='+',
1539 verbose_name
=u
"catégorie d'emploi"
1541 famille_professionnelle
= models
.ForeignKey(
1542 FamilleProfessionnelle
, related_name
='types_de_poste',
1543 verbose_name
=u
"famille professionnelle", blank
=True, null
=True
1548 verbose_name
= u
"Type de poste"
1549 verbose_name_plural
= u
"Types de poste"
1551 def __unicode__(self
):
1552 return u
'%s' % (self
.nom
)
1554 reversion
.register(TypePoste
, format
='xml')
1557 TYPE_PAIEMENT_CHOICES
= (
1558 (u
'Régulier', u
'Régulier'),
1559 (u
'Ponctuel', u
'Ponctuel'),
1562 NATURE_REMUNERATION_CHOICES
= (
1563 (u
'Traitement', u
'Traitement'),
1564 (u
'Indemnité', u
'Indemnités autres'),
1565 (u
'Charges', u
'Charges patronales'),
1566 (u
'Accessoire', u
'Accessoires'),
1567 (u
'RAS', u
'Rémunération autre source'),
1571 class TypeRemuneration(Archivable
):
1573 Catégorie de Remuneration.
1576 objects
= models
.Manager()
1577 sans_archives
= ArchivableManager()
1579 nom
= models
.CharField(max_length
=255)
1580 type_paiement
= models
.CharField(
1581 u
"type de paiement", max_length
=30, choices
=TYPE_PAIEMENT_CHOICES
1584 nature_remuneration
= models
.CharField(
1585 u
"nature de la rémunération", max_length
=30,
1586 choices
=NATURE_REMUNERATION_CHOICES
1591 verbose_name
= u
"Type de rémunération"
1592 verbose_name_plural
= u
"Types de rémunération"
1594 def __unicode__(self
):
1597 reversion
.register(TypeRemuneration
, format
='xml')
1600 class TypeRevalorisation(Archivable
):
1602 Justification du changement de la Remuneration.
1603 (Actuellement utilisé dans aucun traitement informatique.)
1605 nom
= models
.CharField(max_length
=255)
1609 verbose_name
= u
"Type de revalorisation"
1610 verbose_name_plural
= u
"Types de revalorisation"
1612 def __unicode__(self
):
1613 return u
'%s' % (self
.nom
)
1615 reversion
.register(TypeRevalorisation
, format
='xml')
1618 class Service(Archivable
):
1620 Unité administrative où les Postes sont rattachés.
1622 nom
= models
.CharField(max_length
=255)
1626 verbose_name
= u
"service"
1627 verbose_name_plural
= u
"services"
1629 def __unicode__(self
):
1632 reversion
.register(Service
, format
='xml')
1635 TYPE_ORGANISME_CHOICES
= (
1636 ('MAD', 'Mise à disposition'),
1637 ('DET', 'Détachement'),
1641 class OrganismeBstg(models
.Model
):
1643 Organisation d'où provient un Employe mis à disposition (MAD) de
1644 ou détaché (DET) à l'AUF à titre gratuit.
1646 (BSTG = bien et service à titre gratuit.)
1648 nom
= models
.CharField(max_length
=255)
1649 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1650 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1652 related_name
='organismes_bstg',
1653 null
=True, blank
=True)
1656 ordering
= ['type', 'nom']
1657 verbose_name
= u
"Organisme BSTG"
1658 verbose_name_plural
= u
"Organismes BSTG"
1660 def __unicode__(self
):
1661 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1663 reversion
.register(OrganismeBstg
, format
='xml')
1666 class Statut(Archivable
):
1668 Statut de l'Employe dans le cadre d'un Dossier particulier.
1671 code
= models
.CharField(
1672 max_length
=25, unique
=True,
1674 u
"Saisir un code court mais lisible pour ce statut : "
1675 u
"le code est utilisé pour associer les statuts aux autres "
1676 u
"données tout en demeurant plus lisible qu'un identifiant "
1680 nom
= models
.CharField(max_length
=255)
1684 verbose_name
= u
"Statut d'employé"
1685 verbose_name_plural
= u
"Statuts d'employé"
1687 def __unicode__(self
):
1688 return u
'%s : %s' % (self
.code
, self
.nom
)
1690 reversion
.register(Statut
, format
='xml')
1693 TYPE_CLASSEMENT_CHOICES
= (
1694 ('S', 'S -Soutien'),
1695 ('T', 'T - Technicien'),
1696 ('P', 'P - Professionel'),
1698 ('D', 'D - Direction'),
1699 ('SO', 'SO - Sans objet [expatriés]'),
1700 ('HG', 'HG - Hors grille [direction]'),
1704 class ClassementManager(models
.Manager
):
1706 Ordonner les spcéfiquement les classements.
1708 def get_query_set(self
):
1709 qs
= super(ClassementManager
, self
).get_query_set()
1710 qs
= qs
.extra(select
={
1711 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1713 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1717 class ClassementArchivableManager(ClassementManager
,
1722 class Classement_(Archivable
):
1724 Éléments de classement de la
1725 "Grille générique de classement hiérarchique".
1727 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1728 classement dans la grille. Le classement donne le coefficient utilisé dans:
1730 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1732 objects
= ClassementManager()
1733 sans_archives
= ClassementArchivableManager()
1736 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1737 echelon
= models
.IntegerField(u
"échelon", blank
=True, default
=0)
1738 degre
= models
.IntegerField(u
"degré", blank
=True, default
=0)
1739 coefficient
= models
.FloatField(u
"coefficient", blank
=True, null
=True)
1742 # annee # au lieu de date_debut et date_fin
1743 commentaire
= models
.TextField(null
=True, blank
=True)
1747 ordering
= ['type', 'echelon', 'degre', 'coefficient']
1748 verbose_name
= u
"Classement"
1749 verbose_name_plural
= u
"Classements"
1751 def __unicode__(self
):
1752 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1755 class Classement(Classement_
):
1756 __doc__
= Classement_
.__doc__
1758 reversion
.register(Classement
, format
='xml')
1761 class TauxChange_(models
.Model
):
1763 Taux de change de la devise vers l'euro (EUR)
1764 pour chaque année budgétaire.
1767 devise
= models
.ForeignKey('Devise', db_column
='devise')
1768 annee
= models
.IntegerField(u
"année")
1769 taux
= models
.FloatField(u
"taux vers l'euro")
1773 ordering
= ['-annee', 'devise__code']
1774 verbose_name
= u
"Taux de change"
1775 verbose_name_plural
= u
"Taux de change"
1776 unique_together
= ('devise', 'annee')
1778 def __unicode__(self
):
1779 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1782 class TauxChange(TauxChange_
):
1783 __doc__
= TauxChange_
.__doc__
1785 reversion
.register(TauxChange
, format
='xml')
1788 class ValeurPointManager(models
.Manager
):
1790 def get_query_set(self
):
1791 return super(ValeurPointManager
, self
).get_query_set() \
1792 .select_related('devise', 'implantation')
1795 class ValeurPoint_(models
.Model
):
1797 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1798 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1799 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1801 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1804 objects
= models
.Manager()
1805 actuelles
= ValeurPointManager()
1807 valeur
= models
.FloatField(null
=True)
1808 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1809 implantation
= models
.ForeignKey(ref
.Implantation
,
1810 db_column
='implantation',
1811 related_name
='%(app_label)s_valeur_point')
1813 annee
= models
.IntegerField()
1816 ordering
= ['-annee', 'implantation__nom']
1818 verbose_name
= u
"Valeur du point"
1819 verbose_name_plural
= u
"Valeurs du point"
1820 unique_together
= ('implantation', 'annee')
1822 def __unicode__(self
):
1823 return u
'%s %s %s [%s] %s' % (
1824 self
.devise
.code
, self
.annee
, self
.valeur
,
1825 self
.implantation
.nom_court
, self
.devise
.nom
1829 class ValeurPoint(ValeurPoint_
):
1830 __doc__
= ValeurPoint_
.__doc__
1832 reversion
.register(ValeurPoint
, format
='xml')
1835 class Devise(Archivable
):
1839 code
= models
.CharField(max_length
=10, unique
=True)
1840 nom
= models
.CharField(max_length
=255)
1844 verbose_name
= u
"devise"
1845 verbose_name_plural
= u
"devises"
1847 def __unicode__(self
):
1848 return u
'%s - %s' % (self
.code
, self
.nom
)
1850 reversion
.register(Devise
, format
='xml')
1853 class TypeContrat(Archivable
):
1857 nom
= models
.CharField(max_length
=255)
1858 nom_long
= models
.CharField(max_length
=255)
1862 verbose_name
= u
"Type de contrat"
1863 verbose_name_plural
= u
"Types de contrat"
1865 def __unicode__(self
):
1866 return u
'%s' % (self
.nom
)
1868 reversion
.register(TypeContrat
, format
='xml')
1873 class ResponsableImplantationProxy(ref
.Implantation
):
1881 verbose_name
= u
"Responsable d'implantation"
1882 verbose_name_plural
= u
"Responsables d'implantation"
1885 class ResponsableImplantation(models
.Model
):
1887 Le responsable d'une implantation.
1888 Anciennement géré sur le Dossier du responsable.
1890 employe
= models
.ForeignKey(
1891 'Employe', db_column
='employe', related_name
='+', null
=True,
1894 implantation
= models
.OneToOneField(
1895 "ResponsableImplantationProxy", db_column
='implantation',
1896 related_name
='responsable', unique
=True
1899 def __unicode__(self
):
1900 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1903 ordering
= ['implantation__nom']
1904 verbose_name
= "Responsable d'implantation"
1905 verbose_name_plural
= "Responsables d'implantation"
1907 reversion
.register(ResponsableImplantation
, format
='xml')
1910 class UserProfile(models
.Model
):
1911 user
= models
.OneToOneField(User
, related_name
='profile')
1912 zones_administratives
= models
.ManyToManyField(
1913 ref
.ZoneAdministrative
,
1914 related_name
='profiles'
1917 verbose_name
= "Permissions sur zones administratives"
1918 verbose_name_plural
= "Permissions sur zones administratives"
1920 def __unicode__(self
):
1921 return self
.user
.__unicode__()
1923 reversion
.register(UserProfile
, format
='xml')
1927 TYPES_CHANGEMENT
= (
1934 class ChangementPersonnelNotifications(models
.Model
):
1936 verbose_name
= u
"Destinataire pour notices de mouvement de personnel"
1937 verbose_name_plural
= u
"Destinataires pour notices de mouvement de personnel"
1939 type = models
.CharField(
1941 choices
= TYPES_CHANGEMENT
,
1945 destinataires
= models
.ManyToManyField(
1947 related_name
='changement_notifications',
1950 def __unicode__(self
):
1952 self
.get_type_display(), ','.join(
1953 self
.destinataires
.all().values_list(
1954 'courriel', flat
=True))
1958 class ChangementPersonnel(models
.Model
):
1960 Une notice qui enregistre un changement de personnel, incluant:
1963 * Mouvement de personnel
1968 verbose_name
= u
"Mouvement de personnel"
1969 verbose_name_plural
= u
"Mouvements de personnel"
1971 def __unicode__(self
):
1972 return '%s: %s' % (self
.dossier
.__unicode__(),
1973 self
.get_type_display())
1976 def create_changement(cls
, dossier
, type):
1977 # If this employe has existing Changement, set them to invalid.
1978 cls
.objects
.filter(dossier__employe
=dossier
.employe
).update(valide
=False)
1990 def post_save_handler(cls
,
1997 # This defines the time limit used when checking in previous
1998 # files to see if an employee if new. Basically, if emloyee
1999 # left his position new_file.date_debut -
2000 # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then
2001 # if a new file is created for this employee, he will bec
2002 # onsidered "NEW" and a notice will be created to this effect.
2003 NEW_EMPLOYE_THRESHOLD
= datetime
.timedelta(7) # 7 days.
2005 other_dossier_qs
= instance
.employe
.rh_dossiers
.exclude(
2007 dd
= instance
.date_debut
2008 df
= instance
.date_fin
2009 today
= date
.today()
2011 # Here, verify differences between the instance, before and
2013 df_has_changed
= False
2017 df_has_changed
= True
2019 df_has_changed
= (df
!= instance
.before_save
.date_fin
and
2025 # Date de fin est None et c'est une nouvelle instance de
2027 if not df
and created
:
2028 # QS for finding other dossiers with a date_fin of None OR
2029 # with a date_fin >= to this dossier's date_debut
2030 exists_recent_file_qs
= other_dossier_qs
.filter(
2031 Q(date_fin__isnull
=True) |
2032 Q(date_fin__gte
=dd
- NEW_EMPLOYE_THRESHOLD
)
2035 # 1. If existe un Dossier récent
2036 if exists_recent_file_qs
.count() > 0:
2037 cls
.create_changement(
2041 # 2. Il n'existe un Dossier récent, et c'est une nouvelle
2042 # instance de Dossier:
2044 cls
.create_changement(
2049 elif not df
and not created
and cls
.objects
.filter(
2051 date_creation__gte
=today
- NEW_EMPLOYE_THRESHOLD
,
2054 cls
.create_changement(
2059 # Date de fin a été modifiée:
2061 # QS for other active files (date_fin == None), excludes
2063 exists_active_files_qs
= other_dossier_qs
.filter(
2064 Q(date_fin__isnull
=True))
2066 # 3. Date de fin a été modifiée et il n'existe aucun autre
2067 # dossier actifs: Depart
2068 if exists_active_files_qs
.count() == 0:
2069 cls
.create_changement(
2073 # 4. Dossier a une nouvelle date de fin par contre
2074 # d'autres dossiers actifs existent déjà: Mouvement
2076 cls
.create_changement(
2082 dossier
= models
.ForeignKey(
2084 related_name
='mouvements',
2087 valide
= models
.BooleanField(default
=True)
2088 date_creation
= models
.DateTimeField(
2090 communique
= models
.BooleanField(
2094 date_communication
= models
.DateTimeField(
2099 type = models
.CharField(
2101 choices
= TYPES_CHANGEMENT
,
2104 reversion
.register(ChangementPersonnel
, format
='xml')
2107 def dossier_pre_save_handler(sender
,
2111 # Store a copy of the model before save is called.
2112 if instance
.pk
is not None:
2113 instance
.before_save
= Dossier
.objects
.get(pk
=instance
.pk
)
2115 instance
.before_save
= None
2118 # Connect a pre_save handler that assigns a copy of the model as an
2119 # attribute in order to compare it in post_save.
2120 pre_save
.connect(dossier_pre_save_handler
, sender
=Dossier
)
2122 post_save
.connect(ChangementPersonnel
.post_save_handler
, sender
=Dossier
)
2123 post_save
.connect(RHDossierClassementRecord
.post_save_handler
, sender
=Dossier
)