1 # -=- encoding: utf-8 -=-
4 from datetime
import date
5 from decimal
import Decimal
7 from django
.db
.models
import signals
8 from django
.core
.files
.storage
import FileSystemStorage
9 from django
.db
import models
10 from django
.conf
import settings
12 from auf
.django
.emploi
.models
import GENRE_CHOICES
, SITUATION_CHOICES
# devrait plutot être dans references
13 from auf
.django
.metadata
.models
import AUFMetadata
14 from auf
.django
.metadata
.managers
import NoDeleteManager
15 import auf
.django
.references
.models
as ref
16 from validators
import validate_date_passee
17 from managers
import PosteManager
, DossierManager
, DossierComparaisonManager
, PosteComparaisonManager
21 # Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage.
22 # Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK
25 models_stack
= [s
[1].split('/')[-2] for s
in inspect
.stack() if s
[1].endswith('models.py')]
26 return models_stack
[-1]
30 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
31 REGIME_TRAVAIL_DEFAULT
= Decimal('100.00')
32 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
= Decimal('35.00')
35 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
36 base_url
=settings
.PRIVE_MEDIA_URL
)
38 def poste_piece_dispatch(instance
, filename
):
39 path
= "poste/%s/%s" % (instance
.poste_id
, filename
)
42 def dossier_piece_dispatch(instance
, filename
):
43 path
= "dossier/%s/%s" % (instance
.dossier_id
, filename
)
46 def employe_piece_dispatch(instance
, filename
):
47 path
= "employe/%s/%s" % (instance
.employe_id
, filename
)
51 class Commentaire(AUFMetadata
):
52 texte
= models
.TextField()
53 owner
= models
.ForeignKey('auth.User', db_column
='owner', related_name
='+', verbose_name
=u
"Commentaire de")
57 ordering
= ['-date_creation']
59 def __unicode__(self
):
60 return u
'%s' % (self
.texte
)
65 POSTE_APPEL_CHOICES
= (
66 ('interne', 'Interne'),
67 ('externe', 'Externe'),
70 class Poste_(AUFMetadata
):
71 """Un Poste est un emploi (job) à combler dans une implantation.
72 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
73 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
76 objects
= PosteManager()
79 nom
= models
.CharField(max_length
=255,
80 verbose_name
= u
"Titre du poste", )
81 nom_feminin
= models
.CharField(max_length
=255,
82 verbose_name
= u
"Titre du poste (au féminin)",
84 implantation
= models
.ForeignKey(ref
.Implantation
, help_text
=u
"Taper le nom de l'implantation ou sa région",
85 db_column
='implantation', related_name
='+')
86 type_poste
= models
.ForeignKey('TypePoste', db_column
='type_poste', help_text
=u
"Taper le nom du type de poste",
89 service
= models
.ForeignKey('Service', db_column
='service',
91 verbose_name
= u
"Direction/Service/Pôle support", )
92 responsable
= models
.ForeignKey('Poste', db_column
='responsable',
93 related_name
='+', null
=True,help_text
=u
"Taper le nom du poste ou du type de poste",
94 verbose_name
= u
"Poste du responsable", )
97 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
98 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
99 verbose_name
= u
"Temps de travail",
100 help_text
="% du temps complet")
101 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
102 decimal_places
=2, null
=True,
103 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
104 verbose_name
= u
"Nb. heures par semaine")
107 local
= models
.NullBooleanField(verbose_name
= u
"Local", default
=True,
108 null
=True, blank
=True)
109 expatrie
= models
.NullBooleanField(verbose_name
= u
"Expatrié", default
=False,
110 null
=True, blank
=True)
111 mise_a_disposition
= models
.NullBooleanField(
112 verbose_name
= u
"Mise à disposition",
113 null
=True, default
=False)
114 appel
= models
.CharField(max_length
=10, null
=True,
115 verbose_name
= u
"Appel à candidature",
116 choices
=POSTE_APPEL_CHOICES
,
120 classement_min
= models
.ForeignKey('Classement',
121 db_column
='classement_min', related_name
='+',
122 null
=True, blank
=True)
123 classement_max
= models
.ForeignKey('Classement',
124 db_column
='classement_max', related_name
='+',
125 null
=True, blank
=True)
126 valeur_point_min
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
127 db_column
='valeur_point_min', related_name
='+',
128 null
=True, blank
=True)
129 valeur_point_max
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
130 db_column
='valeur_point_max', related_name
='+',
131 null
=True, blank
=True)
132 devise_min
= models
.ForeignKey('Devise', db_column
='devise_min', null
=True,
133 related_name
='+', default
=5)
134 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
135 related_name
='+', default
=5)
136 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
137 null
=True, default
=0)
138 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
139 null
=True, default
=0)
140 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
141 null
=True, default
=0)
142 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
143 null
=True, default
=0)
144 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
145 null
=True, default
=0)
146 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
147 null
=True, default
=0)
149 # Comparatifs de rémunération
150 devise_comparaison
= models
.ForeignKey('Devise', null
=True,
151 db_column
='devise_comparaison',
154 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
155 null
=True, blank
=True)
156 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
157 null
=True, blank
=True)
158 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
159 null
=True, blank
=True)
160 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
161 null
=True, blank
=True)
162 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
163 null
=True, blank
=True)
164 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
165 null
=True, blank
=True)
166 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
167 null
=True, blank
=True)
168 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
169 null
=True, blank
=True)
170 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
171 null
=True, blank
=True)
172 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
173 null
=True, blank
=True)
176 justification
= models
.TextField(null
=True, blank
=True)
179 date_debut
= models
.DateField(verbose_name
=u
"Date de début",
180 null
=True, blank
=True)
181 date_fin
= models
.DateField(verbose_name
=u
"Date de fin",
182 null
=True, blank
=True)
186 ordering
= ['implantation__nom', 'nom']
187 verbose_name
= u
"Poste"
188 verbose_name_plural
= u
"Postes"
190 def __unicode__(self
):
191 representation
= u
'%s - %s [%s]' % (self
.implantation
, self
.nom
,
193 return representation
196 prefix_implantation
= "implantation__region"
197 def get_regions(self
):
198 return [self
.implantation
.region
]
202 __doc__
= Poste_
.__doc__
204 # meta dématérialisation : pour permettre le filtrage
205 vacant
= models
.NullBooleanField(verbose_name
= u
"Vacant", null
=True, blank
=True)
209 if self
.occupe_par():
213 def occupe_par(self
):
214 """Retourne la liste d'employé occupant ce poste.
215 Généralement, retourne une liste d'un élément.
216 Si poste inoccupé, retourne liste vide.
217 UTILISE pour mettre a jour le flag vacant
219 return [d
.employe
for d
in self
.rh_dossiers
.filter(actif
=True, supprime
=False) \
220 .exclude(date_fin__lt
=date
.today())]
223 POSTE_FINANCEMENT_CHOICES
= (
224 ('A', 'A - Frais de personnel'),
225 ('B', 'B - Projet(s)-Titre(s)'),
230 class PosteFinancement_(models
.Model
):
231 """Pour un Poste, structure d'informations décrivant comment on prévoit
234 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
235 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
236 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
237 help_text
="ex.: 33.33 % (décimale avec point)")
238 commentaire
= models
.TextField(
239 help_text
="Spécifiez la source de financement.")
245 def __unicode__(self
):
246 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
249 class PosteFinancement(PosteFinancement_
):
253 class PostePiece_(models
.Model
):
254 """Documents relatifs au Poste.
255 Ex.: Description de poste
257 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
258 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
259 fichier
= models
.FileField(verbose_name
= u
"Fichier",
260 upload_to
=poste_piece_dispatch
,
261 storage
=storage_prive
)
267 def __unicode__(self
):
268 return u
'%s' % (self
.nom
)
270 class PostePiece(PostePiece_
):
273 class PosteComparaison_(models
.Model
):
275 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
277 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
278 objects
= PosteComparaisonManager()
280 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
281 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
282 montant
= models
.IntegerField(null
=True)
283 devise
= models
.ForeignKey("Devise", default
=5, related_name
='+', null
=True, blank
=True)
288 def taux_devise(self
):
289 if self
.devise
.code
== "EUR":
291 annee
= self
.poste
.date_debut
.year
292 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
295 raise Exception(u
"Le taux de la devise %s n'a pas n'existe pas pour %s ou il existe plusieurs taux pour la même année" % (self
.devise
.id, annee
))
299 def montant_euros(self
):
300 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
302 class PosteComparaison(PosteComparaison_
):
305 class PosteCommentaire_(Commentaire
):
306 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
311 class PosteCommentaire(PosteCommentaire_
):
316 class Employe(AUFMetadata
):
317 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
318 Dossiers qu'il occupe ou a occupé de Postes.
320 Cette classe aurait pu avantageusement s'appeler Personne car la notion
321 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
324 nom
= models
.CharField(max_length
=255)
325 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
326 nom_affichage
= models
.CharField(max_length
=255,
327 verbose_name
= u
"Nom d'affichage",
328 null
=True, blank
=True)
329 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
330 db_column
='nationalite',
331 related_name
='employes_nationalite',
332 verbose_name
= u
"Nationalité",
333 blank
=True, null
=True)
334 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
335 help_text
=HELP_TEXT_DATE
,
336 validators
=[validate_date_passee
],
337 null
=True, blank
=True)
338 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
341 situation_famille
= models
.CharField(max_length
=1,
342 choices
=SITUATION_CHOICES
,
343 verbose_name
= u
"Situation familiale",
344 null
=True, blank
=True)
345 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
346 help_text
=HELP_TEXT_DATE
,
347 null
=True, blank
=True)
350 tel_domicile
= models
.CharField(max_length
=255,
351 verbose_name
= u
"Tél. domicile",
352 null
=True, blank
=True)
353 tel_cellulaire
= models
.CharField(max_length
=255,
354 verbose_name
= u
"Tél. cellulaire",
355 null
=True, blank
=True)
356 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
357 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
358 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
359 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
360 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
361 related_name
='employes',
362 null
=True, blank
=True)
365 ordering
= ['nom','prenom']
366 verbose_name
= u
"Employé"
367 verbose_name_plural
= u
"Employés"
369 def __unicode__(self
):
370 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
374 if self
.genre
.upper() == u
'M':
376 elif self
.genre
.upper() == u
'F':
381 """Retourne l'URL du service retournant la photo de l'Employe.
382 Équivalent reverse url 'rh_photo' avec id en param.
384 from django
.core
.urlresolvers
import reverse
385 return reverse('rh_photo', kwargs
={'id':self
.id})
387 def dossiers_passes(self
):
389 dossiers_passes
= self
.dossiers
.filter(date_fin__lt
=today
).order_by('-date_fin')
390 for d
in dossiers_passes
:
392 return dossiers_passes
394 def dossiers_futurs(self
):
396 return self
.dossiers
.filter(date_debut__gt
=today
).order_by('-date_fin')
398 def dossiers_encours(self
):
399 dossiers_p_f
= self
.dossiers_passes() | self
.dossiers_futurs()
400 ids_dossiers_p_f
= [d
.id for d
in dossiers_p_f
]
401 dossiers_encours
= self
.dossiers
.exclude(id__in
=ids_dossiers_p_f
).order_by('-date_fin')
403 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
404 for d
in dossiers_encours
:
405 d
.remunerations
= Remuneration
.objects
.filter(dossier
=d
.id).order_by('-id')
406 return dossiers_encours
408 def postes_encours(self
):
409 postes_encours
= set()
410 for d
in self
.dossiers_encours():
411 postes_encours
.add(d
.poste
)
412 return postes_encours
414 def poste_principal(self
):
416 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
418 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
420 poste
= Poste
.objects
.none()
422 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
427 prefix_implantation
= "dossiers__poste__implantation__region"
428 def get_regions(self
):
430 for d
in self
.dossiers
.all():
431 regions
.append(d
.poste
.implantation
.region
)
435 class EmployePiece(models
.Model
):
436 """Documents relatifs à un employé.
439 employe
= models
.ForeignKey('Employe', db_column
='employe')
440 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
441 fichier
= models
.FileField(verbose_name
="Fichier",
442 upload_to
=employe_piece_dispatch
,
443 storage
=storage_prive
)
447 verbose_name
= u
"Employé pièce"
448 verbose_name_plural
= u
"Employé pièces"
450 def __unicode__(self
):
451 return u
'%s' % (self
.nom
)
453 class EmployeCommentaire(Commentaire
):
454 employe
= models
.ForeignKey('Employe', db_column
='employe',
458 verbose_name
= u
"Employé commentaire"
459 verbose_name_plural
= u
"Employé commentaires"
462 LIEN_PARENTE_CHOICES
= (
463 ('Conjoint', 'Conjoint'),
464 ('Conjointe', 'Conjointe'),
469 class AyantDroit(AUFMetadata
):
470 """Personne en relation avec un Employe.
473 nom
= models
.CharField(max_length
=255)
474 prenom
= models
.CharField(max_length
=255,
475 verbose_name
= u
"Prénom",)
476 nom_affichage
= models
.CharField(max_length
=255,
477 verbose_name
= u
"Nom d'affichage",
478 null
=True, blank
=True)
479 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
480 db_column
='nationalite',
481 related_name
='ayantdroits_nationalite',
482 verbose_name
= u
"Nationalité",
483 null
=True, blank
=True)
484 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
485 help_text
=HELP_TEXT_DATE
,
486 validators
=[validate_date_passee
],
487 null
=True, blank
=True)
488 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
491 employe
= models
.ForeignKey('Employe', db_column
='employe',
492 related_name
='ayantdroits',
493 verbose_name
= u
"Employé")
494 lien_parente
= models
.CharField(max_length
=10,
495 choices
=LIEN_PARENTE_CHOICES
,
496 verbose_name
= u
"Lien de parenté",
497 null
=True, blank
=True)
501 verbose_name
= u
"Ayant droit"
502 verbose_name_plural
= u
"Ayants droit"
504 def __unicode__(self
):
505 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
507 prefix_implantation
= "employe__dossiers__poste__implantation__region"
508 def get_regions(self
):
510 for d
in self
.employe
.dossiers
.all():
511 regions
.append(d
.poste
.implantation
.region
)
515 class AyantDroitCommentaire(Commentaire
):
516 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
522 STATUT_RESIDENCE_CHOICES
= (
524 ('expat', 'Expatrié'),
527 COMPTE_COMPTA_CHOICES
= (
533 class Dossier_(AUFMetadata
):
534 """Le Dossier regroupe les informations relatives à l'occupation
535 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
538 Plusieurs Contrats peuvent être associés au Dossier.
539 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
540 lequel aucun Dossier n'existe est un poste vacant.
543 objects
= DossierManager()
546 statut
= models
.ForeignKey('Statut', related_name
='+', default
=3,
548 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
549 db_column
='organisme_bstg',
551 verbose_name
= u
"Organisme",
552 help_text
="Si détaché (DET) ou \
553 mis à disposition (MAD), \
554 préciser l'organisme.",
555 null
=True, blank
=True)
558 remplacement
= models
.BooleanField(default
=False)
559 remplacement_de
= models
.ForeignKey('self', related_name
='+',
560 help_text
=u
"Taper le nom de l'employé",
561 null
=True, blank
=True)
562 statut_residence
= models
.CharField(max_length
=10, default
='local',
563 verbose_name
= u
"Statut", null
=True,
564 choices
=STATUT_RESIDENCE_CHOICES
)
567 classement
= models
.ForeignKey('Classement', db_column
='classement',
569 null
=True, blank
=True)
570 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
572 default
=REGIME_TRAVAIL_DEFAULT
,
573 verbose_name
= u
"Régime de travail",
574 help_text
="% du temps complet")
575 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
576 decimal_places
=2, null
=True,
577 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
578 verbose_name
= u
"Nb. heures par semaine")
580 # Occupation du Poste par cet Employe (anciennement "mandat")
581 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
583 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
585 null
=True, blank
=True)
592 ordering
= ['employe__nom', ]
593 verbose_name
= u
"Dossier"
594 verbose_name_plural
= "Dossiers"
596 def salaire_theorique(self
):
597 annee
= date
.today().year
598 coeff
= self
.classement
.coefficient
599 implantation
= self
.poste
.implantation
600 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
602 montant
= coeff
* point
.valeur
603 devise
= point
.devise
604 return {'montant':montant
, 'devise':devise
}
606 def __unicode__(self
):
607 poste
= self
.poste
.nom
608 if self
.employe
.genre
== 'F':
609 poste
= self
.poste
.nom_feminin
610 return u
'%s - %s' % (self
.employe
, poste
)
612 prefix_implantation
= "poste__implantation__region"
613 def get_regions(self
):
614 return [self
.poste
.implantation
.region
]
617 def remunerations(self
):
618 return self
.rh_remunerations
.all().order_by('date_debut')
620 def get_salaire(self
):
622 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
626 class Dossier(Dossier_
):
627 __doc__
= Dossier_
.__doc__
628 poste
= models
.ForeignKey('%s.Poste' % app_context(),
630 related_name
='%(app_label)s_dossiers',
631 help_text
=u
"Taper le nom du poste ou du type de poste",
633 employe
= models
.ForeignKey('Employe', db_column
='employe',
634 help_text
=u
"Taper le nom de l'employé",
635 related_name
='%(app_label)s_dossiers',
636 verbose_name
=u
"Employé")
639 class DossierPiece_(models
.Model
):
640 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
641 Ex.: Lettre de motivation.
643 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
644 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
645 fichier
= models
.FileField(verbose_name
= u
"Fichier",
646 upload_to
=dossier_piece_dispatch
,
647 storage
=storage_prive
)
653 def __unicode__(self
):
654 return u
'%s' % (self
.nom
)
656 class DossierPiece(DossierPiece_
):
659 class DossierCommentaire_(Commentaire
):
660 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
664 class DossierCommentaire(DossierCommentaire_
):
667 class DossierComparaison_(models
.Model
):
669 Photo d'une comparaison salariale au moment de l'embauche.
671 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
672 objects
= DossierComparaisonManager()
674 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
675 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
676 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
677 montant
= models
.IntegerField(null
=True)
678 devise
= models
.ForeignKey('Devise', default
=5, related_name
='+', null
=True, blank
=True)
683 def taux_devise(self
):
684 annee
= self
.dossier
.poste
.date_debut
.year
685 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
688 raise Exception(u
"Le taux de la devise %s n'a pas n'existe pas pour %s ou il existe plusieurs taux pour la même année" % (self
.devise
.id, annee
))
692 def montant_euros(self
):
693 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
695 class DossierComparaison(DossierComparaison_
):
700 class RemunerationMixin(AUFMetadata
):
701 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
703 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
705 verbose_name
= u
"Type de rémunération")
706 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
707 db_column
='type_revalorisation',
709 verbose_name
= u
"Type de revalorisation",
710 null
=True, blank
=True)
711 montant
= models
.FloatField(null
=True, blank
=True,
713 # Annuel (12 mois, 52 semaines, 364 jours?)
714 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
715 # commentaire = precision
716 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
717 # date_debut = anciennement date_effectif
718 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
719 null
=True, blank
=True)
720 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
721 null
=True, blank
=True)
725 ordering
= ['type__nom', '-date_fin']
727 def __unicode__(self
):
728 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
730 class Remuneration_(RemunerationMixin
):
731 """Structure de rémunération (données budgétaires) en situation normale
732 pour un Dossier. Si un Evenement existe, utiliser la structure de
733 rémunération EvenementRemuneration de cet événement.
736 def montant_mois(self
):
737 return round(self
.montant
/ 12, 2)
739 def taux_devise(self
):
740 if self
.devise
.code
== "EUR":
743 annee
= datetime
.datetime
.now().year
744 if self
.date_debut
is not None:
745 annee
= self
.date_debut
.year
746 if self
.dossier
.poste
.date_debut
is not None:
747 annee
= self
.dossier
.poste
.date_debut
.year
749 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise_id
, annee
=annee
)]
752 raise Exception(u
"Le taux de la devise %s n'a pas n'existe pas pour %s ou il existe plusieurs taux pour la même année" % (self
.devise
.id, annee
))
756 def montant_euro(self
):
757 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
759 def montant_euro_mois(self
):
760 return round(self
.montant_euro() / 12, 2)
762 def __unicode__(self
):
764 devise
= self
.devise
.code
767 return "%s %s" % (self
.montant
, devise
)
771 verbose_name
= u
"Rémunération"
772 verbose_name_plural
= u
"Rémunérations"
775 class Remuneration(Remuneration_
):
781 class ContratManager(NoDeleteManager
):
782 def get_query_set(self
):
783 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
786 class Contrat_(AUFMetadata
):
787 """Document juridique qui encadre la relation de travail d'un Employe
788 pour un Poste particulier. Pour un Dossier (qui documente cette
789 relation de travail) plusieurs contrats peuvent être associés.
791 objects
= ContratManager()
792 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
793 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
795 verbose_name
= u
"Type de contrat")
796 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
797 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
798 null
=True, blank
=True)
802 ordering
= ['dossier__employe__nom']
803 verbose_name
= u
"Contrat"
804 verbose_name_plural
= u
"Contrats"
806 def __unicode__(self
):
807 return u
'%s - %s' % (self
.dossier
, self
.id)
809 class Contrat(Contrat_
):
815 #class Evenement_(AUFMetadata):
816 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
817 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
818 # (ex.: la Remuneration).
820 # Ex.: congé de maternité, maladie...
822 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
823 # différent et une rémunération en conséquence. On souhaite toutefois
824 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
825 # du retour à la normale.
827 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
829 # nom = models.CharField(max_length=255)
830 # date_debut = models.DateField(verbose_name = u"Date de début")
831 # date_fin = models.DateField(verbose_name = u"Date de fin",
832 # null=True, blank=True)
837 # verbose_name = u"Évènement"
838 # verbose_name_plural = u"Évènements"
840 # def __unicode__(self):
841 # return u'%s' % (self.nom)
844 #class Evenement(Evenement_):
845 # __doc__ = Evenement_.__doc__
848 #class EvenementRemuneration_(RemunerationMixin):
849 # """Structure de rémunération liée à un Evenement qui remplace
850 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
853 # evenement = models.ForeignKey("Evenement", db_column='evenement',
855 # verbose_name = u"Évènement")
856 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
857 # # de l'Evenement associé
861 # ordering = ['evenement', 'type__nom', '-date_fin']
862 # verbose_name = u"Évènement - rémunération"
863 # verbose_name_plural = u"Évènements - rémunérations"
866 #class EvenementRemuneration(EvenementRemuneration_):
867 # __doc__ = EvenementRemuneration_.__doc__
873 #class EvenementRemuneration(EvenementRemuneration_):
874 # __doc__ = EvenementRemuneration_.__doc__
875 # TODO? class ContratPiece(models.Model):
880 class FamilleEmploi(AUFMetadata
):
881 """Catégorie utilisée dans la gestion des Postes.
882 Catégorie supérieure à TypePoste.
884 nom
= models
.CharField(max_length
=255)
888 verbose_name
= u
"Famille d'emploi"
889 verbose_name_plural
= u
"Familles d'emploi"
891 def __unicode__(self
):
892 return u
'%s' % (self
.nom
)
894 class TypePoste(AUFMetadata
):
895 """Catégorie de Poste.
897 nom
= models
.CharField(max_length
=255)
898 nom_feminin
= models
.CharField(max_length
=255,
899 verbose_name
= u
"Nom féminin")
901 is_responsable
= models
.BooleanField(default
=False,
902 verbose_name
= u
"Poste de responsabilité")
903 famille_emploi
= models
.ForeignKey('FamilleEmploi',
904 db_column
='famille_emploi',
906 verbose_name
= u
"Famille d'emploi")
910 verbose_name
= u
"Type de poste"
911 verbose_name_plural
= u
"Types de poste"
913 def __unicode__(self
):
914 return u
'%s' % (self
.nom
)
917 TYPE_PAIEMENT_CHOICES
= (
918 ('Régulier', 'Régulier'),
919 ('Ponctuel', 'Ponctuel'),
922 NATURE_REMUNERATION_CHOICES
= (
923 ('Accessoire', 'Accessoire'),
924 ('Charges', 'Charges'),
925 ('Indemnité', 'Indemnité'),
926 ('RAS', 'Rémunération autre source'),
927 ('Traitement', 'Traitement'),
930 class TypeRemuneration(AUFMetadata
):
931 """Catégorie de Remuneration.
933 nom
= models
.CharField(max_length
=255)
934 type_paiement
= models
.CharField(max_length
=30,
935 choices
=TYPE_PAIEMENT_CHOICES
,
936 verbose_name
= u
"Type de paiement")
937 nature_remuneration
= models
.CharField(max_length
=30,
938 choices
=NATURE_REMUNERATION_CHOICES
,
939 verbose_name
= u
"Nature de la rémunération")
943 verbose_name
= u
"Type de rémunération"
944 verbose_name_plural
= u
"Types de rémunération"
946 def __unicode__(self
):
947 return u
'%s' % (self
.nom
)
949 class TypeRevalorisation(AUFMetadata
):
950 """Justification du changement de la Remuneration.
951 (Actuellement utilisé dans aucun traitement informatique.)
953 nom
= models
.CharField(max_length
=255)
957 verbose_name
= u
"Type de revalorisation"
958 verbose_name_plural
= u
"Types de revalorisation"
960 def __unicode__(self
):
961 return u
'%s' % (self
.nom
)
963 class Service(AUFMetadata
):
964 """Unité administrative où les Postes sont rattachés.
966 nom
= models
.CharField(max_length
=255)
970 verbose_name
= u
"Service"
971 verbose_name_plural
= u
"Services"
973 def __unicode__(self
):
974 return u
'%s' % (self
.nom
)
977 TYPE_ORGANISME_CHOICES
= (
978 ('MAD', 'Mise à disposition'),
979 ('DET', 'Détachement'),
982 class OrganismeBstg(AUFMetadata
):
983 """Organisation d'où provient un Employe mis à disposition (MAD) de
984 ou détaché (DET) à l'AUF à titre gratuit.
986 (BSTG = bien et service à titre gratuit.)
988 nom
= models
.CharField(max_length
=255)
989 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
990 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
992 related_name
='organismes_bstg',
993 null
=True, blank
=True)
996 ordering
= ['type', 'nom']
997 verbose_name
= u
"Organisme BSTG"
998 verbose_name_plural
= u
"Organismes BSTG"
1000 def __unicode__(self
):
1001 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1003 prefix_implantation
= "pays__region"
1004 def get_regions(self
):
1005 return [self
.pays
.region
]
1008 class Statut(AUFMetadata
):
1009 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1012 code
= models
.CharField(max_length
=25, unique
=True, help_text
="Saisir un code court mais lisible pour ce statut : le code est utilisé pour associer les statuts aux autres données tout en demeurant plus lisible qu'un identifiant numérique.")
1013 nom
= models
.CharField(max_length
=255)
1017 verbose_name
= u
"Statut d'employé"
1018 verbose_name_plural
= u
"Statuts d'employé"
1020 def __unicode__(self
):
1021 return u
'%s : %s' % (self
.code
, self
.nom
)
1024 TYPE_CLASSEMENT_CHOICES
= (
1025 ('S', 'S -Soutien'),
1026 ('T', 'T - Technicien'),
1027 ('P', 'P - Professionel'),
1029 ('D', 'D - Direction'),
1030 ('SO', 'SO - Sans objet [expatriés]'),
1031 ('HG', 'HG - Hors grille [direction]'),
1035 class Classement_(AUFMetadata
):
1036 """Éléments de classement de la
1037 "Grille générique de classement hiérarchique".
1039 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1040 classement dans la grille. Le classement donne le coefficient utilisé dans:
1042 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1045 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1046 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1047 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1048 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1051 # annee # au lieu de date_debut et date_fin
1052 commentaire
= models
.TextField(null
=True, blank
=True)
1056 ordering
= ['type','echelon','degre','coefficient']
1057 verbose_name
= u
"Classement"
1058 verbose_name_plural
= u
"Classements"
1060 def __unicode__(self
):
1061 return u
'%s.%s.%s (%s)' % (self
.type, self
.echelon
, self
.degre
,
1064 class Classement(Classement_
):
1065 __doc__
= Classement_
.__doc__
1068 class TauxChange_(AUFMetadata
):
1069 """Taux de change de la devise vers l'euro (EUR)
1070 pour chaque année budgétaire.
1073 devise
= models
.ForeignKey('Devise', db_column
='devise')
1074 annee
= models
.IntegerField(verbose_name
= u
"Année")
1075 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1079 ordering
= ['-annee', 'devise__code']
1080 verbose_name
= u
"Taux de change"
1081 verbose_name_plural
= u
"Taux de change"
1083 def __unicode__(self
):
1084 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1087 class TauxChange(TauxChange_
):
1088 __doc__
= TauxChange_
.__doc__
1090 class ValeurPointManager(NoDeleteManager
):
1092 def get_query_set(self
):
1093 now
= datetime
.datetime
.now()
1094 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1097 class ValeurPoint_(AUFMetadata
):
1098 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1099 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1100 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1102 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1105 actuelles
= ValeurPointManager()
1107 valeur
= models
.FloatField(null
=True)
1108 devise
= models
.ForeignKey('Devise', db_column
='devise', null
=True,
1109 related_name
='+', default
=5)
1110 implantation
= models
.ForeignKey(ref
.Implantation
,
1111 db_column
='implantation',
1112 related_name
='%(app_label)s_valeur_point')
1114 annee
= models
.IntegerField()
1117 ordering
= ['-annee', 'implantation__nom']
1119 verbose_name
= u
"Valeur du point"
1120 verbose_name_plural
= u
"Valeurs du point"
1122 def __unicode__(self
):
1123 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1126 class ValeurPoint(ValeurPoint_
):
1127 __doc__
= ValeurPoint_
.__doc__
1131 class DeviseManager(NoDeleteManager
):
1133 def get_query_set(self
):
1135 return super(DeviseManager
, self
).get_query_set().exclude(id__in
=(3, 15))
1137 class Devise(AUFMetadata
):
1138 """Devise monétaire.
1140 objects
= DeviseManager()
1142 code
= models
.CharField(max_length
=10, unique
=True)
1143 nom
= models
.CharField(max_length
=255)
1147 verbose_name
= u
"Devise"
1148 verbose_name_plural
= u
"Devises"
1150 def __unicode__(self
):
1151 return u
'%s - %s' % (self
.code
, self
.nom
)
1153 class TypeContrat(AUFMetadata
):
1156 nom
= models
.CharField(max_length
=255)
1157 nom_long
= models
.CharField(max_length
=255)
1161 verbose_name
= u
"Type de contrat"
1162 verbose_name_plural
= u
"Types de contrat"
1164 def __unicode__(self
):
1165 return u
'%s' % (self
.nom
)
1170 class ResponsableImplantation(AUFMetadata
):
1171 """Le responsable d'une implantation.
1172 Anciennement géré sur le Dossier du responsable.
1174 employe
= models
.ForeignKey('Employe', db_column
='employe',
1176 null
=True, blank
=True)
1177 implantation
= models
.ForeignKey(ref
.Implantation
,
1178 db_column
='implantation', related_name
='+',
1181 def __unicode__(self
):
1182 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1185 ordering
= ['implantation__nom']
1186 verbose_name
= "Responsable d'implantation"
1187 verbose_name_plural
= "Responsables d'implantation"