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
.db
import models
14 from django
.db
.models
import Q
15 from django
.db
.models
.signals
import post_save
, pre_save
16 from django
.conf
import settings
18 from project
.rh
.change_list
import \
19 RechercheTemporelle
, KEY_STATUT
, STATUT_ACTIF
, STATUT_INACTIF
, \
21 from project
import groups
22 from project
.rh
.managers
import (
26 DossierComparaisonManager
,
27 PosteComparaisonManager
,
34 TWOPLACES
= Decimal('0.01')
36 from project
.rh
.validators
import validate_date_passee
38 # import pour relocaliser le modèle selon la convention (models.py pour
40 from project
.rh
.historique
import ModificationTraite
43 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
44 REGIME_TRAVAIL_DEFAULT
= Decimal('100.00')
45 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
= Decimal('35.00')
46 REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
= \
47 "Saisir le nombre d'heure de travail à temps complet (100%), " \
48 "sans tenir compte du régime de travail"
51 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
52 base_url
=settings
.PRIVE_MEDIA_URL
)
55 class RemunIntegrityException(Exception):
58 def poste_piece_dispatch(instance
, filename
):
59 path
= "%s/poste/%s/%s" % (
60 instance
._meta
.app_label
, instance
.poste_id
, filename
65 def dossier_piece_dispatch(instance
, filename
):
66 path
= "%s/dossier/%s/%s" % (
67 instance
._meta
.app_label
, instance
.dossier_id
, filename
72 def employe_piece_dispatch(instance
, filename
):
73 path
= "%s/employe/%s/%s" % (
74 instance
._meta
.app_label
, instance
.employe_id
, filename
79 def contrat_dispatch(instance
, filename
):
80 path
= "%s/contrat/%s/%s" % (
81 instance
._meta
.app_label
, instance
.dossier_id
, filename
86 class DateActiviteMixin(models
.Model
):
88 Mixin pour mettre à jour l'activité d'un modèle
92 date_creation
= models
.DateTimeField(auto_now_add
=True,
93 null
=True, blank
=True,
94 verbose_name
=u
"Date de création",)
95 date_modification
= models
.DateTimeField(auto_now
=True,
96 null
=True, blank
=True,
97 verbose_name
=u
"Date de modification",)
100 class Archivable(models
.Model
):
101 archive
= models
.BooleanField(u
'archivé', default
=False)
103 objects
= ArchivableManager()
104 avec_archives
= models
.Manager()
110 class DevisableMixin(object):
112 def get_annee_pour_taux_devise(self
):
113 return datetime
.datetime
.now().year
115 def taux_devise(self
, devise
=None):
121 if devise
.code
== "EUR":
124 annee
= self
.get_annee_pour_taux_devise()
125 taux
= TauxChange
.objects
.filter(devise
=devise
, annee__lte
=annee
) \
129 def montant_euros_float(self
):
131 taux
= self
.taux_devise()
136 return float(self
.montant
) * float(taux
)
138 def montant_euros(self
):
139 return int(round(self
.montant_euros_float(), 2))
142 class Commentaire(models
.Model
):
143 texte
= models
.TextField()
144 owner
= models
.ForeignKey(
145 'auth.User', db_column
='owner', related_name
='+',
146 verbose_name
=u
"Commentaire de"
148 date_creation
= models
.DateTimeField(
149 u
'date', auto_now_add
=True, blank
=True, null
=True
154 ordering
= ['-date_creation']
156 def __unicode__(self
):
157 return u
'%s' % (self
.texte
)
162 POSTE_APPEL_CHOICES
= (
163 ('interne', 'Interne'),
164 ('externe', 'Externe'),
168 class Poste_( DateActiviteMixin
, models
.Model
,):
170 Un Poste est un emploi (job) à combler dans une implantation.
171 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
172 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
175 objects
= PosteManager()
178 nom
= models
.CharField(u
"Titre du poste", max_length
=255)
179 nom_feminin
= models
.CharField(
180 u
"Titre du poste (au féminin)", max_length
=255, null
=True
182 implantation
= models
.ForeignKey(
184 help_text
=u
"Taper le nom de l'implantation ou sa région",
185 db_column
='implantation', related_name
='+'
187 type_poste
= models
.ForeignKey(
188 'TypePoste', db_column
='type_poste',
189 help_text
=u
"Taper le nom du type de poste", related_name
='+',
190 null
=True, verbose_name
=u
"type de poste"
192 service
= models
.ForeignKey(
193 'Service', db_column
='service', related_name
='%(app_label)s_postes',
194 verbose_name
=u
"direction/service/pôle support", null
=True
196 responsable
= models
.ForeignKey(
197 'Poste', db_column
='responsable',
198 related_name
='+', null
=True,
199 help_text
=u
"Taper le nom du poste ou du type de poste",
200 verbose_name
=u
"Poste du responsable"
204 regime_travail
= models
.DecimalField(
205 u
"temps de travail", max_digits
=12, decimal_places
=2,
206 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
207 help_text
="% du temps complet"
209 regime_travail_nb_heure_semaine
= models
.DecimalField(
210 u
"nb. heures par semaine", max_digits
=12, decimal_places
=2,
211 null
=True, default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
212 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
216 local
= models
.NullBooleanField(
217 u
"local", default
=True, null
=True, blank
=True
219 expatrie
= models
.NullBooleanField(
220 u
"expatrié", default
=False, null
=True, blank
=True
222 mise_a_disposition
= models
.NullBooleanField(
223 u
"mise à disposition", null
=True, default
=False
225 appel
= models
.CharField(
226 u
"Appel à candidature", max_length
=10, null
=True,
227 choices
=POSTE_APPEL_CHOICES
, default
='interne'
231 classement_min
= models
.ForeignKey(
232 'Classement', db_column
='classement_min', related_name
='+',
233 null
=True, blank
=True
235 classement_max
= models
.ForeignKey(
236 'Classement', db_column
='classement_max', related_name
='+',
237 null
=True, blank
=True
239 valeur_point_min
= models
.ForeignKey(
241 help_text
=u
"Taper le code ou le nom de l'implantation",
242 db_column
='valeur_point_min', related_name
='+', null
=True,
245 valeur_point_max
= models
.ForeignKey(
247 help_text
=u
"Taper le code ou le nom de l'implantation",
248 db_column
='valeur_point_max', related_name
='+', null
=True,
251 devise_min
= models
.ForeignKey(
252 'Devise', db_column
='devise_min', null
=True, related_name
='+'
254 devise_max
= models
.ForeignKey(
255 'Devise', db_column
='devise_max', null
=True, related_name
='+'
257 salaire_min
= models
.DecimalField(
258 max_digits
=12, decimal_places
=2, default
=0,
260 salaire_max
= models
.DecimalField(
261 max_digits
=12, decimal_places
=2, default
=0,
263 indemn_min
= models
.DecimalField(
264 max_digits
=12, decimal_places
=2, default
=0,
266 indemn_max
= models
.DecimalField(
267 max_digits
=12, decimal_places
=2, default
=0,
269 autre_min
= models
.DecimalField(
270 max_digits
=12, decimal_places
=2, default
=0,
272 autre_max
= models
.DecimalField(
273 max_digits
=12, decimal_places
=2, default
=0,
276 # Comparatifs de rémunération
277 devise_comparaison
= models
.ForeignKey(
278 'Devise', null
=True, blank
=True, db_column
='devise_comparaison',
281 comp_locale_min
= models
.DecimalField(
282 max_digits
=12, decimal_places
=2, null
=True, blank
=True
284 comp_locale_max
= models
.DecimalField(
285 max_digits
=12, decimal_places
=2, null
=True, blank
=True
287 comp_universite_min
= models
.DecimalField(
288 max_digits
=12, decimal_places
=2, null
=True, blank
=True
290 comp_universite_max
= models
.DecimalField(
291 max_digits
=12, decimal_places
=2, null
=True, blank
=True
293 comp_fonctionpub_min
= models
.DecimalField(
294 max_digits
=12, decimal_places
=2, null
=True, blank
=True
296 comp_fonctionpub_max
= models
.DecimalField(
297 max_digits
=12, decimal_places
=2, null
=True, blank
=True
299 comp_ong_min
= models
.DecimalField(
300 max_digits
=12, decimal_places
=2, null
=True, blank
=True
302 comp_ong_max
= models
.DecimalField(
303 max_digits
=12, decimal_places
=2, null
=True, blank
=True
305 comp_autre_min
= models
.DecimalField(
306 max_digits
=12, decimal_places
=2, null
=True, blank
=True
308 comp_autre_max
= models
.DecimalField(
309 max_digits
=12, decimal_places
=2, null
=True, blank
=True
313 justification
= models
.TextField(null
=True, blank
=True)
316 date_debut
= models
.DateField(
317 u
"date de début", help_text
=HELP_TEXT_DATE
, null
=True, blank
=True,
320 date_fin
= models
.DateField(
321 u
"date de fin", help_text
=HELP_TEXT_DATE
, null
=True, blank
=True,
327 ordering
= ['implantation__nom', 'nom']
328 verbose_name
= u
"Poste"
329 verbose_name_plural
= u
"Postes"
332 def __unicode__(self
):
333 representation
= u
'%s - %s [%s]' % (
334 self
.implantation
, self
.nom
, self
.id
336 return representation
338 prefix_implantation
= "implantation__zone_administrative"
340 def get_zones_administratives(self
):
341 return [self
.implantation
.zone_administrative
]
343 def get_devise(self
):
344 vp
= ValeurPoint
.objects
.filter(
345 implantation
=self
.implantation
, devise__archive
=False
350 return Devise
.objects
.get(code
='EUR')
354 __doc__
= Poste_
.__doc__
356 # meta dématérialisation : pour permettre le filtrage
357 vacant
= models
.NullBooleanField(u
"vacant", null
=True, blank
=True)
361 if self
.occupe_par():
365 def occupe_par(self
):
367 Retourne la liste d'employé occupant ce poste.
368 Généralement, retourne une liste d'un élément.
369 Si poste inoccupé, retourne liste vide.
370 UTILISE pour mettre a jour le flag vacant
374 for d
in self
.rh_dossiers
.exclude(date_fin__lt
=date
.today())
377 reversion
.register(Poste
, format
='xml', follow
=[
378 'rh_financements', 'rh_pieces', 'rh_comparaisons_internes',
383 POSTE_FINANCEMENT_CHOICES
= (
384 ('A', 'A - Frais de personnel'),
385 ('B', 'B - Projet(s)-Titre(s)'),
390 class PosteFinancement_(models
.Model
):
392 Pour un Poste, structure d'informations décrivant comment on prévoit
395 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
396 pourcentage
= models
.DecimalField(
397 max_digits
=12, decimal_places
=2,
398 help_text
="ex.: 33.33 % (décimale avec point)"
400 commentaire
= models
.TextField(
401 help_text
="Spécifiez la source de financement."
408 def __unicode__(self
):
409 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
412 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
415 class PosteFinancement(PosteFinancement_
):
416 poste
= models
.ForeignKey(
417 Poste
, db_column
='poste', related_name
='rh_financements'
420 reversion
.register(PosteFinancement
, format
='xml')
423 class PostePiece_(models
.Model
):
425 Documents relatifs au Poste.
426 Ex.: Description de poste
428 nom
= models
.CharField(u
"Nom", max_length
=255)
429 fichier
= models
.FileField(
430 u
"Fichier", upload_to
=poste_piece_dispatch
, storage
=storage_prive
437 def __unicode__(self
):
438 return u
'%s' % (self
.nom
)
441 class PostePiece(PostePiece_
):
442 poste
= models
.ForeignKey(
443 Poste
, db_column
='poste', related_name
='rh_pieces'
446 reversion
.register(PostePiece
, format
='xml')
449 class PosteComparaison_(models
.Model
, DevisableMixin
):
451 De la même manière qu'un dossier, un poste peut-être comparé à un autre
454 objects
= PosteComparaisonManager()
456 implantation
= models
.ForeignKey(
457 ref
.Implantation
, null
=True, blank
=True, related_name
="+"
459 nom
= models
.CharField(u
"Poste", max_length
=255, null
=True, blank
=True)
460 montant
= models
.IntegerField(null
=True)
461 devise
= models
.ForeignKey(
462 "Devise", related_name
='+', null
=True, blank
=True
468 def __unicode__(self
):
472 class PosteComparaison(PosteComparaison_
):
473 poste
= models
.ForeignKey(
474 Poste
, related_name
='rh_comparaisons_internes'
477 reversion
.register(PosteComparaison
, format
='xml')
480 class PosteCommentaire(Commentaire
):
481 poste
= models
.ForeignKey(
482 Poste
, db_column
='poste', related_name
='commentaires'
485 reversion
.register(PosteCommentaire
, format
='xml')
489 class Employe(models
.Model
):
491 Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
492 Dossiers qu'il occupe ou a occupé de Postes.
494 Cette classe aurait pu avantageusement s'appeler Personne car la notion
495 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
498 objects
= EmployeManager()
501 nom
= models
.CharField(max_length
=255)
502 prenom
= models
.CharField(u
"prénom", max_length
=255)
503 nom_affichage
= models
.CharField(
504 u
"nom d'affichage", max_length
=255, null
=True, blank
=True
506 nationalite
= models
.ForeignKey(
507 ref
.Pays
, to_field
='code', db_column
='nationalite',
508 related_name
='employes_nationalite', verbose_name
=u
"nationalité",
509 blank
=True, null
=True
511 date_naissance
= models
.DateField(
512 u
"date de naissance", help_text
=HELP_TEXT_DATE
,
513 validators
=[validate_date_passee
], null
=True, blank
=True
515 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
518 situation_famille
= models
.CharField(
519 u
"situation familiale", max_length
=1, choices
=SITUATION_CHOICES
,
520 null
=True, blank
=True
522 date_entree
= models
.DateField(
523 u
"date d'entrée à l'AUF", help_text
=HELP_TEXT_DATE
, null
=True,
528 tel_domicile
= models
.CharField(
529 u
"tél. domicile", max_length
=255, null
=True, blank
=True
531 tel_cellulaire
= models
.CharField(
532 u
"tél. cellulaire", max_length
=255, null
=True, blank
=True
534 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
535 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
536 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
537 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
538 pays
= models
.ForeignKey(
539 ref
.Pays
, to_field
='code', db_column
='pays',
540 related_name
='employes', null
=True, blank
=True
542 courriel_perso
= models
.EmailField(
543 u
'adresse courriel personnelle', blank
=True
546 # meta dématérialisation : pour permettre le filtrage
547 nb_postes
= models
.IntegerField(u
"nombre de postes", null
=True, blank
=True)
550 ordering
= ['nom', 'prenom']
551 verbose_name
= u
"Employé"
552 verbose_name_plural
= u
"Employés"
554 def __unicode__(self
):
555 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
557 def get_latest_dossier_ordered_by_date_fin_and_principal(self
):
558 res
= self
.rh_dossiers
.order_by(
559 '-principal', 'date_fin')
561 # Retourne en le premier du queryset si la date de fin est None
562 # Sinon, retourne le plus récent selon la date de fin.
564 if first
.date_fin
== None:
567 return res
.order_by('-principal', '-date_fin')[0]
571 if self
.genre
.upper() == u
'M':
573 elif self
.genre
.upper() == u
'F':
579 Retourne l'URL du service retournant la photo de l'Employe.
580 Équivalent reverse url 'rh_photo' avec id en param.
582 from django
.core
.urlresolvers
import reverse
583 return reverse('rh_photo', kwargs
={'id': self
.id})
585 def dossiers_passes(self
):
586 params
= {KEY_STATUT
: STATUT_INACTIF
, }
587 search
= RechercheTemporelle(params
, Dossier
)
588 search
.purge_params(params
)
589 q
= search
.get_q_temporel(self
.rh_dossiers
)
590 return self
.rh_dossiers
.filter(q
)
592 def dossiers_futurs(self
):
593 params
= {KEY_STATUT
: STATUT_FUTUR
, }
594 search
= RechercheTemporelle(params
, Dossier
)
595 search
.purge_params(params
)
596 q
= search
.get_q_temporel(self
.rh_dossiers
)
597 return self
.rh_dossiers
.filter(q
)
599 def dossiers_encours(self
):
600 params
= {KEY_STATUT
: STATUT_ACTIF
, }
601 search
= RechercheTemporelle(params
, Dossier
)
602 search
.purge_params(params
)
603 q
= search
.get_q_temporel(self
.rh_dossiers
)
604 return self
.rh_dossiers
.filter(q
)
606 def dossier_principal_pour_annee(self
):
607 return self
.dossier_principal(pour_annee
=True)
609 def dossier_principal(self
, pour_annee
=False):
611 Retourne le dossier principal (ou le plus ancien si il y en a
614 Si pour_annee == True, retourne le ou les dossiers principaux
615 pour l'annee en cours, sinon, le ou les dossiers principaux
616 pour la journee en cours.
618 TODO: (Refactoring possible): Utiliser meme logique dans
619 dae/templatetags/dae.py
625 year_start
= date(year
, 1, 1)
626 year_end
= date(year
, 12, 31)
629 dossier
= self
.rh_dossiers
.filter(
630 (Q(date_debut__lte
=year_end
, date_fin__isnull
=True) |
631 Q(date_debut__isnull
=True, date_fin__gte
=year_start
) |
632 Q(date_debut__lte
=year_end
, date_fin__gte
=year_start
) |
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 dossier
= self
.rh_dossiers
.filter(
641 (Q(date_debut__lte
=today
, date_fin__isnull
=True) |
642 Q(date_debut__isnull
=True, date_fin__gte
=today
) |
643 Q(date_debut__lte
=today
, date_fin__gte
=today
) |
644 Q(date_debut__isnull
=True, date_fin__isnull
=True)) &
645 Q(principal
=True)).order_by('date_debut')[0]
646 except IndexError, Dossier
.DoesNotExist
:
651 def postes_encours(self
):
652 postes_encours
= set()
653 for d
in self
.dossiers_encours():
654 postes_encours
.add(d
.poste
)
655 return postes_encours
657 def poste_principal(self
):
659 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
661 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
663 # DEPRECATED : on a maintenant Dossier.principal
664 poste
= Poste
.objects
.none()
666 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
671 prefix_implantation
= \
672 "rh_dossiers__poste__implantation__zone_administrative"
674 def get_zones_administratives(self
):
676 d
.poste
.implantation
.zone_administrative
677 for d
in self
.dossiers
.all()
680 reversion
.register(Employe
, format
='xml', follow
=[
681 'pieces', 'commentaires', 'ayantdroits'
685 class EmployePiece(models
.Model
):
687 Documents relatifs à un employé.
690 employe
= models
.ForeignKey(
691 'Employe', db_column
='employe', related_name
="pieces",
692 verbose_name
=u
"employé"
694 nom
= models
.CharField(max_length
=255)
695 fichier
= models
.FileField(
696 u
"fichier", upload_to
=employe_piece_dispatch
, storage
=storage_prive
701 verbose_name
= u
"Employé pièce"
702 verbose_name_plural
= u
"Employé pièces"
704 def __unicode__(self
):
705 return u
'%s' % (self
.nom
)
707 reversion
.register(EmployePiece
, format
='xml')
710 class EmployeCommentaire(Commentaire
):
711 employe
= models
.ForeignKey(
712 'Employe', db_column
='employe', related_name
='commentaires'
716 verbose_name
= u
"Employé commentaire"
717 verbose_name_plural
= u
"Employé commentaires"
719 reversion
.register(EmployeCommentaire
, format
='xml')
722 LIEN_PARENTE_CHOICES
= (
723 ('Conjoint', 'Conjoint'),
724 ('Conjointe', 'Conjointe'),
730 class AyantDroit(models
.Model
):
732 Personne en relation avec un Employe.
735 nom
= models
.CharField(max_length
=255)
736 prenom
= models
.CharField(u
"prénom", max_length
=255)
737 nom_affichage
= models
.CharField(
738 u
"nom d'affichage", max_length
=255, null
=True, blank
=True
740 nationalite
= models
.ForeignKey(
741 ref
.Pays
, to_field
='code', db_column
='nationalite',
742 related_name
='ayantdroits_nationalite',
743 verbose_name
=u
"nationalité", null
=True, blank
=True
745 date_naissance
= models
.DateField(
746 u
"Date de naissance", help_text
=HELP_TEXT_DATE
,
747 validators
=[validate_date_passee
], null
=True, blank
=True
749 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
752 employe
= models
.ForeignKey(
753 'Employe', db_column
='employe', related_name
='ayantdroits',
754 verbose_name
=u
"Employé"
756 lien_parente
= models
.CharField(
757 u
"lien de parenté", max_length
=10, choices
=LIEN_PARENTE_CHOICES
,
758 null
=True, blank
=True
763 verbose_name
= u
"Ayant droit"
764 verbose_name_plural
= u
"Ayants droit"
766 def __unicode__(self
):
767 return u
'%s %s' % (self
.nom
.upper(), self
.prenom
, )
769 prefix_implantation
= \
770 "employe__dossiers__poste__implantation__zone_administrative"
772 def get_zones_administratives(self
):
774 d
.poste
.implantation
.zone_administrative
775 for d
in self
.employe
.dossiers
.all()
778 reversion
.register(AyantDroit
, format
='xml', follow
=['commentaires'])
781 class AyantDroitCommentaire(Commentaire
):
782 ayant_droit
= models
.ForeignKey(
783 'AyantDroit', db_column
='ayant_droit', related_name
='commentaires'
786 reversion
.register(AyantDroitCommentaire
, format
='xml')
791 STATUT_RESIDENCE_CHOICES
= (
793 ('expat', 'Expatrié'),
796 COMPTE_COMPTA_CHOICES
= (
803 class Dossier_(DateActiviteMixin
, models
.Model
, DevisableMixin
,):
805 Le Dossier regroupe les informations relatives à l'occupation
806 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
809 Plusieurs Contrats peuvent être associés au Dossier.
810 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
811 lequel aucun Dossier n'existe est un poste vacant.
814 objects
= DossierManager()
817 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
818 organisme_bstg
= models
.ForeignKey(
819 'OrganismeBstg', db_column
='organisme_bstg', related_name
='+',
820 verbose_name
=u
"organisme",
822 u
"Si détaché (DET) ou mis à disposition (MAD), "
823 u
"préciser l'organisme."
824 ), null
=True, blank
=True
828 remplacement
= models
.BooleanField(default
=False)
829 remplacement_de
= models
.ForeignKey(
830 'self', related_name
='+', help_text
=u
"Taper le nom de l'employé",
831 null
=True, blank
=True
833 statut_residence
= models
.CharField(
834 u
"statut", max_length
=10, default
='local', null
=True,
835 choices
=STATUT_RESIDENCE_CHOICES
839 classement
= models
.ForeignKey(
840 'Classement', db_column
='classement', related_name
='+', null
=True,
843 regime_travail
= models
.DecimalField(
844 u
"régime de travail", max_digits
=12, null
=True, decimal_places
=2,
845 default
=REGIME_TRAVAIL_DEFAULT
, help_text
="% du temps complet"
847 regime_travail_nb_heure_semaine
= models
.DecimalField(
848 u
"nb. heures par semaine", max_digits
=12,
849 decimal_places
=2, null
=True,
850 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
851 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
854 # Occupation du Poste par cet Employe (anciennement "mandat")
855 date_debut
= models
.DateField(
856 u
"date de début d'occupation de poste", db_index
=True
858 date_fin
= models
.DateField(
859 u
"Date de fin d'occupation de poste", null
=True, blank
=True,
864 est_cadre
= models
.BooleanField(
870 compte_compta
= models
.CharField(max_length
=10, default
='aucun',
871 verbose_name
=u
'Compte comptabilité',
872 choices
=COMPTE_COMPTA_CHOICES
)
873 compte_courriel
= models
.BooleanField()
877 ordering
= ['employe__nom', ]
878 verbose_name
= u
"Dossier"
879 verbose_name_plural
= "Dossiers"
881 def salaire_theorique(self
):
882 annee
= date
.today().year
883 coeff
= self
.classement
.coefficient
884 implantation
= self
.poste
.implantation
885 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
887 montant
= coeff
* point
.valeur
888 devise
= point
.devise
889 return {'montant': montant
, 'devise': devise
}
891 def __unicode__(self
):
892 poste
= self
.poste
.nom
893 if self
.employe
.genre
== 'F':
894 poste
= self
.poste
.nom_feminin
895 return u
'%s - %s' % (self
.employe
, poste
)
897 prefix_implantation
= "poste__implantation__zone_administrative"
899 def get_zones_administratives(self
):
900 return [self
.poste
.implantation
.zone_administrative
]
902 def remunerations(self
):
903 key
= "%s_remunerations" % self
._meta
.app_label
904 remunerations
= getattr(self
, key
)
905 return remunerations
.all().order_by('-date_debut')
907 def remunerations_en_cours(self
):
908 q
= Q(date_fin__exact
=None) |
Q(date_fin__gt
=datetime
.date
.today())
909 return self
.remunerations().all().filter(q
).order_by('date_debut')
911 def get_salaire(self
):
913 return [r
for r
in self
.remunerations().order_by('-date_debut')
914 if r
.type_id
== 1][0]
918 def get_salaire_euros(self
):
919 tx
= self
.taux_devise()
920 return (float)(tx
) * (float)(self
.salaire
)
922 def get_remunerations_brutes(self
):
926 4 Indemnité d'expatriation
927 5 Indemnité pour frais
928 6 Indemnité de logement
929 7 Indemnité de fonction
930 8 Indemnité de responsabilité
931 9 Indemnité de transport
932 10 Indemnité compensatrice
933 11 Indemnité de subsistance
934 12 Indemnité différentielle
935 13 Prime d'installation
938 16 Indemnité de départ
939 18 Prime de 13ième mois
942 ids
= [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
943 return [r
for r
in self
.remunerations_en_cours().all()
946 def get_charges_salariales(self
):
948 20 Charges salariales ?
951 return [r
for r
in self
.remunerations_en_cours().all()
954 def get_charges_patronales(self
):
956 17 Charges patronales
959 return [r
for r
in self
.remunerations_en_cours().all()
962 def get_remunerations_tierces(self
):
966 return [r
for r
in self
.remunerations_en_cours().all()
967 if r
.type_id
in (2,)]
971 def get_total_local_charges_salariales(self
):
972 devise
= self
.poste
.get_devise()
974 for r
in self
.get_charges_salariales():
975 if r
.devise
!= devise
:
977 total
+= float(r
.montant
)
980 def get_total_local_charges_patronales(self
):
981 devise
= self
.poste
.get_devise()
983 for r
in self
.get_charges_patronales():
984 if r
.devise
!= devise
:
986 total
+= float(r
.montant
)
989 def get_local_salaire_brut(self
):
991 somme des rémuérations brutes
993 devise
= self
.poste
.get_devise()
995 for r
in self
.get_remunerations_brutes():
996 if r
.devise
!= devise
:
998 total
+= float(r
.montant
)
1001 def get_local_salaire_net(self
):
1003 salaire brut - charges salariales
1005 devise
= self
.poste
.get_devise()
1007 for r
in self
.get_charges_salariales():
1008 if r
.devise
!= devise
:
1010 total_charges
+= float(r
.montant
)
1011 return self
.get_local_salaire_brut() - total_charges
1013 def get_local_couts_auf(self
):
1015 salaire net + charges patronales
1017 devise
= self
.poste
.get_devise()
1019 for r
in self
.get_charges_patronales():
1020 if r
.devise
!= devise
:
1022 total_charges
+= float(r
.montant
)
1023 return self
.get_local_salaire_net() + total_charges
1025 def get_total_local_remunerations_tierces(self
):
1026 devise
= self
.poste
.get_devise()
1028 for r
in self
.get_remunerations_tierces():
1029 if r
.devise
!= devise
:
1031 total
+= float(r
.montant
)
1036 def get_total_charges_salariales(self
):
1038 for r
in self
.get_charges_salariales():
1039 total
+= r
.montant_euros()
1042 def get_total_charges_patronales(self
):
1044 for r
in self
.get_charges_patronales():
1045 total
+= r
.montant_euros()
1048 def get_salaire_brut(self
):
1050 somme des rémuérations brutes
1053 for r
in self
.get_remunerations_brutes():
1054 total
+= r
.montant_euros()
1057 def get_salaire_net(self
):
1059 salaire brut - charges salariales
1062 for r
in self
.get_charges_salariales():
1063 total_charges
+= r
.montant_euros()
1064 return self
.get_salaire_brut() - total_charges
1066 def get_couts_auf(self
):
1068 salaire net + charges patronales
1071 for r
in self
.get_charges_patronales():
1072 total_charges
+= r
.montant_euros()
1073 return self
.get_salaire_net() + total_charges
1075 def get_total_remunerations_tierces(self
):
1077 for r
in self
.get_remunerations_tierces():
1078 total
+= r
.montant_euros()
1081 def premier_contrat(self
):
1082 """contrat avec plus petite date de début"""
1084 contrat
= self
.rh_contrats
.exclude(date_debut
=None) \
1085 .order_by('date_debut')[0]
1086 except IndexError, Contrat
.DoesNotExist
:
1090 def dernier_contrat(self
):
1091 """contrat avec plus grande date de fin"""
1093 contrat
= self
.rh_contrats
.exclude(date_debut
=None) \
1094 .order_by('-date_debut')[0]
1095 except IndexError, Contrat
.DoesNotExist
:
1100 today
= date
.today()
1101 return (self
.date_debut
is None or self
.date_debut
<= today
) \
1102 and (self
.date_fin
is None or self
.date_fin
>= today
) \
1103 and not (self
.date_fin
is None and self
.date_debut
is None)
1106 class Dossier(Dossier_
):
1107 __doc__
= Dossier_
.__doc__
1108 poste
= models
.ForeignKey(
1109 Poste
, db_column
='poste', related_name
='rh_dossiers',
1110 help_text
=u
"Taper le nom du poste ou du type de poste",
1112 employe
= models
.ForeignKey(
1113 'Employe', db_column
='employe',
1114 help_text
=u
"Taper le nom de l'employé",
1115 related_name
='rh_dossiers', verbose_name
=u
"employé"
1117 principal
= models
.BooleanField(
1118 u
"dossier principal", default
=True,
1120 u
"Ce dossier est pour le principal poste occupé par l'employé"
1125 reversion
.register(Dossier
, format
='xml', follow
=[
1126 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
1127 'rh_contrats', 'commentaires'
1131 class RHDossierClassementRecord(models
.Model
):
1132 classement
= models
.ForeignKey(
1134 related_name
='classement_records',
1136 dossier
= models
.ForeignKey(
1138 related_name
='classement_records',
1140 date_debut
= models
.DateField(
1142 help_text
=HELP_TEXT_DATE
,
1147 date_fin
= models
.DateField(
1149 help_text
=HELP_TEXT_DATE
,
1154 commentaire
= models
.CharField(
1161 def __unicode__(self
):
1162 return self
.classement
.__unicode__()
1165 verbose_name
= u
"Element d'historique de classement"
1166 verbose_name_plural
= u
"Historique de classement"
1169 def post_save_handler(cls
,
1176 today
= date
.today()
1177 previous_record
= None
1178 previous_classement
= None
1181 # Premièrement, pour les nouvelles instances:
1183 if not instance
.classement
:
1187 date_debut
=instance
.date_debut
,
1188 classement
=instance
.classement
,
1193 # Deuxièmement, pour les instances existantes:
1196 # 1. Est-ce que le classement a changé?
1197 # 2. Est-ce qu'une historique de classement existe déjà
1199 previous_record
= cls
.objects
.get(
1201 classement
=instance
.before_save
.classement
,
1204 except cls
.DoesNotExist
:
1205 if instance
.before_save
.classement
:
1206 # Il était censé avoir une historique de classement
1208 previous_record
= cls
.objects
.create(
1209 date_debut
=instance
.before_save
.date_debut
,
1210 classement
=instance
.before_save
.classement
,
1213 previous_classement
= instance
.before_save
.classement
1216 previous_classement
= previous_record
.classement
1219 instance
.classement
!=
1223 # Cas aucun changement:
1228 # Classement a changé
1230 previous_record
.date_fin
= today
1231 previous_record
.save()
1233 if instance
.classement
:
1236 classement
=instance
.classement
,
1241 class DossierPiece_(models
.Model
):
1243 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
1244 Ex.: Lettre de motivation.
1246 nom
= models
.CharField(max_length
=255)
1247 fichier
= models
.FileField(
1248 upload_to
=dossier_piece_dispatch
, storage
=storage_prive
1255 def __unicode__(self
):
1256 return u
'%s' % (self
.nom
)
1259 class DossierPiece(DossierPiece_
):
1260 dossier
= models
.ForeignKey(
1261 Dossier
, db_column
='dossier', related_name
='rh_dossierpieces'
1264 reversion
.register(DossierPiece
, format
='xml')
1266 class DossierCommentaire(Commentaire
):
1267 dossier
= models
.ForeignKey(
1268 Dossier
, db_column
='dossier', related_name
='commentaires'
1271 reversion
.register(DossierCommentaire
, format
='xml')
1274 class DossierComparaison_(models
.Model
, DevisableMixin
):
1276 Photo d'une comparaison salariale au moment de l'embauche.
1278 objects
= DossierComparaisonManager()
1280 implantation
= models
.ForeignKey(
1281 ref
.Implantation
, related_name
="+", null
=True, blank
=True
1283 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
1284 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
1285 montant
= models
.IntegerField(null
=True)
1286 devise
= models
.ForeignKey(
1287 'Devise', related_name
='+', null
=True, blank
=True
1293 def __unicode__(self
):
1294 return "%s (%s)" % (self
.poste
, self
.personne
)
1297 class DossierComparaison(DossierComparaison_
):
1298 dossier
= models
.ForeignKey(
1299 Dossier
, related_name
='rh_comparaisons'
1302 reversion
.register(DossierComparaison
, format
='xml')
1307 class RemunerationMixin(models
.Model
):
1310 type = models
.ForeignKey(
1311 'TypeRemuneration', db_column
='type', related_name
='+',
1312 verbose_name
=u
"type de rémunération"
1314 type_revalorisation
= models
.ForeignKey(
1315 'TypeRevalorisation', db_column
='type_revalorisation',
1316 related_name
='+', verbose_name
=u
"type de revalorisation",
1317 null
=True, blank
=True
1319 montant
= models
.DecimalField(
1320 null
=True, blank
=True, max_digits
=12, decimal_places
=2
1321 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1322 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+')
1324 # commentaire = precision
1325 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
1327 # date_debut = anciennement date_effectif
1328 date_debut
= models
.DateField(
1329 u
"date de début", null
=True, blank
=True, db_index
=True
1331 date_fin
= models
.DateField(
1332 u
"date de fin", null
=True, blank
=True, db_index
=True
1337 ordering
= ['type__nom', '-date_fin']
1339 def __unicode__(self
):
1340 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
1343 class Remuneration_(RemunerationMixin
, DevisableMixin
):
1345 Structure de rémunération (données budgétaires) en situation normale
1346 pour un Dossier. Si un Evenement existe, utiliser la structure de
1347 rémunération EvenementRemuneration de cet événement.
1349 objects
= RemunerationManager()
1352 def find_yearly_range(from_date
, to_date
, year
):
1353 today
= date
.today()
1354 year
= year
or date
.today().year
1355 year_start
= date(year
, 1, 1)
1356 year_end
= date(year
, 12, 31)
1358 def constrain_to_year(*dates
):
1360 S'assure que les dates soient dans le range year_start a
1363 return [min(max(year_start
, d
), year_end
)
1367 from_date
or year_start
, year_start
)
1369 to_date
or year_end
, year_end
)
1371 start_date
, end_date
= constrain_to_year(start_date
, end_date
)
1373 jours_annee
= (year_end
- year_start
).days
1374 jours_dates
= (end_date
- start_date
).days
1375 factor
= Decimal(str(jours_dates
)) / Decimal(str(jours_annee
))
1377 return start_date
, end_date
, factor
1380 def montant_ajuste_euros(self
, annee
=None):
1382 Le montant ajusté représente le montant annuel, ajusté sur la
1383 période de temps travaillée, multipliée par le ratio de temps
1384 travaillé (en rapport au temps plein).
1386 date_debut
, date_fin
, factor
= self
.find_yearly_range(
1392 montant_euros
= Decimal(str(self
.montant_euros_float()) or '0')
1394 if self
.type.nature_remuneration
!= u
'Accessoire':
1395 dossier
= getattr(self
, 'dossier', None)
1398 Dans le cas d'un DossierComparaisonRemuneration, il
1399 n'y a plus de reference au dossier.
1401 regime_travail
= REGIME_TRAVAIL_DEFAULT
1403 regime_travail
= self
.dossier
.regime_travail
1404 return (montant_euros
* factor
*
1405 regime_travail
/ 100)
1407 return montant_euros
1409 def montant_mois(self
):
1410 return round(self
.montant
/ 12, 2)
1412 def montant_avec_regime(self
):
1413 return round(self
.montant
* (self
.dossier
.regime_travail
/ 100), 2)
1415 def montant_euro_mois(self
):
1416 return round(self
.montant_euros() / 12, 2)
1418 def __unicode__(self
):
1420 devise
= self
.devise
.code
1423 return "%s %s" % (self
.montant
, devise
)
1427 verbose_name
= u
"Rémunération"
1428 verbose_name_plural
= u
"Rémunérations"
1431 class Remuneration(Remuneration_
):
1432 dossier
= models
.ForeignKey(
1433 Dossier
, db_column
='dossier', related_name
='rh_remunerations'
1436 reversion
.register(Remuneration
, format
='xml')
1441 class Contrat_(models
.Model
):
1443 Document juridique qui encadre la relation de travail d'un Employe
1444 pour un Poste particulier. Pour un Dossier (qui documente cette
1445 relation de travail) plusieurs contrats peuvent être associés.
1447 objects
= ContratManager()
1448 type_contrat
= models
.ForeignKey(
1449 'TypeContrat', db_column
='type_contrat',
1450 verbose_name
=u
'type de contrat', related_name
='+'
1452 date_debut
= models
.DateField(
1453 u
"date de début", db_index
=True
1455 date_fin
= models
.DateField(
1456 u
"date de fin", null
=True, blank
=True, db_index
=True
1458 fichier
= models
.FileField(
1459 upload_to
=contrat_dispatch
, storage
=storage_prive
, null
=True,
1465 ordering
= ['dossier__employe__nom']
1466 verbose_name
= u
"Contrat"
1467 verbose_name_plural
= u
"Contrats"
1469 def __unicode__(self
):
1470 return u
'%s - %s' % (self
.dossier
, self
.id)
1473 class Contrat(Contrat_
):
1474 dossier
= models
.ForeignKey(
1475 Dossier
, db_column
='dossier', related_name
='rh_contrats'
1478 reversion
.register(Contrat
, format
='xml')
1483 class CategorieEmploi(models
.Model
):
1485 Catégorie utilisée dans la gestion des Postes.
1486 Catégorie supérieure à TypePoste.
1488 nom
= models
.CharField(max_length
=255)
1492 verbose_name
= u
"catégorie d'emploi"
1493 verbose_name_plural
= u
"catégories d'emploi"
1495 def __unicode__(self
):
1498 reversion
.register(CategorieEmploi
, format
='xml')
1501 class FamilleProfessionnelle(models
.Model
):
1503 Famille professionnelle d'un poste.
1505 nom
= models
.CharField(max_length
=100)
1509 verbose_name
= u
'famille professionnelle'
1510 verbose_name_plural
= u
'familles professionnelles'
1512 def __unicode__(self
):
1515 reversion
.register(FamilleProfessionnelle
, format
='xml')
1518 class TypePoste(Archivable
):
1522 nom
= models
.CharField(max_length
=255)
1523 nom_feminin
= models
.CharField(u
"nom féminin", max_length
=255)
1524 is_responsable
= models
.BooleanField(
1525 u
"poste de responsabilité", default
=False
1527 categorie_emploi
= models
.ForeignKey(
1528 CategorieEmploi
, db_column
='categorie_emploi', related_name
='+',
1529 verbose_name
=u
"catégorie d'emploi"
1531 famille_professionnelle
= models
.ForeignKey(
1532 FamilleProfessionnelle
, related_name
='types_de_poste',
1533 verbose_name
=u
"famille professionnelle", blank
=True, null
=True
1538 verbose_name
= u
"Type de poste"
1539 verbose_name_plural
= u
"Types de poste"
1541 def __unicode__(self
):
1542 return u
'%s' % (self
.nom
)
1544 reversion
.register(TypePoste
, format
='xml')
1547 TYPE_PAIEMENT_CHOICES
= (
1548 (u
'Régulier', u
'Régulier'),
1549 (u
'Ponctuel', u
'Ponctuel'),
1552 NATURE_REMUNERATION_CHOICES
= (
1553 (u
'Traitement', u
'Traitement'),
1554 (u
'Indemnité', u
'Indemnités autres'),
1555 (u
'Charges', u
'Charges patronales'),
1556 (u
'Accessoire', u
'Accessoires'),
1557 (u
'RAS', u
'Rémunération autre source'),
1561 class TypeRemuneration(Archivable
):
1563 Catégorie de Remuneration.
1566 objects
= models
.Manager()
1567 sans_archives
= ArchivableManager()
1569 nom
= models
.CharField(max_length
=255)
1570 type_paiement
= models
.CharField(
1571 u
"type de paiement", max_length
=30, choices
=TYPE_PAIEMENT_CHOICES
1574 nature_remuneration
= models
.CharField(
1575 u
"nature de la rémunération", max_length
=30,
1576 choices
=NATURE_REMUNERATION_CHOICES
1581 verbose_name
= u
"Type de rémunération"
1582 verbose_name_plural
= u
"Types de rémunération"
1584 def __unicode__(self
):
1587 reversion
.register(TypeRemuneration
, format
='xml')
1590 class TypeRevalorisation(Archivable
):
1592 Justification du changement de la Remuneration.
1593 (Actuellement utilisé dans aucun traitement informatique.)
1595 nom
= models
.CharField(max_length
=255)
1599 verbose_name
= u
"Type de revalorisation"
1600 verbose_name_plural
= u
"Types de revalorisation"
1602 def __unicode__(self
):
1603 return u
'%s' % (self
.nom
)
1605 reversion
.register(TypeRevalorisation
, format
='xml')
1608 class Service(Archivable
):
1610 Unité administrative où les Postes sont rattachés.
1612 nom
= models
.CharField(max_length
=255)
1616 verbose_name
= u
"service"
1617 verbose_name_plural
= u
"services"
1619 def __unicode__(self
):
1622 reversion
.register(Service
, format
='xml')
1625 TYPE_ORGANISME_CHOICES
= (
1626 ('MAD', 'Mise à disposition'),
1627 ('DET', 'Détachement'),
1631 class OrganismeBstg(models
.Model
):
1633 Organisation d'où provient un Employe mis à disposition (MAD) de
1634 ou détaché (DET) à l'AUF à titre gratuit.
1636 (BSTG = bien et service à titre gratuit.)
1638 nom
= models
.CharField(max_length
=255)
1639 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1640 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1642 related_name
='organismes_bstg',
1643 null
=True, blank
=True)
1646 ordering
= ['type', 'nom']
1647 verbose_name
= u
"Organisme BSTG"
1648 verbose_name_plural
= u
"Organismes BSTG"
1650 def __unicode__(self
):
1651 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1653 reversion
.register(OrganismeBstg
, format
='xml')
1656 class Statut(Archivable
):
1658 Statut de l'Employe dans le cadre d'un Dossier particulier.
1661 code
= models
.CharField(
1662 max_length
=25, unique
=True,
1664 u
"Saisir un code court mais lisible pour ce statut : "
1665 u
"le code est utilisé pour associer les statuts aux autres "
1666 u
"données tout en demeurant plus lisible qu'un identifiant "
1670 nom
= models
.CharField(max_length
=255)
1674 verbose_name
= u
"Statut d'employé"
1675 verbose_name_plural
= u
"Statuts d'employé"
1677 def __unicode__(self
):
1678 return u
'%s : %s' % (self
.code
, self
.nom
)
1680 reversion
.register(Statut
, format
='xml')
1683 TYPE_CLASSEMENT_CHOICES
= (
1684 ('S', 'S -Soutien'),
1685 ('T', 'T - Technicien'),
1686 ('P', 'P - Professionel'),
1688 ('D', 'D - Direction'),
1689 ('SO', 'SO - Sans objet [expatriés]'),
1690 ('HG', 'HG - Hors grille [direction]'),
1694 class ClassementManager(models
.Manager
):
1696 Ordonner les spcéfiquement les classements.
1698 def get_query_set(self
):
1699 qs
= super(ClassementManager
, self
).get_query_set()
1700 qs
= qs
.extra(select
={
1701 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1703 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1707 class ClassementArchivableManager(ClassementManager
,
1712 class Classement_(Archivable
):
1714 Éléments de classement de la
1715 "Grille générique de classement hiérarchique".
1717 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1718 classement dans la grille. Le classement donne le coefficient utilisé dans:
1720 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1722 objects
= ClassementManager()
1723 sans_archives
= ClassementArchivableManager()
1726 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1727 echelon
= models
.IntegerField(u
"échelon", blank
=True, default
=0)
1728 degre
= models
.IntegerField(u
"degré", blank
=True, default
=0)
1729 coefficient
= models
.FloatField(u
"coefficient", blank
=True, null
=True)
1732 # annee # au lieu de date_debut et date_fin
1733 commentaire
= models
.TextField(null
=True, blank
=True)
1737 ordering
= ['type', 'echelon', 'degre', 'coefficient']
1738 verbose_name
= u
"Classement"
1739 verbose_name_plural
= u
"Classements"
1741 def __unicode__(self
):
1742 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1745 class Classement(Classement_
):
1746 __doc__
= Classement_
.__doc__
1748 reversion
.register(Classement
, format
='xml')
1751 class TauxChange_(models
.Model
):
1753 Taux de change de la devise vers l'euro (EUR)
1754 pour chaque année budgétaire.
1757 devise
= models
.ForeignKey('Devise', db_column
='devise')
1758 annee
= models
.IntegerField(u
"année")
1759 taux
= models
.FloatField(u
"taux vers l'euro")
1763 ordering
= ['-annee', 'devise__code']
1764 verbose_name
= u
"Taux de change"
1765 verbose_name_plural
= u
"Taux de change"
1766 unique_together
= ('devise', 'annee')
1768 def __unicode__(self
):
1769 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1772 class TauxChange(TauxChange_
):
1773 __doc__
= TauxChange_
.__doc__
1775 reversion
.register(TauxChange
, format
='xml')
1778 class ValeurPointManager(models
.Manager
):
1780 def get_query_set(self
):
1781 return super(ValeurPointManager
, self
).get_query_set() \
1782 .select_related('devise', 'implantation')
1785 class ValeurPoint_(models
.Model
):
1787 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1788 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1789 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1791 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1794 objects
= models
.Manager()
1795 actuelles
= ValeurPointManager()
1797 valeur
= models
.FloatField(null
=True)
1798 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1799 implantation
= models
.ForeignKey(ref
.Implantation
,
1800 db_column
='implantation',
1801 related_name
='%(app_label)s_valeur_point')
1803 annee
= models
.IntegerField()
1806 ordering
= ['-annee', 'implantation__nom']
1808 verbose_name
= u
"Valeur du point"
1809 verbose_name_plural
= u
"Valeurs du point"
1810 unique_together
= ('implantation', 'annee')
1812 def __unicode__(self
):
1813 return u
'%s %s %s [%s] %s' % (
1814 self
.devise
.code
, self
.annee
, self
.valeur
,
1815 self
.implantation
.nom_court
, self
.devise
.nom
1819 class ValeurPoint(ValeurPoint_
):
1820 __doc__
= ValeurPoint_
.__doc__
1822 reversion
.register(ValeurPoint
, format
='xml')
1825 class Devise(Archivable
):
1829 code
= models
.CharField(max_length
=10, unique
=True)
1830 nom
= models
.CharField(max_length
=255)
1834 verbose_name
= u
"devise"
1835 verbose_name_plural
= u
"devises"
1837 def __unicode__(self
):
1838 return u
'%s - %s' % (self
.code
, self
.nom
)
1840 reversion
.register(Devise
, format
='xml')
1843 class TypeContrat(Archivable
):
1847 nom
= models
.CharField(max_length
=255)
1848 nom_long
= models
.CharField(max_length
=255)
1852 verbose_name
= u
"Type de contrat"
1853 verbose_name_plural
= u
"Types de contrat"
1855 def __unicode__(self
):
1856 return u
'%s' % (self
.nom
)
1858 reversion
.register(TypeContrat
, format
='xml')
1863 class ResponsableImplantationProxy(ref
.Implantation
):
1871 verbose_name
= u
"Responsable d'implantation"
1872 verbose_name_plural
= u
"Responsables d'implantation"
1875 class ResponsableImplantation(models
.Model
):
1877 Le responsable d'une implantation.
1878 Anciennement géré sur le Dossier du responsable.
1880 employe
= models
.ForeignKey(
1881 'Employe', db_column
='employe', related_name
='+', null
=True,
1884 implantation
= models
.OneToOneField(
1885 "ResponsableImplantationProxy", db_column
='implantation',
1886 related_name
='responsable', unique
=True
1889 def __unicode__(self
):
1890 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1893 ordering
= ['implantation__nom']
1894 verbose_name
= "Responsable d'implantation"
1895 verbose_name_plural
= "Responsables d'implantation"
1897 reversion
.register(ResponsableImplantation
, format
='xml')
1900 class UserProfile(models
.Model
):
1901 user
= models
.OneToOneField(User
, related_name
='profile')
1902 zones_administratives
= models
.ManyToManyField(
1903 ref
.ZoneAdministrative
,
1904 related_name
='profiles'
1907 verbose_name
= "Permissions sur zones administratives"
1908 verbose_name_plural
= "Permissions sur zones administratives"
1910 def __unicode__(self
):
1911 return self
.user
.__unicode__()
1913 reversion
.register(UserProfile
, format
='xml')
1917 TYPES_CHANGEMENT
= (
1924 class ChangementPersonnelNotifications(models
.Model
):
1926 verbose_name
= u
"Destinataire pour notices de mouvement de personnel"
1927 verbose_name_plural
= u
"Destinataires pour notices de mouvement de personnel"
1929 type = models
.CharField(
1931 choices
= TYPES_CHANGEMENT
,
1935 destinataires
= models
.ManyToManyField(
1937 related_name
='changement_notifications',
1940 def __unicode__(self
):
1942 self
.get_type_display(), ','.join(
1943 self
.destinataires
.all().values_list(
1944 'courriel', flat
=True))
1948 class ChangementPersonnel(models
.Model
):
1950 Une notice qui enregistre un changement de personnel, incluant:
1953 * Mouvement de personnel
1958 verbose_name
= u
"Mouvement de personnel"
1959 verbose_name_plural
= u
"Mouvements de personnel"
1961 def __unicode__(self
):
1962 return '%s: %s' % (self
.dossier
.__unicode__(),
1963 self
.get_type_display())
1966 def create_changement(cls
, dossier
, type):
1967 # If this employe has existing Changement, set them to invalid.
1968 cls
.objects
.filter(dossier__employe
=dossier
.employe
).update(valide
=False)
1980 def post_save_handler(cls
,
1987 # This defines the time limit used when checking in previous
1988 # files to see if an employee if new. Basically, if emloyee
1989 # left his position new_file.date_debut -
1990 # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then
1991 # if a new file is created for this employee, he will bec
1992 # onsidered "NEW" and a notice will be created to this effect.
1993 NEW_EMPLOYE_THRESHOLD
= datetime
.timedelta(7) # 7 days.
1995 other_dossier_qs
= instance
.employe
.rh_dossiers
.exclude(
1997 dd
= instance
.date_debut
1998 df
= instance
.date_fin
1999 today
= date
.today()
2001 # Here, verify differences between the instance, before and
2003 df_has_changed
= False
2007 df_has_changed
= True
2009 df_has_changed
= (df
!= instance
.before_save
.date_fin
and
2015 # Date de fin est None et c'est une nouvelle instance de
2017 if not df
and created
:
2018 # QS for finding other dossiers with a date_fin of None OR
2019 # with a date_fin >= to this dossier's date_debut
2020 exists_recent_file_qs
= other_dossier_qs
.filter(
2021 Q(date_fin__isnull
=True) |
2022 Q(date_fin__gte
=dd
- NEW_EMPLOYE_THRESHOLD
)
2025 # 1. If existe un Dossier récent
2026 if exists_recent_file_qs
.count() > 0:
2027 cls
.create_changement(
2031 # 2. Il n'existe un Dossier récent, et c'est une nouvelle
2032 # instance de Dossier:
2034 cls
.create_changement(
2039 elif not df
and not created
and cls
.objects
.filter(
2041 date_creation__gte
=today
- NEW_EMPLOYE_THRESHOLD
,
2044 cls
.create_changement(
2049 # Date de fin a été modifiée:
2051 # QS for other active files (date_fin == None), excludes
2053 exists_active_files_qs
= other_dossier_qs
.filter(
2054 Q(date_fin__isnull
=True))
2056 # 3. Date de fin a été modifiée et il n'existe aucun autre
2057 # dossier actifs: Depart
2058 if exists_active_files_qs
.count() == 0:
2059 cls
.create_changement(
2063 # 4. Dossier a une nouvelle date de fin par contre
2064 # d'autres dossiers actifs existent déjà: Mouvement
2066 cls
.create_changement(
2072 dossier
= models
.ForeignKey(
2074 related_name
='mouvements',
2077 valide
= models
.BooleanField(default
=True)
2078 date_creation
= models
.DateTimeField(
2080 communique
= models
.BooleanField(
2084 date_communication
= models
.DateTimeField(
2089 type = models
.CharField(
2091 choices
= TYPES_CHANGEMENT
,
2094 reversion
.register(ChangementPersonnel
, format
='xml')
2097 def dossier_pre_save_handler(sender
,
2101 # Store a copy of the model before save is called.
2102 if instance
.pk
is not None:
2103 instance
.before_save
= Dossier
.objects
.get(pk
=instance
.pk
)
2105 instance
.before_save
= None
2108 # Connect a pre_save handler that assigns a copy of the model as an
2109 # attribute in order to compare it in post_save.
2110 pre_save
.connect(dossier_pre_save_handler
, sender
=Dossier
)
2112 post_save
.connect(ChangementPersonnel
.post_save_handler
, sender
=Dossier
)
2113 post_save
.connect(RHDossierClassementRecord
.post_save_handler
, sender
=Dossier
)