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)
560 if self
.genre
.upper() == u
'M':
562 elif self
.genre
.upper() == u
'F':
568 Retourne l'URL du service retournant la photo de l'Employe.
569 Équivalent reverse url 'rh_photo' avec id en param.
571 from django
.core
.urlresolvers
import reverse
572 return reverse('rh_photo', kwargs
={'id': self
.id})
574 def dossiers_passes(self
):
575 params
= {KEY_STATUT
: STATUT_INACTIF
, }
576 search
= RechercheTemporelle(params
, Dossier
)
577 search
.purge_params(params
)
578 q
= search
.get_q_temporel(self
.rh_dossiers
)
579 return self
.rh_dossiers
.filter(q
)
581 def dossiers_futurs(self
):
582 params
= {KEY_STATUT
: STATUT_FUTUR
, }
583 search
= RechercheTemporelle(params
, Dossier
)
584 search
.purge_params(params
)
585 q
= search
.get_q_temporel(self
.rh_dossiers
)
586 return self
.rh_dossiers
.filter(q
)
588 def dossiers_encours(self
):
589 params
= {KEY_STATUT
: STATUT_ACTIF
, }
590 search
= RechercheTemporelle(params
, Dossier
)
591 search
.purge_params(params
)
592 q
= search
.get_q_temporel(self
.rh_dossiers
)
593 return self
.rh_dossiers
.filter(q
)
595 def dossier_principal_pour_annee(self
):
596 return self
.dossier_principal(pour_annee
=True)
598 def dossier_principal(self
, pour_annee
=False):
600 Retourne le dossier principal (ou le plus ancien si il y en a
603 Si pour_annee == True, retourne le ou les dossiers principaux
604 pour l'annee en cours, sinon, le ou les dossiers principaux
605 pour la journee en cours.
607 TODO: (Refactoring possible): Utiliser meme logique dans
608 dae/templatetags/dae.py
614 year_start
= date(year
, 1, 1)
615 year_end
= date(year
, 12, 31)
618 dossier
= self
.rh_dossiers
.filter(
619 (Q(date_debut__lte
=year_end
, date_fin__isnull
=True) |
620 Q(date_debut__isnull
=True, date_fin__gte
=year_start
) |
621 Q(date_debut__lte
=year_end
, date_fin__gte
=year_start
) |
622 Q(date_debut__isnull
=True, date_fin__isnull
=True)) &
623 Q(principal
=True)).order_by('date_debut')[0]
624 except IndexError, Dossier
.DoesNotExist
:
629 dossier
= self
.rh_dossiers
.filter(
630 (Q(date_debut__lte
=today
, date_fin__isnull
=True) |
631 Q(date_debut__isnull
=True, date_fin__gte
=today
) |
632 Q(date_debut__lte
=today
, date_fin__gte
=today
) |
633 Q(date_debut__isnull
=True, date_fin__isnull
=True)) &
634 Q(principal
=True)).order_by('date_debut')[0]
635 except IndexError, Dossier
.DoesNotExist
:
640 def postes_encours(self
):
641 postes_encours
= set()
642 for d
in self
.dossiers_encours():
643 postes_encours
.add(d
.poste
)
644 return postes_encours
646 def poste_principal(self
):
648 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
650 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
652 # DEPRECATED : on a maintenant Dossier.principal
653 poste
= Poste
.objects
.none()
655 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
660 prefix_implantation
= \
661 "rh_dossiers__poste__implantation__zone_administrative"
663 def get_zones_administratives(self
):
665 d
.poste
.implantation
.zone_administrative
666 for d
in self
.dossiers
.all()
669 reversion
.register(Employe
, format
='xml', follow
=[
670 'pieces', 'commentaires', 'ayantdroits'
674 class EmployePiece(models
.Model
):
676 Documents relatifs à un employé.
679 employe
= models
.ForeignKey(
680 'Employe', db_column
='employe', related_name
="pieces",
681 verbose_name
=u
"employé"
683 nom
= models
.CharField(max_length
=255)
684 fichier
= models
.FileField(
685 u
"fichier", upload_to
=employe_piece_dispatch
, storage
=storage_prive
690 verbose_name
= u
"Employé pièce"
691 verbose_name_plural
= u
"Employé pièces"
693 def __unicode__(self
):
694 return u
'%s' % (self
.nom
)
696 reversion
.register(EmployePiece
, format
='xml')
699 class EmployeCommentaire(Commentaire
):
700 employe
= models
.ForeignKey(
701 'Employe', db_column
='employe', related_name
='commentaires'
705 verbose_name
= u
"Employé commentaire"
706 verbose_name_plural
= u
"Employé commentaires"
708 reversion
.register(EmployeCommentaire
, format
='xml')
711 LIEN_PARENTE_CHOICES
= (
712 ('Conjoint', 'Conjoint'),
713 ('Conjointe', 'Conjointe'),
719 class AyantDroit(models
.Model
):
721 Personne en relation avec un Employe.
724 nom
= models
.CharField(max_length
=255)
725 prenom
= models
.CharField(u
"prénom", max_length
=255)
726 nom_affichage
= models
.CharField(
727 u
"nom d'affichage", max_length
=255, null
=True, blank
=True
729 nationalite
= models
.ForeignKey(
730 ref
.Pays
, to_field
='code', db_column
='nationalite',
731 related_name
='ayantdroits_nationalite',
732 verbose_name
=u
"nationalité", null
=True, blank
=True
734 date_naissance
= models
.DateField(
735 u
"Date de naissance", help_text
=HELP_TEXT_DATE
,
736 validators
=[validate_date_passee
], null
=True, blank
=True
738 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
741 employe
= models
.ForeignKey(
742 'Employe', db_column
='employe', related_name
='ayantdroits',
743 verbose_name
=u
"Employé"
745 lien_parente
= models
.CharField(
746 u
"lien de parenté", max_length
=10, choices
=LIEN_PARENTE_CHOICES
,
747 null
=True, blank
=True
752 verbose_name
= u
"Ayant droit"
753 verbose_name_plural
= u
"Ayants droit"
755 def __unicode__(self
):
756 return u
'%s %s' % (self
.nom
.upper(), self
.prenom
, )
758 prefix_implantation
= \
759 "employe__dossiers__poste__implantation__zone_administrative"
761 def get_zones_administratives(self
):
763 d
.poste
.implantation
.zone_administrative
764 for d
in self
.employe
.dossiers
.all()
767 reversion
.register(AyantDroit
, format
='xml', follow
=['commentaires'])
770 class AyantDroitCommentaire(Commentaire
):
771 ayant_droit
= models
.ForeignKey(
772 'AyantDroit', db_column
='ayant_droit', related_name
='commentaires'
775 reversion
.register(AyantDroitCommentaire
, format
='xml')
780 STATUT_RESIDENCE_CHOICES
= (
782 ('expat', 'Expatrié'),
785 COMPTE_COMPTA_CHOICES
= (
792 class Dossier_(DateActiviteMixin
, models
.Model
, DevisableMixin
,):
794 Le Dossier regroupe les informations relatives à l'occupation
795 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
798 Plusieurs Contrats peuvent être associés au Dossier.
799 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
800 lequel aucun Dossier n'existe est un poste vacant.
803 objects
= DossierManager()
806 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
807 organisme_bstg
= models
.ForeignKey(
808 'OrganismeBstg', db_column
='organisme_bstg', related_name
='+',
809 verbose_name
=u
"organisme",
811 u
"Si détaché (DET) ou mis à disposition (MAD), "
812 u
"préciser l'organisme."
813 ), null
=True, blank
=True
817 remplacement
= models
.BooleanField(default
=False)
818 remplacement_de
= models
.ForeignKey(
819 'self', related_name
='+', help_text
=u
"Taper le nom de l'employé",
820 null
=True, blank
=True
822 statut_residence
= models
.CharField(
823 u
"statut", max_length
=10, default
='local', null
=True,
824 choices
=STATUT_RESIDENCE_CHOICES
828 classement
= models
.ForeignKey(
829 'Classement', db_column
='classement', related_name
='+', null
=True,
832 regime_travail
= models
.DecimalField(
833 u
"régime de travail", max_digits
=12, null
=True, decimal_places
=2,
834 default
=REGIME_TRAVAIL_DEFAULT
, help_text
="% du temps complet"
836 regime_travail_nb_heure_semaine
= models
.DecimalField(
837 u
"nb. heures par semaine", max_digits
=12,
838 decimal_places
=2, null
=True,
839 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
840 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
843 # Occupation du Poste par cet Employe (anciennement "mandat")
844 date_debut
= models
.DateField(
845 u
"date de début d'occupation de poste", db_index
=True
847 date_fin
= models
.DateField(
848 u
"Date de fin d'occupation de poste", null
=True, blank
=True,
853 est_cadre
= models
.BooleanField(
859 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
860 verbose_name
=u
'Compte comptabilité',
861 choices
=COMPTE_COMPTA_CHOICES
)
862 compte_courriel
= models
.BooleanField()
866 ordering
= ['employe__nom', ]
867 verbose_name
= u
"Dossier"
868 verbose_name_plural
= "Dossiers"
870 def salaire_theorique(self
):
871 annee
= date
.today().year
872 coeff
= self
.classement
.coefficient
873 implantation
= self
.poste
.implantation
874 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
876 montant
= coeff
* point
.valeur
877 devise
= point
.devise
878 return {'montant': montant
, 'devise': devise
}
880 def __unicode__(self
):
881 poste
= self
.poste
.nom
882 if self
.employe
.genre
== 'F':
883 poste
= self
.poste
.nom_feminin
884 return u
'%s - %s' % (self
.employe
, poste
)
886 prefix_implantation
= "poste__implantation__zone_administrative"
888 def get_zones_administratives(self
):
889 return [self
.poste
.implantation
.zone_administrative
]
891 def remunerations(self
):
892 key
= "%s_remunerations" % self
._meta
.app_label
893 remunerations
= getattr(self
, key
)
894 return remunerations
.all().order_by('-date_debut')
896 def remunerations_en_cours(self
):
897 q
= Q(date_fin__exact
=None) |
Q(date_fin__gt
=datetime
.date
.today())
898 return self
.remunerations().all().filter(q
).order_by('date_debut')
900 def get_salaire(self
):
902 return [r
for r
in self
.remunerations().order_by('-date_debut')
903 if r
.type_id
== 1][0]
907 def get_salaire_euros(self
):
908 tx
= self
.taux_devise()
909 return (float)(tx
) * (float)(self
.salaire
)
911 def get_remunerations_brutes(self
):
915 4 Indemnité d'expatriation
916 5 Indemnité pour frais
917 6 Indemnité de logement
918 7 Indemnité de fonction
919 8 Indemnité de responsabilité
920 9 Indemnité de transport
921 10 Indemnité compensatrice
922 11 Indemnité de subsistance
923 12 Indemnité différentielle
924 13 Prime d'installation
927 16 Indemnité de départ
928 18 Prime de 13ième mois
931 ids
= [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
932 return [r
for r
in self
.remunerations_en_cours().all()
935 def get_charges_salariales(self
):
937 20 Charges salariales ?
940 return [r
for r
in self
.remunerations_en_cours().all()
943 def get_charges_patronales(self
):
945 17 Charges patronales
948 return [r
for r
in self
.remunerations_en_cours().all()
951 def get_remunerations_tierces(self
):
955 return [r
for r
in self
.remunerations_en_cours().all()
956 if r
.type_id
in (2,)]
960 def get_total_local_charges_salariales(self
):
961 devise
= self
.poste
.get_devise()
963 for r
in self
.get_charges_salariales():
964 if r
.devise
!= devise
:
966 total
+= float(r
.montant
)
969 def get_total_local_charges_patronales(self
):
970 devise
= self
.poste
.get_devise()
972 for r
in self
.get_charges_patronales():
973 if r
.devise
!= devise
:
975 total
+= float(r
.montant
)
978 def get_local_salaire_brut(self
):
980 somme des rémuérations brutes
982 devise
= self
.poste
.get_devise()
984 for r
in self
.get_remunerations_brutes():
985 if r
.devise
!= devise
:
987 total
+= float(r
.montant
)
990 def get_local_salaire_net(self
):
992 salaire brut - charges salariales
994 devise
= self
.poste
.get_devise()
996 for r
in self
.get_charges_salariales():
997 if r
.devise
!= devise
:
999 total_charges
+= float(r
.montant
)
1000 return self
.get_local_salaire_brut() - total_charges
1002 def get_local_couts_auf(self
):
1004 salaire net + charges patronales
1006 devise
= self
.poste
.get_devise()
1008 for r
in self
.get_charges_patronales():
1009 if r
.devise
!= devise
:
1011 total_charges
+= float(r
.montant
)
1012 return self
.get_local_salaire_net() + total_charges
1014 def get_total_local_remunerations_tierces(self
):
1015 devise
= self
.poste
.get_devise()
1017 for r
in self
.get_remunerations_tierces():
1018 if r
.devise
!= devise
:
1020 total
+= float(r
.montant
)
1025 def get_total_charges_salariales(self
):
1027 for r
in self
.get_charges_salariales():
1028 total
+= r
.montant_euros()
1031 def get_total_charges_patronales(self
):
1033 for r
in self
.get_charges_patronales():
1034 total
+= r
.montant_euros()
1037 def get_salaire_brut(self
):
1039 somme des rémuérations brutes
1042 for r
in self
.get_remunerations_brutes():
1043 total
+= r
.montant_euros()
1046 def get_salaire_net(self
):
1048 salaire brut - charges salariales
1051 for r
in self
.get_charges_salariales():
1052 total_charges
+= r
.montant_euros()
1053 return self
.get_salaire_brut() - total_charges
1055 def get_couts_auf(self
):
1057 salaire net + charges patronales
1060 for r
in self
.get_charges_patronales():
1061 total_charges
+= r
.montant_euros()
1062 return self
.get_salaire_net() + total_charges
1064 def get_total_remunerations_tierces(self
):
1066 for r
in self
.get_remunerations_tierces():
1067 total
+= r
.montant_euros()
1070 def premier_contrat(self
):
1071 """contrat avec plus petite date de début"""
1073 contrat
= self
.rh_contrats
.exclude(date_debut
=None) \
1074 .order_by('date_debut')[0]
1075 except IndexError, Contrat
.DoesNotExist
:
1079 def dernier_contrat(self
):
1080 """contrat avec plus grande date de fin"""
1082 contrat
= self
.rh_contrats
.exclude(date_debut
=None) \
1083 .order_by('-date_debut')[0]
1084 except IndexError, Contrat
.DoesNotExist
:
1089 today
= date
.today()
1090 return (self
.date_debut
is None or self
.date_debut
<= today
) \
1091 and (self
.date_fin
is None or self
.date_fin
>= today
) \
1092 and not (self
.date_fin
is None and self
.date_debut
is None)
1095 class Dossier(Dossier_
):
1096 __doc__
= Dossier_
.__doc__
1097 poste
= models
.ForeignKey(
1098 Poste
, db_column
='poste', related_name
='rh_dossiers',
1099 help_text
=u
"Taper le nom du poste ou du type de poste",
1101 employe
= models
.ForeignKey(
1102 'Employe', db_column
='employe',
1103 help_text
=u
"Taper le nom de l'employé",
1104 related_name
='rh_dossiers', verbose_name
=u
"employé"
1106 principal
= models
.BooleanField(
1107 u
"dossier principal", default
=True,
1109 u
"Ce dossier est pour le principal poste occupé par l'employé"
1114 reversion
.register(Dossier
, format
='xml', follow
=[
1115 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
1116 'rh_contrats', 'commentaires'
1120 class RHDossierClassementRecord(models
.Model
):
1121 classement
= models
.ForeignKey(
1123 related_name
='classement_records',
1125 dossier
= models
.ForeignKey(
1127 related_name
='classement_records',
1129 date_debut
= models
.DateField(
1131 help_text
=HELP_TEXT_DATE
,
1136 date_fin
= models
.DateField(
1138 help_text
=HELP_TEXT_DATE
,
1144 def __unicode__(self
):
1145 return self
.classement
.__unicode__()
1148 verbose_name
= u
"Element d'historique de classement"
1149 verbose_name_plural
= u
"Historique de classement"
1152 def post_save_handler(cls
,
1159 today
= date
.today()
1160 previous_record
= None
1161 previous_classement
= None
1164 # Premièrement, pour les nouvelles instances:
1166 if not instance
.classement
:
1170 date_debut
=instance
.date_debut
,
1171 classement
=instance
.classement
,
1176 # Deuxièmement, pour les instances existantes:
1179 # 1. Est-ce que le classement a changé?
1180 # 2. Est-ce qu'une historique de classement existe déjà
1182 previous_record
= cls
.objects
.get(
1184 classement
=instance
.before_save
.classement
,
1187 except cls
.DoesNotExist
:
1188 if instance
.before_save
.classement
:
1189 # Il était censé avoir une historique de classement
1191 previous_record
= cls
.objects
.create(
1192 date_debut
=instance
.before_save
.date_debut
,
1193 classement
=instance
.before_save
.classement
,
1196 previous_classement
= instance
.before_save
.classement
1197 except MultipleObjectsReturned
:
1198 qs
= cls
.objects
.filter(
1200 classement
=instance
.before_save
.classement
,
1203 latest
= qs
.latest('date_debut')
1204 qs
.exclude(id=latest
.id).update(date_fin
=today
)
1205 previous_record
= latest
1206 previous_classement
= latest
.classement
1208 previous_classement
= previous_record
.classement
1211 instance
.classement
!=
1215 # Cas aucun changement:
1220 # Classement a changé
1222 previous_record
.date_fin
= today
1223 previous_record
.save()
1225 if instance
.classement
:
1228 classement
=instance
.classement
,
1233 class DossierPiece_(models
.Model
):
1235 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
1236 Ex.: Lettre de motivation.
1238 nom
= models
.CharField(max_length
=255)
1239 fichier
= models
.FileField(
1240 upload_to
=dossier_piece_dispatch
, storage
=storage_prive
1247 def __unicode__(self
):
1248 return u
'%s' % (self
.nom
)
1251 class DossierPiece(DossierPiece_
):
1252 dossier
= models
.ForeignKey(
1253 Dossier
, db_column
='dossier', related_name
='rh_dossierpieces'
1256 reversion
.register(DossierPiece
, format
='xml')
1258 class DossierCommentaire(Commentaire
):
1259 dossier
= models
.ForeignKey(
1260 Dossier
, db_column
='dossier', related_name
='commentaires'
1263 reversion
.register(DossierCommentaire
, format
='xml')
1266 class DossierComparaison_(models
.Model
, DevisableMixin
):
1268 Photo d'une comparaison salariale au moment de l'embauche.
1270 objects
= DossierComparaisonManager()
1272 implantation
= models
.ForeignKey(
1273 ref
.Implantation
, related_name
="+", null
=True, blank
=True
1275 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
1276 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
1277 montant
= models
.IntegerField(null
=True)
1278 devise
= models
.ForeignKey(
1279 'Devise', related_name
='+', null
=True, blank
=True
1285 def __unicode__(self
):
1286 return "%s (%s)" % (self
.poste
, self
.personne
)
1289 class DossierComparaison(DossierComparaison_
):
1290 dossier
= models
.ForeignKey(
1291 Dossier
, related_name
='rh_comparaisons'
1294 reversion
.register(DossierComparaison
, format
='xml')
1299 class RemunerationMixin(models
.Model
):
1302 type = models
.ForeignKey(
1303 'TypeRemuneration', db_column
='type', related_name
='+',
1304 verbose_name
=u
"type de rémunération"
1306 type_revalorisation
= models
.ForeignKey(
1307 'TypeRevalorisation', db_column
='type_revalorisation',
1308 related_name
='+', verbose_name
=u
"type de revalorisation",
1309 null
=True, blank
=True
1311 montant
= models
.DecimalField(
1312 null
=True, blank
=True, max_digits
=12, decimal_places
=2
1313 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1314 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+')
1316 # commentaire = precision
1317 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
1319 # date_debut = anciennement date_effectif
1320 date_debut
= models
.DateField(
1321 u
"date de début", null
=True, blank
=True, db_index
=True
1323 date_fin
= models
.DateField(
1324 u
"date de fin", null
=True, blank
=True, db_index
=True
1329 ordering
= ['type__nom', '-date_fin']
1331 def __unicode__(self
):
1332 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
1335 class Remuneration_(RemunerationMixin
, DevisableMixin
):
1337 Structure de rémunération (données budgétaires) en situation normale
1338 pour un Dossier. Si un Evenement existe, utiliser la structure de
1339 rémunération EvenementRemuneration de cet événement.
1341 objects
= RemunerationManager()
1344 def find_yearly_range(from_date
, to_date
, year
):
1345 today
= date
.today()
1346 year
= year
or date
.today().year
1347 year_start
= date(year
, 1, 1)
1348 year_end
= date(year
, 12, 31)
1350 def constrain_to_year(*dates
):
1352 S'assure que les dates soient dans le range year_start a
1355 return [min(max(year_start
, d
), year_end
)
1359 from_date
or year_start
, year_start
)
1361 to_date
or year_end
, year_end
)
1363 start_date
, end_date
= constrain_to_year(start_date
, end_date
)
1365 jours_annee
= (year_end
- year_start
).days
1366 jours_dates
= (end_date
- start_date
).days
1367 factor
= Decimal(str(jours_dates
)) / Decimal(str(jours_annee
))
1369 return start_date
, end_date
, factor
1372 def montant_ajuste_euros(self
, annee
=None):
1374 Le montant ajusté représente le montant annuel, ajusté sur la
1375 période de temps travaillée, multipliée par le ratio de temps
1376 travaillé (en rapport au temps plein).
1378 date_debut
, date_fin
, factor
= self
.find_yearly_range(
1384 montant_euros
= Decimal(str(self
.montant_euros_float()) or '0')
1386 if self
.type.nature_remuneration
!= u
'Accessoire':
1387 dossier
= getattr(self
, 'dossier', None)
1390 Dans le cas d'un DossierComparaisonRemuneration, il
1391 n'y a plus de reference au dossier.
1393 regime_travail
= REGIME_TRAVAIL_DEFAULT
1395 regime_travail
= self
.dossier
.regime_travail
1396 return (montant_euros
* factor
*
1397 regime_travail
/ 100)
1399 return montant_euros
1401 def montant_mois(self
):
1402 return round(self
.montant
/ 12, 2)
1404 def montant_avec_regime(self
):
1405 return round(self
.montant
* (self
.dossier
.regime_travail
/ 100), 2)
1407 def montant_euro_mois(self
):
1408 return round(self
.montant_euros() / 12, 2)
1410 def __unicode__(self
):
1412 devise
= self
.devise
.code
1415 return "%s %s" % (self
.montant
, devise
)
1419 verbose_name
= u
"Rémunération"
1420 verbose_name_plural
= u
"Rémunérations"
1423 class Remuneration(Remuneration_
):
1424 dossier
= models
.ForeignKey(
1425 Dossier
, db_column
='dossier', related_name
='rh_remunerations'
1428 reversion
.register(Remuneration
, format
='xml')
1433 class Contrat_(models
.Model
):
1435 Document juridique qui encadre la relation de travail d'un Employe
1436 pour un Poste particulier. Pour un Dossier (qui documente cette
1437 relation de travail) plusieurs contrats peuvent être associés.
1439 objects
= ContratManager()
1440 type_contrat
= models
.ForeignKey(
1441 'TypeContrat', db_column
='type_contrat',
1442 verbose_name
=u
'type de contrat', related_name
='+'
1444 date_debut
= models
.DateField(
1445 u
"date de début", db_index
=True
1447 date_fin
= models
.DateField(
1448 u
"date de fin", null
=True, blank
=True, db_index
=True
1450 fichier
= models
.FileField(
1451 upload_to
=contrat_dispatch
, storage
=storage_prive
, null
=True,
1457 ordering
= ['dossier__employe__nom']
1458 verbose_name
= u
"Contrat"
1459 verbose_name_plural
= u
"Contrats"
1461 def __unicode__(self
):
1462 return u
'%s - %s' % (self
.dossier
, self
.id)
1465 class Contrat(Contrat_
):
1466 dossier
= models
.ForeignKey(
1467 Dossier
, db_column
='dossier', related_name
='rh_contrats'
1470 reversion
.register(Contrat
, format
='xml')
1475 class CategorieEmploi(models
.Model
):
1477 Catégorie utilisée dans la gestion des Postes.
1478 Catégorie supérieure à TypePoste.
1480 nom
= models
.CharField(max_length
=255)
1484 verbose_name
= u
"catégorie d'emploi"
1485 verbose_name_plural
= u
"catégories d'emploi"
1487 def __unicode__(self
):
1490 reversion
.register(CategorieEmploi
, format
='xml')
1493 class FamilleProfessionnelle(models
.Model
):
1495 Famille professionnelle d'un poste.
1497 nom
= models
.CharField(max_length
=100)
1501 verbose_name
= u
'famille professionnelle'
1502 verbose_name_plural
= u
'familles professionnelles'
1504 def __unicode__(self
):
1507 reversion
.register(FamilleProfessionnelle
, format
='xml')
1510 class TypePoste(Archivable
):
1514 nom
= models
.CharField(max_length
=255)
1515 nom_feminin
= models
.CharField(u
"nom féminin", max_length
=255)
1516 is_responsable
= models
.BooleanField(
1517 u
"poste de responsabilité", default
=False
1519 categorie_emploi
= models
.ForeignKey(
1520 CategorieEmploi
, db_column
='categorie_emploi', related_name
='+',
1521 verbose_name
=u
"catégorie d'emploi"
1523 famille_professionnelle
= models
.ForeignKey(
1524 FamilleProfessionnelle
, related_name
='types_de_poste',
1525 verbose_name
=u
"famille professionnelle", blank
=True, null
=True
1530 verbose_name
= u
"Type de poste"
1531 verbose_name_plural
= u
"Types de poste"
1533 def __unicode__(self
):
1534 return u
'%s' % (self
.nom
)
1536 reversion
.register(TypePoste
, format
='xml')
1539 TYPE_PAIEMENT_CHOICES
= (
1540 (u
'Régulier', u
'Régulier'),
1541 (u
'Ponctuel', u
'Ponctuel'),
1544 NATURE_REMUNERATION_CHOICES
= (
1545 (u
'Traitement', u
'Traitements'),
1546 (u
'Indemnité', u
'Indemnités autres'),
1547 (u
'Charges', u
'Charges patronales'),
1548 (u
'Accessoire', u
'Accessoires'),
1549 (u
'RAS', u
'Rémunération autre source'),
1553 class TypeRemuneration(Archivable
):
1555 Catégorie de Remuneration.
1558 objects
= models
.Manager()
1559 sans_archives
= ArchivableManager()
1561 nom
= models
.CharField(max_length
=255)
1562 type_paiement
= models
.CharField(
1563 u
"type de paiement", max_length
=30, choices
=TYPE_PAIEMENT_CHOICES
1566 nature_remuneration
= models
.CharField(
1567 u
"nature de la rémunération", max_length
=30,
1568 choices
=NATURE_REMUNERATION_CHOICES
1573 verbose_name
= u
"Type de rémunération"
1574 verbose_name_plural
= u
"Types de rémunération"
1576 def __unicode__(self
):
1579 reversion
.register(TypeRemuneration
, format
='xml')
1582 class TypeRevalorisation(Archivable
):
1584 Justification du changement de la Remuneration.
1585 (Actuellement utilisé dans aucun traitement informatique.)
1587 nom
= models
.CharField(max_length
=255)
1591 verbose_name
= u
"Type de revalorisation"
1592 verbose_name_plural
= u
"Types de revalorisation"
1594 def __unicode__(self
):
1595 return u
'%s' % (self
.nom
)
1597 reversion
.register(TypeRevalorisation
, format
='xml')
1600 class Service(Archivable
):
1602 Unité administrative où les Postes sont rattachés.
1604 nom
= models
.CharField(max_length
=255)
1608 verbose_name
= u
"service"
1609 verbose_name_plural
= u
"services"
1611 def __unicode__(self
):
1614 reversion
.register(Service
, format
='xml')
1617 TYPE_ORGANISME_CHOICES
= (
1618 ('MAD', 'Mise à disposition'),
1619 ('DET', 'Détachement'),
1623 class OrganismeBstg(models
.Model
):
1625 Organisation d'où provient un Employe mis à disposition (MAD) de
1626 ou détaché (DET) à l'AUF à titre gratuit.
1628 (BSTG = bien et service à titre gratuit.)
1630 nom
= models
.CharField(max_length
=255)
1631 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1632 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1634 related_name
='organismes_bstg',
1635 null
=True, blank
=True)
1638 ordering
= ['type', 'nom']
1639 verbose_name
= u
"Organisme BSTG"
1640 verbose_name_plural
= u
"Organismes BSTG"
1642 def __unicode__(self
):
1643 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1645 reversion
.register(OrganismeBstg
, format
='xml')
1648 class Statut(Archivable
):
1650 Statut de l'Employe dans le cadre d'un Dossier particulier.
1653 code
= models
.CharField(
1654 max_length
=25, unique
=True,
1656 u
"Saisir un code court mais lisible pour ce statut : "
1657 u
"le code est utilisé pour associer les statuts aux autres "
1658 u
"données tout en demeurant plus lisible qu'un identifiant "
1662 nom
= models
.CharField(max_length
=255)
1666 verbose_name
= u
"Statut d'employé"
1667 verbose_name_plural
= u
"Statuts d'employé"
1669 def __unicode__(self
):
1670 return u
'%s : %s' % (self
.code
, self
.nom
)
1672 reversion
.register(Statut
, format
='xml')
1675 TYPE_CLASSEMENT_CHOICES
= (
1676 ('S', 'S -Soutien'),
1677 ('T', 'T - Technicien'),
1678 ('P', 'P - Professionel'),
1680 ('D', 'D - Direction'),
1681 ('SO', 'SO - Sans objet [expatriés]'),
1682 ('HG', 'HG - Hors grille [direction]'),
1686 class ClassementManager(models
.Manager
):
1688 Ordonner les spcéfiquement les classements.
1690 def get_query_set(self
):
1691 qs
= super(ClassementManager
, self
).get_query_set()
1692 qs
= qs
.extra(select
={
1693 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1695 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1699 class ClassementArchivableManager(ClassementManager
,
1704 class Classement_(Archivable
):
1706 Éléments de classement de la
1707 "Grille générique de classement hiérarchique".
1709 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1710 classement dans la grille. Le classement donne le coefficient utilisé dans:
1712 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1714 objects
= ClassementManager()
1715 sans_archives
= ClassementArchivableManager()
1718 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1719 echelon
= models
.IntegerField(u
"échelon", blank
=True, default
=0)
1720 degre
= models
.IntegerField(u
"degré", blank
=True, default
=0)
1721 coefficient
= models
.FloatField(u
"coefficient", blank
=True, null
=True)
1724 # annee # au lieu de date_debut et date_fin
1725 commentaire
= models
.TextField(null
=True, blank
=True)
1729 ordering
= ['type', 'echelon', 'degre', 'coefficient']
1730 verbose_name
= u
"Classement"
1731 verbose_name_plural
= u
"Classements"
1733 def __unicode__(self
):
1734 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1737 class Classement(Classement_
):
1738 __doc__
= Classement_
.__doc__
1740 reversion
.register(Classement
, format
='xml')
1743 class TauxChange_(models
.Model
):
1745 Taux de change de la devise vers l'euro (EUR)
1746 pour chaque année budgétaire.
1749 devise
= models
.ForeignKey('Devise', db_column
='devise')
1750 annee
= models
.IntegerField(u
"année")
1751 taux
= models
.FloatField(u
"taux vers l'euro")
1755 ordering
= ['-annee', 'devise__code']
1756 verbose_name
= u
"Taux de change"
1757 verbose_name_plural
= u
"Taux de change"
1758 unique_together
= ('devise', 'annee')
1760 def __unicode__(self
):
1761 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1764 class TauxChange(TauxChange_
):
1765 __doc__
= TauxChange_
.__doc__
1767 reversion
.register(TauxChange
, format
='xml')
1770 class ValeurPointManager(models
.Manager
):
1772 def get_query_set(self
):
1773 return super(ValeurPointManager
, self
).get_query_set() \
1774 .select_related('devise', 'implantation')
1777 class ValeurPoint_(models
.Model
):
1779 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1780 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1781 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1783 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1786 objects
= models
.Manager()
1787 actuelles
= ValeurPointManager()
1789 valeur
= models
.FloatField(null
=True)
1790 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1791 implantation
= models
.ForeignKey(ref
.Implantation
,
1792 db_column
='implantation',
1793 related_name
='%(app_label)s_valeur_point')
1795 annee
= models
.IntegerField()
1798 ordering
= ['-annee', 'implantation__nom']
1800 verbose_name
= u
"Valeur du point"
1801 verbose_name_plural
= u
"Valeurs du point"
1802 unique_together
= ('implantation', 'annee')
1804 def __unicode__(self
):
1805 return u
'%s %s %s [%s] %s' % (
1806 self
.devise
.code
, self
.annee
, self
.valeur
,
1807 self
.implantation
.nom_court
, self
.devise
.nom
1811 class ValeurPoint(ValeurPoint_
):
1812 __doc__
= ValeurPoint_
.__doc__
1814 reversion
.register(ValeurPoint
, format
='xml')
1817 class Devise(Archivable
):
1821 code
= models
.CharField(max_length
=10, unique
=True)
1822 nom
= models
.CharField(max_length
=255)
1826 verbose_name
= u
"devise"
1827 verbose_name_plural
= u
"devises"
1829 def __unicode__(self
):
1830 return u
'%s - %s' % (self
.code
, self
.nom
)
1832 reversion
.register(Devise
, format
='xml')
1835 class TypeContrat(Archivable
):
1839 nom
= models
.CharField(max_length
=255)
1840 nom_long
= models
.CharField(max_length
=255)
1844 verbose_name
= u
"Type de contrat"
1845 verbose_name_plural
= u
"Types de contrat"
1847 def __unicode__(self
):
1848 return u
'%s' % (self
.nom
)
1850 reversion
.register(TypeContrat
, format
='xml')
1855 class ResponsableImplantationProxy(ref
.Implantation
):
1863 verbose_name
= u
"Responsable d'implantation"
1864 verbose_name_plural
= u
"Responsables d'implantation"
1867 class ResponsableImplantation(models
.Model
):
1869 Le responsable d'une implantation.
1870 Anciennement géré sur le Dossier du responsable.
1872 employe
= models
.ForeignKey(
1873 'Employe', db_column
='employe', related_name
='+', null
=True,
1876 implantation
= models
.OneToOneField(
1877 "ResponsableImplantationProxy", db_column
='implantation',
1878 related_name
='responsable', unique
=True
1881 def __unicode__(self
):
1882 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1885 ordering
= ['implantation__nom']
1886 verbose_name
= "Responsable d'implantation"
1887 verbose_name_plural
= "Responsables d'implantation"
1889 reversion
.register(ResponsableImplantation
, format
='xml')
1892 class UserProfile(models
.Model
):
1893 user
= models
.OneToOneField(User
, related_name
='profile')
1894 zones_administratives
= models
.ManyToManyField(
1895 ref
.ZoneAdministrative
,
1896 related_name
='profiles'
1899 verbose_name
= "Permissions sur zones administratives"
1900 verbose_name_plural
= "Permissions sur zones administratives"
1902 def __unicode__(self
):
1903 return self
.user
.__unicode__()
1905 reversion
.register(UserProfile
, format
='xml')
1909 TYPES_CHANGEMENT
= (
1916 class ChangementPersonnelNotifications(models
.Model
):
1918 verbose_name
= u
"Destinataire pour notices de mouvement de personnel"
1919 verbose_name_plural
= u
"Destinataires pour notices de mouvement de personnel"
1921 type = models
.CharField(
1923 choices
= TYPES_CHANGEMENT
,
1927 destinataires
= models
.ManyToManyField(
1929 related_name
='changement_notifications',
1932 def __unicode__(self
):
1934 self
.get_type_display(), ','.join(
1935 self
.destinataires
.all().values_list(
1936 'courriel', flat
=True))
1940 class ChangementPersonnel(models
.Model
):
1942 Une notice qui enregistre un changement de personnel, incluant:
1945 * Mouvement de personnel
1950 verbose_name
= u
"Mouvement de personnel"
1951 verbose_name_plural
= u
"Mouvements de personnel"
1953 def __unicode__(self
):
1954 return '%s: %s' % (self
.dossier
.__unicode__(),
1955 self
.get_type_display())
1958 def create_changement(cls
, dossier
, type):
1959 # If this employe has existing Changement, set them to invalid.
1960 cls
.objects
.filter(dossier__employe
=dossier
.employe
).update(valide
=False)
1972 def post_save_handler(cls
,
1979 # This defines the time limit used when checking in previous
1980 # files to see if an employee if new. Basically, if emloyee
1981 # left his position new_file.date_debut -
1982 # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then
1983 # if a new file is created for this employee, he will bec
1984 # onsidered "NEW" and a notice will be created to this effect.
1985 NEW_EMPLOYE_THRESHOLD
= datetime
.timedelta(7) # 7 days.
1987 other_dossier_qs
= instance
.employe
.rh_dossiers
.exclude(
1989 dd
= instance
.date_debut
1990 df
= instance
.date_fin
1992 # Here, verify differences between the instance, before and
1994 df_has_changed
= False
1998 df_has_changed
= True
2000 df_has_changed
= (df
!= instance
.before_save
.date_fin
and
2006 # Date de fin est None et c'est une nouvelle instance de
2008 if not df
and created
:
2009 # QS for finding other dossiers with a date_fin of None OR
2010 # with a date_fin >= to this dossier's date_debut
2011 exists_recent_file_qs
= other_dossier_qs
.filter(
2012 Q(date_fin__isnull
=True) |
2013 Q(date_fin__gte
=dd
- NEW_EMPLOYE_THRESHOLD
)
2016 # 1. If existe un Dossier récent, et c'est une nouvelle
2017 # instance de Dossier:
2018 if exists_recent_file_qs
.count() > 0:
2019 cls
.create_changement(
2023 # 2. Il n'existe un Dossier récent, et c'est une nouvelle
2024 # instance de Dossier:
2026 cls
.create_changement(
2032 # Date de fin a été modifiée:
2034 # QS for other active files (date_fin == None), excludes
2036 exists_active_files_qs
= other_dossier_qs
.filter(
2037 Q(date_fin__isnull
=True))
2039 # 3. Date de fin a été modifiée et il n'existe aucun autre
2040 # dossier actifs: Depart
2041 if exists_active_files_qs
.count() == 0:
2042 cls
.create_changement(
2046 # 4. Dossier a une nouvelle date de fin par contre
2047 # d'autres dossiers actifs existent déjà: Mouvement
2049 cls
.create_changement(
2055 dossier
= models
.ForeignKey(
2057 related_name
='mouvements',
2060 valide
= models
.BooleanField(default
=True)
2061 date_creation
= models
.DateTimeField(
2063 communique
= models
.BooleanField(
2067 date_communication
= models
.DateTimeField(
2072 type = models
.CharField(
2074 choices
= TYPES_CHANGEMENT
,
2077 reversion
.register(ChangementPersonnel
, format
='xml')
2080 def dossier_pre_save_handler(sender
,
2084 # Store a copy of the model before save is called.
2085 if instance
.pk
is not None:
2086 instance
.before_save
= Dossier
.objects
.get(pk
=instance
.pk
)
2088 instance
.before_save
= None
2091 # Connect a pre_save handler that assigns a copy of the model as an
2092 # attribute in order to compare it in post_save.
2093 pre_save
.connect(dossier_pre_save_handler
, sender
=Dossier
)
2095 post_save
.connect(ChangementPersonnel
.post_save_handler
, sender
=Dossier
)
2096 post_save
.connect(RHDossierClassementRecord
.post_save_handler
, sender
=Dossier
)