1 # -=- encoding: utf-8 -=-
4 from datetime
import date
5 from decimal
import Decimal
7 from django
.core
.files
.storage
import FileSystemStorage
8 from django
.db
import models
9 from django
.conf
import settings
11 from auf
.django
.emploi
.models
import GENRE_CHOICES
, SITUATION_CHOICES
# devrait plutot être dans references
12 from auf
.django
.metadata
.models
import AUFMetadata
13 from auf
.django
.metadata
.managers
import NoDeleteManager
14 import auf
.django
.references
.models
as ref
15 from validators
import validate_date_passee
16 from managers
import PosteManager
, DossierManager
, DossierComparaisonManager
, PosteComparaisonManager
, DeviseManager
19 # Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage.
20 # Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK
23 models_stack
= [s
[1].split('/')[-2] for s
in inspect
.stack() if s
[1].endswith('models.py')]
24 return models_stack
[-1]
28 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
29 REGIME_TRAVAIL_DEFAULT
= Decimal('100.00')
30 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
= Decimal('35.00')
33 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
34 base_url
=settings
.PRIVE_MEDIA_URL
)
36 def poste_piece_dispatch(instance
, filename
):
37 path
= "%s/poste/%s/%s" % (instance
._meta
.app_label
, instance
.poste_id
, filename
)
40 def dossier_piece_dispatch(instance
, filename
):
41 path
= "%s/dossier/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
44 def employe_piece_dispatch(instance
, filename
):
45 path
= "%s/employe/%s/%s" % (instance
._meta
.app_label
, instance
.employe_id
, filename
)
48 def contrat_dispatch(instance
, filename
):
49 path
= "%s/contrat/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
53 class Commentaire(AUFMetadata
):
54 texte
= models
.TextField()
55 owner
= models
.ForeignKey('auth.User', db_column
='owner', related_name
='+', verbose_name
=u
"Commentaire de")
59 ordering
= ['-date_creation']
61 def __unicode__(self
):
62 return u
'%s' % (self
.texte
)
67 POSTE_APPEL_CHOICES
= (
68 ('interne', 'Interne'),
69 ('externe', 'Externe'),
72 class Poste_(AUFMetadata
):
73 """Un Poste est un emploi (job) à combler dans une implantation.
74 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
75 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
78 objects
= PosteManager()
81 nom
= models
.CharField(max_length
=255,
82 verbose_name
= u
"Titre du poste", )
83 nom_feminin
= models
.CharField(max_length
=255,
84 verbose_name
= u
"Titre du poste (au féminin)",
86 implantation
= models
.ForeignKey(ref
.Implantation
, help_text
=u
"Taper le nom de l'implantation ou sa région",
87 db_column
='implantation', related_name
='+')
88 type_poste
= models
.ForeignKey('TypePoste', db_column
='type_poste', help_text
=u
"Taper le nom du type de poste",
91 verbose_name
=u
"type de poste")
92 service
= models
.ForeignKey('Service', db_column
='service',
94 verbose_name
= u
"direction/service/pôle support",
96 responsable
= models
.ForeignKey('Poste', db_column
='responsable',
99 help_text
=u
"Taper le nom du poste ou du type de poste",
100 verbose_name
= u
"Poste du responsable", )
103 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
104 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
105 verbose_name
= u
"Temps de travail",
106 help_text
="% du temps complet")
107 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
108 decimal_places
=2, null
=True,
109 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
110 verbose_name
= u
"Nb. heures par semaine")
113 local
= models
.NullBooleanField(verbose_name
= u
"Local", default
=True,
114 null
=True, blank
=True)
115 expatrie
= models
.NullBooleanField(verbose_name
= u
"Expatrié", default
=False,
116 null
=True, blank
=True)
117 mise_a_disposition
= models
.NullBooleanField(
118 verbose_name
= u
"Mise à disposition",
119 null
=True, default
=False)
120 appel
= models
.CharField(max_length
=10, null
=True,
121 verbose_name
= u
"Appel à candidature",
122 choices
=POSTE_APPEL_CHOICES
,
126 classement_min
= models
.ForeignKey('Classement',
127 db_column
='classement_min', related_name
='+',
128 null
=True, blank
=True)
129 classement_max
= models
.ForeignKey('Classement',
130 db_column
='classement_max', related_name
='+',
131 null
=True, blank
=True)
132 valeur_point_min
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
133 db_column
='valeur_point_min', related_name
='+',
134 null
=True, blank
=True)
135 valeur_point_max
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
136 db_column
='valeur_point_max', related_name
='+',
137 null
=True, blank
=True)
138 devise_min
= models
.ForeignKey('Devise', db_column
='devise_min', null
=True,
140 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
142 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
143 null
=True, default
=0)
144 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
145 null
=True, default
=0)
146 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
147 null
=True, default
=0)
148 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
149 null
=True, default
=0)
150 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
151 null
=True, default
=0)
152 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
153 null
=True, default
=0)
155 # Comparatifs de rémunération
156 devise_comparaison
= models
.ForeignKey('Devise', null
=True, blank
=True,
157 db_column
='devise_comparaison',
159 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
160 null
=True, blank
=True)
161 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
162 null
=True, blank
=True)
163 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
164 null
=True, blank
=True)
165 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
166 null
=True, blank
=True)
167 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
168 null
=True, blank
=True)
169 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
170 null
=True, blank
=True)
171 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
172 null
=True, blank
=True)
173 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
174 null
=True, blank
=True)
175 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
176 null
=True, blank
=True)
177 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
178 null
=True, blank
=True)
181 justification
= models
.TextField(null
=True, blank
=True)
184 date_debut
= models
.DateField(verbose_name
=u
"Date de début", help_text
=HELP_TEXT_DATE
,
185 null
=True, blank
=True)
186 date_fin
= models
.DateField(verbose_name
=u
"Date de fin", help_text
=HELP_TEXT_DATE
,
187 null
=True, blank
=True)
191 ordering
= ['implantation__nom', 'nom']
192 verbose_name
= u
"Poste"
193 verbose_name_plural
= u
"Postes"
196 def __unicode__(self
):
197 representation
= u
'%s - %s [%s]' % (self
.implantation
, self
.nom
,
199 return representation
202 prefix_implantation
= "implantation__region"
203 def get_regions(self
):
204 return [self
.implantation
.region
]
208 __doc__
= Poste_
.__doc__
210 # meta dématérialisation : pour permettre le filtrage
211 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
215 if self
.occupe_par():
219 def occupe_par(self
):
220 """Retourne la liste d'employé occupant ce poste.
221 Généralement, retourne une liste d'un élément.
222 Si poste inoccupé, retourne liste vide.
223 UTILISE pour mettre a jour le flag vacant
225 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
228 POSTE_FINANCEMENT_CHOICES
= (
229 ('A', 'A - Frais de personnel'),
230 ('B', 'B - Projet(s)-Titre(s)'),
235 class PosteFinancement_(models
.Model
):
236 """Pour un Poste, structure d'informations décrivant comment on prévoit
239 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
240 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
241 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
242 help_text
="ex.: 33.33 % (décimale avec point)")
243 commentaire
= models
.TextField(
244 help_text
="Spécifiez la source de financement.")
250 def __unicode__(self
):
251 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
254 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
257 class PosteFinancement(PosteFinancement_
):
261 class PostePiece_(models
.Model
):
262 """Documents relatifs au Poste.
263 Ex.: Description de poste
265 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
266 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
267 fichier
= models
.FileField(verbose_name
= u
"Fichier",
268 upload_to
=poste_piece_dispatch
,
269 storage
=storage_prive
)
275 def __unicode__(self
):
276 return u
'%s' % (self
.nom
)
278 class PostePiece(PostePiece_
):
281 class PosteComparaison_(AUFMetadata
):
283 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
285 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
286 objects
= PosteComparaisonManager()
288 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
289 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
290 montant
= models
.IntegerField(null
=True)
291 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
296 def taux_devise(self
):
297 if self
.devise
.code
== "EUR":
299 annee
= self
.poste
.date_debut
.year
300 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
303 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
))
307 def montant_euros(self
):
308 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
310 def __unicode__(self
):
313 class PosteComparaison(PosteComparaison_
):
316 class PosteCommentaire_(Commentaire
):
317 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
322 class PosteCommentaire(PosteCommentaire_
):
327 class Employe(AUFMetadata
):
328 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
329 Dossiers qu'il occupe ou a occupé de Postes.
331 Cette classe aurait pu avantageusement s'appeler Personne car la notion
332 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
335 nom
= models
.CharField(max_length
=255)
336 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
337 nom_affichage
= models
.CharField(max_length
=255,
338 verbose_name
= u
"Nom d'affichage",
339 null
=True, blank
=True)
340 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
341 db_column
='nationalite',
342 related_name
='employes_nationalite',
343 verbose_name
= u
"Nationalité",
344 blank
=True, null
=True)
345 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
346 help_text
=HELP_TEXT_DATE
,
347 validators
=[validate_date_passee
],
348 null
=True, blank
=True)
349 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
352 situation_famille
= models
.CharField(max_length
=1,
353 choices
=SITUATION_CHOICES
,
354 verbose_name
= u
"Situation familiale",
355 null
=True, blank
=True)
356 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
357 help_text
=HELP_TEXT_DATE
,
358 null
=True, blank
=True)
361 tel_domicile
= models
.CharField(max_length
=255,
362 verbose_name
= u
"Tél. domicile",
363 null
=True, blank
=True)
364 tel_cellulaire
= models
.CharField(max_length
=255,
365 verbose_name
= u
"Tél. cellulaire",
366 null
=True, blank
=True)
367 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
368 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
369 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
370 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
371 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
372 related_name
='employes',
373 null
=True, blank
=True)
375 # meta dématérialisation : pour permettre le filtrage
376 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
379 ordering
= ['nom','prenom']
380 verbose_name
= u
"Employé"
381 verbose_name_plural
= u
"Employés"
383 def __unicode__(self
):
384 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
388 if self
.genre
.upper() == u
'M':
390 elif self
.genre
.upper() == u
'F':
395 """Retourne l'URL du service retournant la photo de l'Employe.
396 Équivalent reverse url 'rh_photo' avec id en param.
398 from django
.core
.urlresolvers
import reverse
399 return reverse('rh_photo', kwargs
={'id':self
.id})
401 def dossiers_passes(self
):
403 dossiers_passes
= self
.dossiers
.filter(date_fin__lt
=today
).order_by('-date_fin')
404 for d
in dossiers_passes
:
406 return dossiers_passes
408 def dossiers_futurs(self
):
410 return self
.dossiers
.filter(date_debut__gt
=today
).order_by('-date_fin')
412 def dossiers_encours(self
):
413 dossiers_p_f
= self
.dossiers_passes() | self
.dossiers_futurs()
414 ids_dossiers_p_f
= [d
.id for d
in dossiers_p_f
]
415 dossiers_encours
= self
.dossiers
.exclude(id__in
=ids_dossiers_p_f
).order_by('-date_fin')
417 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
418 for d
in dossiers_encours
:
419 d
.remunerations
= Remuneration
.objects
.filter(dossier
=d
.id).order_by('-id')
420 return dossiers_encours
422 def postes_encours(self
):
423 postes_encours
= set()
424 for d
in self
.dossiers_encours():
425 postes_encours
.add(d
.poste
)
426 return postes_encours
428 def poste_principal(self
):
430 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
432 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
434 poste
= Poste
.objects
.none()
436 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
441 prefix_implantation
= "rh_dossiers__poste__implantation__region"
442 def get_regions(self
):
444 for d
in self
.dossiers
.all():
445 regions
.append(d
.poste
.implantation
.region
)
449 class EmployePiece(models
.Model
):
450 """Documents relatifs à un employé.
453 employe
= models
.ForeignKey('Employe', db_column
='employe')
454 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
455 fichier
= models
.FileField(verbose_name
="Fichier",
456 upload_to
=employe_piece_dispatch
,
457 storage
=storage_prive
)
461 verbose_name
= u
"Employé pièce"
462 verbose_name_plural
= u
"Employé pièces"
464 def __unicode__(self
):
465 return u
'%s' % (self
.nom
)
467 class EmployeCommentaire(Commentaire
):
468 employe
= models
.ForeignKey('Employe', db_column
='employe',
472 verbose_name
= u
"Employé commentaire"
473 verbose_name_plural
= u
"Employé commentaires"
476 LIEN_PARENTE_CHOICES
= (
477 ('Conjoint', 'Conjoint'),
478 ('Conjointe', 'Conjointe'),
483 class AyantDroit(AUFMetadata
):
484 """Personne en relation avec un Employe.
487 nom
= models
.CharField(max_length
=255)
488 prenom
= models
.CharField(max_length
=255,
489 verbose_name
= u
"Prénom",)
490 nom_affichage
= models
.CharField(max_length
=255,
491 verbose_name
= u
"Nom d'affichage",
492 null
=True, blank
=True)
493 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
494 db_column
='nationalite',
495 related_name
='ayantdroits_nationalite',
496 verbose_name
= u
"Nationalité",
497 null
=True, blank
=True)
498 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
499 help_text
=HELP_TEXT_DATE
,
500 validators
=[validate_date_passee
],
501 null
=True, blank
=True)
502 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
505 employe
= models
.ForeignKey('Employe', db_column
='employe',
506 related_name
='ayantdroits',
507 verbose_name
= u
"Employé")
508 lien_parente
= models
.CharField(max_length
=10,
509 choices
=LIEN_PARENTE_CHOICES
,
510 verbose_name
= u
"Lien de parenté",
511 null
=True, blank
=True)
515 verbose_name
= u
"Ayant droit"
516 verbose_name_plural
= u
"Ayants droit"
518 def __unicode__(self
):
519 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
521 prefix_implantation
= "employe__dossiers__poste__implantation__region"
522 def get_regions(self
):
524 for d
in self
.employe
.dossiers
.all():
525 regions
.append(d
.poste
.implantation
.region
)
529 class AyantDroitCommentaire(Commentaire
):
530 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
536 STATUT_RESIDENCE_CHOICES
= (
538 ('expat', 'Expatrié'),
541 COMPTE_COMPTA_CHOICES
= (
547 class Dossier_(AUFMetadata
):
548 """Le Dossier regroupe les informations relatives à l'occupation
549 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
552 Plusieurs Contrats peuvent être associés au Dossier.
553 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
554 lequel aucun Dossier n'existe est un poste vacant.
557 objects
= DossierManager()
560 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
561 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
562 db_column
='organisme_bstg',
564 verbose_name
= u
"Organisme",
565 help_text
="Si détaché (DET) ou \
566 mis à disposition (MAD), \
567 préciser l'organisme.",
568 null
=True, blank
=True)
571 remplacement
= models
.BooleanField(default
=False)
572 remplacement_de
= models
.ForeignKey('self', related_name
='+',
573 help_text
=u
"Taper le nom de l'employé",
574 null
=True, blank
=True)
575 statut_residence
= models
.CharField(max_length
=10, default
='local',
576 verbose_name
= u
"Statut", null
=True,
577 choices
=STATUT_RESIDENCE_CHOICES
)
580 classement
= models
.ForeignKey('Classement', db_column
='classement',
582 null
=True, blank
=True)
583 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
585 default
=REGIME_TRAVAIL_DEFAULT
,
586 verbose_name
= u
"Régime de travail",
587 help_text
="% du temps complet")
588 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
589 decimal_places
=2, null
=True,
590 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
591 verbose_name
= u
"Nb. heures par semaine")
593 # Occupation du Poste par cet Employe (anciennement "mandat")
594 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
596 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
598 null
=True, blank
=True)
605 ordering
= ['employe__nom', ]
606 verbose_name
= u
"Dossier"
607 verbose_name_plural
= "Dossiers"
609 def salaire_theorique(self
):
610 annee
= date
.today().year
611 coeff
= self
.classement
.coefficient
612 implantation
= self
.poste
.implantation
613 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
615 montant
= coeff
* point
.valeur
616 devise
= point
.devise
617 return {'montant':montant
, 'devise':devise
}
619 def __unicode__(self
):
620 poste
= self
.poste
.nom
621 if self
.employe
.genre
== 'F':
622 poste
= self
.poste
.nom_feminin
623 return u
'%s - %s' % (self
.employe
, poste
)
625 prefix_implantation
= "poste__implantation__region"
626 def get_regions(self
):
627 return [self
.poste
.implantation
.region
]
630 def remunerations(self
):
631 return self
.rh_remunerations
.all().order_by('date_debut')
633 def remunerations_en_cours(self
):
634 return self
.rh_remunerations
.all().filter(date_fin__exact
=None).order_by('date_debut')
636 def get_salaire(self
):
638 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
643 class Dossier(Dossier_
):
644 __doc__
= Dossier_
.__doc__
645 poste
= models
.ForeignKey('%s.Poste' % app_context(),
647 related_name
='%(app_label)s_dossiers',
648 help_text
=u
"Taper le nom du poste ou du type de poste",
650 employe
= models
.ForeignKey('Employe', db_column
='employe',
651 help_text
=u
"Taper le nom de l'employé",
652 related_name
='%(app_label)s_dossiers',
653 verbose_name
=u
"Employé")
656 class DossierPiece_(models
.Model
):
657 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
658 Ex.: Lettre de motivation.
660 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_dossierpieces')
661 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
662 fichier
= models
.FileField(verbose_name
= u
"Fichier",
663 upload_to
=dossier_piece_dispatch
,
664 storage
=storage_prive
)
670 def __unicode__(self
):
671 return u
'%s' % (self
.nom
)
673 class DossierPiece(DossierPiece_
):
676 class DossierCommentaire_(Commentaire
):
677 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
681 class DossierCommentaire(DossierCommentaire_
):
684 class DossierComparaison_(models
.Model
):
686 Photo d'une comparaison salariale au moment de l'embauche.
688 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
689 objects
= DossierComparaisonManager()
691 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
692 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
693 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
694 montant
= models
.IntegerField(null
=True)
695 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
700 def taux_devise(self
):
701 annee
= self
.dossier
.contrat_date_debut
.year
702 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
705 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
))
709 def montant_euros(self
):
710 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
712 class DossierComparaison(DossierComparaison_
):
717 class RemunerationMixin(AUFMetadata
):
718 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
720 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
722 verbose_name
= u
"Type de rémunération")
723 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
724 db_column
='type_revalorisation',
726 verbose_name
= u
"Type de revalorisation",
727 null
=True, blank
=True)
728 montant
= models
.DecimalField(null
=True, blank
=True,
729 default
=0, max_digits
=12, decimal_places
=2)
730 # Annuel (12 mois, 52 semaines, 364 jours?)
731 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
732 # commentaire = precision
733 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
734 # date_debut = anciennement date_effectif
735 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
736 null
=True, blank
=True)
737 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
738 null
=True, blank
=True)
742 ordering
= ['type__nom', '-date_fin']
744 def __unicode__(self
):
745 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
747 class Remuneration_(RemunerationMixin
):
748 """Structure de rémunération (données budgétaires) en situation normale
749 pour un Dossier. Si un Evenement existe, utiliser la structure de
750 rémunération EvenementRemuneration de cet événement.
753 def montant_mois(self
):
754 return round(self
.montant
/ 12, 2)
756 def taux_devise(self
):
757 if self
.devise
.code
== "EUR":
760 annee
= datetime
.datetime
.now().year
761 if self
.dossier
.poste
.date_debut
is not None:
762 annee
= self
.dossier
.poste
.date_debut
.year
763 if self
.dossier
.date_debut
is not None:
764 annee
= self
.dossier
.date_debut
.year
765 if self
.date_debut
is not None:
766 annee
= self
.date_debut
.year
768 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise_id
, annee
=annee
)]
771 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 %s (%s)" % (self
.devise
.code
, annee
, taux
, self
.dossier
))
775 def montant_euro(self
):
776 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
778 def montant_euro_mois(self
):
779 return round(self
.montant_euro() / 12, 2)
781 def __unicode__(self
):
783 devise
= self
.devise
.code
786 return "%s %s" % (self
.montant
, devise
)
790 verbose_name
= u
"Rémunération"
791 verbose_name_plural
= u
"Rémunérations"
794 class Remuneration(Remuneration_
):
800 class ContratManager(NoDeleteManager
):
801 def get_query_set(self
):
802 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
805 class Contrat_(AUFMetadata
):
806 """Document juridique qui encadre la relation de travail d'un Employe
807 pour un Poste particulier. Pour un Dossier (qui documente cette
808 relation de travail) plusieurs contrats peuvent être associés.
810 objects
= ContratManager()
811 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
812 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
814 verbose_name
= u
"type de contrat")
815 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
816 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
817 null
=True, blank
=True)
818 fichier
= models
.FileField(verbose_name
= u
"Fichier",
819 upload_to
=contrat_dispatch
,
820 storage
=storage_prive
,
821 null
=True, blank
=True)
825 ordering
= ['dossier__employe__nom']
826 verbose_name
= u
"Contrat"
827 verbose_name_plural
= u
"Contrats"
829 def __unicode__(self
):
830 return u
'%s - %s' % (self
.dossier
, self
.id)
832 class Contrat(Contrat_
):
838 #class Evenement_(AUFMetadata):
839 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
840 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
841 # (ex.: la Remuneration).
843 # Ex.: congé de maternité, maladie...
845 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
846 # différent et une rémunération en conséquence. On souhaite toutefois
847 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
848 # du retour à la normale.
850 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
852 # nom = models.CharField(max_length=255)
853 # date_debut = models.DateField(verbose_name = u"Date de début")
854 # date_fin = models.DateField(verbose_name = u"Date de fin",
855 # null=True, blank=True)
860 # verbose_name = u"Évènement"
861 # verbose_name_plural = u"Évènements"
863 # def __unicode__(self):
864 # return u'%s' % (self.nom)
867 #class Evenement(Evenement_):
868 # __doc__ = Evenement_.__doc__
871 #class EvenementRemuneration_(RemunerationMixin):
872 # """Structure de rémunération liée à un Evenement qui remplace
873 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
876 # evenement = models.ForeignKey("Evenement", db_column='evenement',
878 # verbose_name = u"Évènement")
879 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
880 # # de l'Evenement associé
884 # ordering = ['evenement', 'type__nom', '-date_fin']
885 # verbose_name = u"Évènement - rémunération"
886 # verbose_name_plural = u"Évènements - rémunérations"
889 #class EvenementRemuneration(EvenementRemuneration_):
890 # __doc__ = EvenementRemuneration_.__doc__
896 #class EvenementRemuneration(EvenementRemuneration_):
897 # __doc__ = EvenementRemuneration_.__doc__
898 # TODO? class ContratPiece(models.Model):
903 class FamilleEmploi(AUFMetadata
):
904 """Catégorie utilisée dans la gestion des Postes.
905 Catégorie supérieure à TypePoste.
907 nom
= models
.CharField(max_length
=255)
911 verbose_name
= u
"Famille d'emploi"
912 verbose_name_plural
= u
"Familles d'emploi"
914 def __unicode__(self
):
915 return u
'%s' % (self
.nom
)
917 class TypePoste(AUFMetadata
):
918 """Catégorie de Poste.
920 nom
= models
.CharField(max_length
=255)
921 nom_feminin
= models
.CharField(max_length
=255,
922 verbose_name
= u
"Nom féminin")
924 is_responsable
= models
.BooleanField(default
=False,
925 verbose_name
= u
"Poste de responsabilité")
926 famille_emploi
= models
.ForeignKey('FamilleEmploi',
927 db_column
='famille_emploi',
929 verbose_name
= u
"famille d'emploi")
933 verbose_name
= u
"Type de poste"
934 verbose_name_plural
= u
"Types de poste"
936 def __unicode__(self
):
937 return u
'%s' % (self
.nom
)
940 TYPE_PAIEMENT_CHOICES
= (
941 (u
'Régulier', u
'Régulier'),
942 (u
'Ponctuel', u
'Ponctuel'),
945 NATURE_REMUNERATION_CHOICES
= (
946 (u
'Accessoire', u
'Accessoire'),
947 (u
'Charges', u
'Charges'),
948 (u
'Indemnité', u
'Indemnité'),
949 (u
'RAS', u
'Rémunération autre source'),
950 (u
'Traitement', u
'Traitement'),
953 class TypeRemuneration(AUFMetadata
):
954 """Catégorie de Remuneration.
956 nom
= models
.CharField(max_length
=255)
957 type_paiement
= models
.CharField(max_length
=30,
958 choices
=TYPE_PAIEMENT_CHOICES
,
959 verbose_name
= u
"Type de paiement")
960 nature_remuneration
= models
.CharField(max_length
=30,
961 choices
=NATURE_REMUNERATION_CHOICES
,
962 verbose_name
= u
"Nature de la rémunération")
966 verbose_name
= u
"Type de rémunération"
967 verbose_name_plural
= u
"Types de rémunération"
969 def __unicode__(self
):
970 return u
'%s' % (self
.nom
)
972 class TypeRevalorisation(AUFMetadata
):
973 """Justification du changement de la Remuneration.
974 (Actuellement utilisé dans aucun traitement informatique.)
976 nom
= models
.CharField(max_length
=255)
980 verbose_name
= u
"Type de revalorisation"
981 verbose_name_plural
= u
"Types de revalorisation"
983 def __unicode__(self
):
984 return u
'%s' % (self
.nom
)
986 class Service(AUFMetadata
):
987 """Unité administrative où les Postes sont rattachés.
989 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
990 nom
= models
.CharField(max_length
=255)
994 verbose_name
= u
"Service"
995 verbose_name_plural
= u
"Services"
997 def __unicode__(self
):
999 archive
= u
"(archivé)"
1002 return u
'%s %s' % (self
.nom
, archive
)
1005 TYPE_ORGANISME_CHOICES
= (
1006 ('MAD', 'Mise à disposition'),
1007 ('DET', 'Détachement'),
1010 class OrganismeBstg(AUFMetadata
):
1011 """Organisation d'où provient un Employe mis à disposition (MAD) de
1012 ou détaché (DET) à l'AUF à titre gratuit.
1014 (BSTG = bien et service à titre gratuit.)
1016 nom
= models
.CharField(max_length
=255)
1017 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1018 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1020 related_name
='organismes_bstg',
1021 null
=True, blank
=True)
1024 ordering
= ['type', 'nom']
1025 verbose_name
= u
"Organisme BSTG"
1026 verbose_name_plural
= u
"Organismes BSTG"
1028 def __unicode__(self
):
1029 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1031 prefix_implantation
= "pays__region"
1032 def get_regions(self
):
1033 return [self
.pays
.region
]
1036 class Statut(AUFMetadata
):
1037 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1040 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.")
1041 nom
= models
.CharField(max_length
=255)
1045 verbose_name
= u
"Statut d'employé"
1046 verbose_name_plural
= u
"Statuts d'employé"
1048 def __unicode__(self
):
1049 return u
'%s : %s' % (self
.code
, self
.nom
)
1052 TYPE_CLASSEMENT_CHOICES
= (
1053 ('S', 'S -Soutien'),
1054 ('T', 'T - Technicien'),
1055 ('P', 'P - Professionel'),
1057 ('D', 'D - Direction'),
1058 ('SO', 'SO - Sans objet [expatriés]'),
1059 ('HG', 'HG - Hors grille [direction]'),
1062 class ClassementManager(models
.Manager
):
1064 Ordonner les spcéfiquement les classements.
1066 def get_query_set(self
):
1067 qs
= super(self
.__class__
, self
).get_query_set()
1068 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1069 qs
= qs
.extra(order_by
=('ponderation', ))
1073 class Classement_(AUFMetadata
):
1074 """Éléments de classement de la
1075 "Grille générique de classement hiérarchique".
1077 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1078 classement dans la grille. Le classement donne le coefficient utilisé dans:
1080 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1082 objects
= ClassementManager()
1085 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1086 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1087 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1088 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1091 # annee # au lieu de date_debut et date_fin
1092 commentaire
= models
.TextField(null
=True, blank
=True)
1096 ordering
= ['type','echelon','degre','coefficient']
1097 verbose_name
= u
"Classement"
1098 verbose_name_plural
= u
"Classements"
1100 def __unicode__(self
):
1101 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1103 class Classement(Classement_
):
1104 __doc__
= Classement_
.__doc__
1107 class TauxChange_(AUFMetadata
):
1108 """Taux de change de la devise vers l'euro (EUR)
1109 pour chaque année budgétaire.
1112 devise
= models
.ForeignKey('Devise', db_column
='devise')
1113 annee
= models
.IntegerField(verbose_name
= u
"Année")
1114 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1118 ordering
= ['-annee', 'devise__code']
1119 verbose_name
= u
"Taux de change"
1120 verbose_name_plural
= u
"Taux de change"
1122 def __unicode__(self
):
1123 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1126 class TauxChange(TauxChange_
):
1127 __doc__
= TauxChange_
.__doc__
1129 class ValeurPointManager(NoDeleteManager
):
1131 def get_query_set(self
):
1132 now
= datetime
.datetime
.now()
1133 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1136 class ValeurPoint_(AUFMetadata
):
1137 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1138 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1139 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1141 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1144 actuelles
= ValeurPointManager()
1146 valeur
= models
.FloatField(null
=True)
1147 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1148 implantation
= models
.ForeignKey(ref
.Implantation
,
1149 db_column
='implantation',
1150 related_name
='%(app_label)s_valeur_point')
1152 annee
= models
.IntegerField()
1155 ordering
= ['-annee', 'implantation__nom']
1157 verbose_name
= u
"Valeur du point"
1158 verbose_name_plural
= u
"Valeurs du point"
1160 def __unicode__(self
):
1161 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1164 class ValeurPoint(ValeurPoint_
):
1165 __doc__
= ValeurPoint_
.__doc__
1169 class Devise(AUFMetadata
):
1170 """Devise monétaire.
1173 objects
= DeviseManager()
1175 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1176 code
= models
.CharField(max_length
=10, unique
=True)
1177 nom
= models
.CharField(max_length
=255)
1181 verbose_name
= u
"Devise"
1182 verbose_name_plural
= u
"Devises"
1184 def __unicode__(self
):
1185 return u
'%s - %s' % (self
.code
, self
.nom
)
1187 class TypeContrat(AUFMetadata
):
1190 nom
= models
.CharField(max_length
=255)
1191 nom_long
= models
.CharField(max_length
=255)
1195 verbose_name
= u
"Type de contrat"
1196 verbose_name_plural
= u
"Types de contrat"
1198 def __unicode__(self
):
1199 return u
'%s' % (self
.nom
)
1204 class ResponsableImplantation(AUFMetadata
):
1205 """Le responsable d'une implantation.
1206 Anciennement géré sur le Dossier du responsable.
1208 employe
= models
.ForeignKey('Employe', db_column
='employe',
1210 null
=True, blank
=True)
1211 implantation
= models
.ForeignKey(ref
.Implantation
,
1212 db_column
='implantation', related_name
='+',
1215 def __unicode__(self
):
1216 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1219 ordering
= ['implantation__nom']
1220 verbose_name
= "Responsable d'implantation"
1221 verbose_name_plural
= "Responsables d'implantation"