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
20 # Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage.
21 # Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK
24 models_stack
= [s
[1].split('/')[-2] for s
in inspect
.stack() if s
[1].endswith('models.py')]
25 return models_stack
[-1]
29 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
30 REGIME_TRAVAIL_DEFAULT
= Decimal('100.00')
31 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
= Decimal('35.00')
34 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
35 base_url
=settings
.PRIVE_MEDIA_URL
)
37 def poste_piece_dispatch(instance
, filename
):
38 path
= "poste/%s/%s" % (instance
.poste_id
, filename
)
41 def dossier_piece_dispatch(instance
, filename
):
42 path
= "dossier/%s/%s" % (instance
.dossier_id
, filename
)
45 def employe_piece_dispatch(instance
, filename
):
46 path
= "employe/%s/%s" % (instance
.employe_id
, filename
)
49 def contrat_dispatch(instance
, filename
):
50 path
= "contrat/%s/%s" % (instance
.dossier_id
, filename
)
54 class Commentaire(AUFMetadata
):
55 texte
= models
.TextField()
56 owner
= models
.ForeignKey('auth.User', db_column
='owner', related_name
='+', verbose_name
=u
"Commentaire de")
60 ordering
= ['-date_creation']
62 def __unicode__(self
):
63 return u
'%s' % (self
.texte
)
68 POSTE_APPEL_CHOICES
= (
69 ('interne', 'Interne'),
70 ('externe', 'Externe'),
73 class Poste_(AUFMetadata
):
74 """Un Poste est un emploi (job) à combler dans une implantation.
75 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
76 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
79 objects
= PosteManager()
82 nom
= models
.CharField(max_length
=255,
83 verbose_name
= u
"Titre du poste", )
84 nom_feminin
= models
.CharField(max_length
=255,
85 verbose_name
= u
"Titre du poste (au féminin)",
87 implantation
= models
.ForeignKey(ref
.Implantation
, help_text
=u
"Taper le nom de l'implantation ou sa région",
88 db_column
='implantation', related_name
='+')
89 type_poste
= models
.ForeignKey('TypePoste', db_column
='type_poste', help_text
=u
"Taper le nom du type de poste",
92 verbose_name
=u
"type de poste")
93 service
= models
.ForeignKey('Service', db_column
='service',
95 verbose_name
= u
"direction/service/pôle support", )
96 responsable
= models
.ForeignKey('Poste', db_column
='responsable',
97 related_name
='+', null
=True,help_text
=u
"Taper le nom du poste ou du type de poste",
98 verbose_name
= u
"Poste du responsable", )
101 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
102 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
103 verbose_name
= u
"Temps de travail",
104 help_text
="% du temps complet")
105 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
106 decimal_places
=2, null
=True,
107 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
108 verbose_name
= u
"Nb. heures par semaine")
111 local
= models
.NullBooleanField(verbose_name
= u
"Local", default
=True,
112 null
=True, blank
=True)
113 expatrie
= models
.NullBooleanField(verbose_name
= u
"Expatrié", default
=False,
114 null
=True, blank
=True)
115 mise_a_disposition
= models
.NullBooleanField(
116 verbose_name
= u
"Mise à disposition",
117 null
=True, default
=False)
118 appel
= models
.CharField(max_length
=10, null
=True,
119 verbose_name
= u
"Appel à candidature",
120 choices
=POSTE_APPEL_CHOICES
,
124 classement_min
= models
.ForeignKey('Classement',
125 db_column
='classement_min', related_name
='+',
126 null
=True, blank
=True)
127 classement_max
= models
.ForeignKey('Classement',
128 db_column
='classement_max', related_name
='+',
129 null
=True, blank
=True)
130 valeur_point_min
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
131 db_column
='valeur_point_min', related_name
='+',
132 null
=True, blank
=True)
133 valeur_point_max
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
134 db_column
='valeur_point_max', related_name
='+',
135 null
=True, blank
=True)
136 devise_min
= models
.ForeignKey('Devise', db_column
='devise_min', null
=True,
138 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
140 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
141 null
=True, default
=0)
142 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
143 null
=True, default
=0)
144 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
145 null
=True, default
=0)
146 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
147 null
=True, default
=0)
148 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
149 null
=True, default
=0)
150 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
151 null
=True, default
=0)
153 # Comparatifs de rémunération
154 devise_comparaison
= models
.ForeignKey('Devise', null
=True,
155 db_column
='devise_comparaison',
157 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
158 null
=True, blank
=True)
159 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
160 null
=True, blank
=True)
161 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
162 null
=True, blank
=True)
163 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
164 null
=True, blank
=True)
165 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
166 null
=True, blank
=True)
167 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
168 null
=True, blank
=True)
169 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
170 null
=True, blank
=True)
171 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
172 null
=True, blank
=True)
173 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
174 null
=True, blank
=True)
175 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
176 null
=True, blank
=True)
179 justification
= models
.TextField(null
=True, blank
=True)
182 date_debut
= models
.DateField(verbose_name
=u
"Date de début",
183 null
=True, blank
=True)
184 date_fin
= models
.DateField(verbose_name
=u
"Date de fin",
185 null
=True, blank
=True)
189 ordering
= ['implantation__nom', 'nom']
190 verbose_name
= u
"Poste"
191 verbose_name_plural
= u
"Postes"
194 def __unicode__(self
):
195 representation
= u
'%s - %s [%s]' % (self
.implantation
, self
.nom
,
197 return representation
200 prefix_implantation
= "implantation__region"
201 def get_regions(self
):
202 return [self
.implantation
.region
]
206 __doc__
= Poste_
.__doc__
208 # meta dématérialisation : pour permettre le filtrage
209 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
213 if self
.occupe_par():
217 def occupe_par(self
):
218 """Retourne la liste d'employé occupant ce poste.
219 Généralement, retourne une liste d'un élément.
220 Si poste inoccupé, retourne liste vide.
221 UTILISE pour mettre a jour le flag vacant
223 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
226 POSTE_FINANCEMENT_CHOICES
= (
227 ('A', 'A - Frais de personnel'),
228 ('B', 'B - Projet(s)-Titre(s)'),
233 class PosteFinancement_(models
.Model
):
234 """Pour un Poste, structure d'informations décrivant comment on prévoit
237 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
238 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
239 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
240 help_text
="ex.: 33.33 % (décimale avec point)")
241 commentaire
= models
.TextField(
242 help_text
="Spécifiez la source de financement.")
248 def __unicode__(self
):
249 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
252 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
255 class PosteFinancement(PosteFinancement_
):
259 class PostePiece_(models
.Model
):
260 """Documents relatifs au Poste.
261 Ex.: Description de poste
263 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
264 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
265 fichier
= models
.FileField(verbose_name
= u
"Fichier",
266 upload_to
=poste_piece_dispatch
,
267 storage
=storage_prive
)
273 def __unicode__(self
):
274 return u
'%s' % (self
.nom
)
276 class PostePiece(PostePiece_
):
279 class PosteComparaison_(AUFMetadata
):
281 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
283 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
284 objects
= PosteComparaisonManager()
286 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
287 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
288 montant
= models
.IntegerField(null
=True)
289 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
294 def taux_devise(self
):
295 if self
.devise
.code
== "EUR":
297 annee
= self
.poste
.date_debut
.year
298 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
301 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
))
305 def montant_euros(self
):
306 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
308 class PosteComparaison(PosteComparaison_
):
311 class PosteCommentaire_(Commentaire
):
312 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
317 class PosteCommentaire(PosteCommentaire_
):
322 class Employe(AUFMetadata
):
323 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
324 Dossiers qu'il occupe ou a occupé de Postes.
326 Cette classe aurait pu avantageusement s'appeler Personne car la notion
327 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
330 nom
= models
.CharField(max_length
=255)
331 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
332 nom_affichage
= models
.CharField(max_length
=255,
333 verbose_name
= u
"Nom d'affichage",
334 null
=True, blank
=True)
335 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
336 db_column
='nationalite',
337 related_name
='employes_nationalite',
338 verbose_name
= u
"Nationalité",
339 blank
=True, null
=True)
340 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
341 help_text
=HELP_TEXT_DATE
,
342 validators
=[validate_date_passee
],
343 null
=True, blank
=True)
344 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
347 situation_famille
= models
.CharField(max_length
=1,
348 choices
=SITUATION_CHOICES
,
349 verbose_name
= u
"Situation familiale",
350 null
=True, blank
=True)
351 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
352 help_text
=HELP_TEXT_DATE
,
353 null
=True, blank
=True)
356 tel_domicile
= models
.CharField(max_length
=255,
357 verbose_name
= u
"Tél. domicile",
358 null
=True, blank
=True)
359 tel_cellulaire
= models
.CharField(max_length
=255,
360 verbose_name
= u
"Tél. cellulaire",
361 null
=True, blank
=True)
362 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
363 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
364 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
365 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
366 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
367 related_name
='employes',
368 null
=True, blank
=True)
370 # meta dématérialisation : pour permettre le filtrage
371 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
374 ordering
= ['nom','prenom']
375 verbose_name
= u
"Employé"
376 verbose_name_plural
= u
"Employés"
378 def __unicode__(self
):
379 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
383 if self
.genre
.upper() == u
'M':
385 elif self
.genre
.upper() == u
'F':
390 """Retourne l'URL du service retournant la photo de l'Employe.
391 Équivalent reverse url 'rh_photo' avec id en param.
393 from django
.core
.urlresolvers
import reverse
394 return reverse('rh_photo', kwargs
={'id':self
.id})
396 def dossiers_passes(self
):
398 dossiers_passes
= self
.dossiers
.filter(date_fin__lt
=today
).order_by('-date_fin')
399 for d
in dossiers_passes
:
401 return dossiers_passes
403 def dossiers_futurs(self
):
405 return self
.dossiers
.filter(date_debut__gt
=today
).order_by('-date_fin')
407 def dossiers_encours(self
):
408 dossiers_p_f
= self
.dossiers_passes() | self
.dossiers_futurs()
409 ids_dossiers_p_f
= [d
.id for d
in dossiers_p_f
]
410 dossiers_encours
= self
.dossiers
.exclude(id__in
=ids_dossiers_p_f
).order_by('-date_fin')
412 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
413 for d
in dossiers_encours
:
414 d
.remunerations
= Remuneration
.objects
.filter(dossier
=d
.id).order_by('-id')
415 return dossiers_encours
417 def postes_encours(self
):
418 postes_encours
= set()
419 for d
in self
.dossiers_encours():
420 postes_encours
.add(d
.poste
)
421 return postes_encours
423 def poste_principal(self
):
425 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
427 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
429 poste
= Poste
.objects
.none()
431 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
436 prefix_implantation
= "rh_dossiers__poste__implantation__region"
437 def get_regions(self
):
439 for d
in self
.dossiers
.all():
440 regions
.append(d
.poste
.implantation
.region
)
444 class EmployePiece(models
.Model
):
445 """Documents relatifs à un employé.
448 employe
= models
.ForeignKey('Employe', db_column
='employe')
449 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
450 fichier
= models
.FileField(verbose_name
="Fichier",
451 upload_to
=employe_piece_dispatch
,
452 storage
=storage_prive
)
456 verbose_name
= u
"Employé pièce"
457 verbose_name_plural
= u
"Employé pièces"
459 def __unicode__(self
):
460 return u
'%s' % (self
.nom
)
462 class EmployeCommentaire(Commentaire
):
463 employe
= models
.ForeignKey('Employe', db_column
='employe',
467 verbose_name
= u
"Employé commentaire"
468 verbose_name_plural
= u
"Employé commentaires"
471 LIEN_PARENTE_CHOICES
= (
472 ('Conjoint', 'Conjoint'),
473 ('Conjointe', 'Conjointe'),
478 class AyantDroit(AUFMetadata
):
479 """Personne en relation avec un Employe.
482 nom
= models
.CharField(max_length
=255)
483 prenom
= models
.CharField(max_length
=255,
484 verbose_name
= u
"Prénom",)
485 nom_affichage
= models
.CharField(max_length
=255,
486 verbose_name
= u
"Nom d'affichage",
487 null
=True, blank
=True)
488 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
489 db_column
='nationalite',
490 related_name
='ayantdroits_nationalite',
491 verbose_name
= u
"Nationalité",
492 null
=True, blank
=True)
493 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
494 help_text
=HELP_TEXT_DATE
,
495 validators
=[validate_date_passee
],
496 null
=True, blank
=True)
497 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
500 employe
= models
.ForeignKey('Employe', db_column
='employe',
501 related_name
='ayantdroits',
502 verbose_name
= u
"Employé")
503 lien_parente
= models
.CharField(max_length
=10,
504 choices
=LIEN_PARENTE_CHOICES
,
505 verbose_name
= u
"Lien de parenté",
506 null
=True, blank
=True)
510 verbose_name
= u
"Ayant droit"
511 verbose_name_plural
= u
"Ayants droit"
513 def __unicode__(self
):
514 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
516 prefix_implantation
= "employe__dossiers__poste__implantation__region"
517 def get_regions(self
):
519 for d
in self
.employe
.dossiers
.all():
520 regions
.append(d
.poste
.implantation
.region
)
524 class AyantDroitCommentaire(Commentaire
):
525 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
531 STATUT_RESIDENCE_CHOICES
= (
533 ('expat', 'Expatrié'),
536 COMPTE_COMPTA_CHOICES
= (
542 class Dossier_(AUFMetadata
):
543 """Le Dossier regroupe les informations relatives à l'occupation
544 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
547 Plusieurs Contrats peuvent être associés au Dossier.
548 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
549 lequel aucun Dossier n'existe est un poste vacant.
552 objects
= DossierManager()
555 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
556 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
557 db_column
='organisme_bstg',
559 verbose_name
= u
"Organisme",
560 help_text
="Si détaché (DET) ou \
561 mis à disposition (MAD), \
562 préciser l'organisme.",
563 null
=True, blank
=True)
566 remplacement
= models
.BooleanField(default
=False)
567 remplacement_de
= models
.ForeignKey('self', related_name
='+',
568 help_text
=u
"Taper le nom de l'employé",
569 null
=True, blank
=True)
570 statut_residence
= models
.CharField(max_length
=10, default
='local',
571 verbose_name
= u
"Statut", null
=True,
572 choices
=STATUT_RESIDENCE_CHOICES
)
575 classement
= models
.ForeignKey('Classement', db_column
='classement',
577 null
=True, blank
=True)
578 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
580 default
=REGIME_TRAVAIL_DEFAULT
,
581 verbose_name
= u
"Régime de travail",
582 help_text
="% du temps complet")
583 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
584 decimal_places
=2, null
=True,
585 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
586 verbose_name
= u
"Nb. heures par semaine")
588 # Occupation du Poste par cet Employe (anciennement "mandat")
589 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
591 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
593 null
=True, blank
=True)
600 ordering
= ['employe__nom', ]
601 verbose_name
= u
"Dossier"
602 verbose_name_plural
= "Dossiers"
604 def salaire_theorique(self
):
605 annee
= date
.today().year
606 coeff
= self
.classement
.coefficient
607 implantation
= self
.poste
.implantation
608 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
610 montant
= coeff
* point
.valeur
611 devise
= point
.devise
612 return {'montant':montant
, 'devise':devise
}
614 def __unicode__(self
):
615 poste
= self
.poste
.nom
616 if self
.employe
.genre
== 'F':
617 poste
= self
.poste
.nom_feminin
618 return u
'%s - %s' % (self
.employe
, poste
)
620 prefix_implantation
= "poste__implantation__region"
621 def get_regions(self
):
622 return [self
.poste
.implantation
.region
]
625 def remunerations(self
):
626 return self
.rh_remunerations
.all().order_by('date_debut')
628 def remunerations_en_cours(self
):
629 return self
.rh_remunerations
.all().filter(date_fin__exact
=None).order_by('date_debut')
631 def get_salaire(self
):
633 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
637 class Dossier(Dossier_
):
638 __doc__
= Dossier_
.__doc__
639 poste
= models
.ForeignKey('%s.Poste' % app_context(),
641 related_name
='%(app_label)s_dossiers',
642 help_text
=u
"Taper le nom du poste ou du type de poste",
644 employe
= models
.ForeignKey('Employe', db_column
='employe',
645 help_text
=u
"Taper le nom de l'employé",
646 related_name
='%(app_label)s_dossiers',
647 verbose_name
=u
"Employé")
650 class DossierPiece_(models
.Model
):
651 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
652 Ex.: Lettre de motivation.
654 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
655 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
656 fichier
= models
.FileField(verbose_name
= u
"Fichier",
657 upload_to
=dossier_piece_dispatch
,
658 storage
=storage_prive
)
664 def __unicode__(self
):
665 return u
'%s' % (self
.nom
)
667 class DossierPiece(DossierPiece_
):
670 class DossierCommentaire_(Commentaire
):
671 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
675 class DossierCommentaire(DossierCommentaire_
):
678 class DossierComparaison_(models
.Model
):
680 Photo d'une comparaison salariale au moment de l'embauche.
682 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
683 objects
= DossierComparaisonManager()
685 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
686 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
687 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
688 montant
= models
.IntegerField(null
=True)
689 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
694 def taux_devise(self
):
695 annee
= self
.dossier
.poste
.date_debut
.year
696 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
699 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
))
703 def montant_euros(self
):
704 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
706 class DossierComparaison(DossierComparaison_
):
711 class RemunerationMixin(AUFMetadata
):
712 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
714 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
716 verbose_name
= u
"Type de rémunération")
717 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
718 db_column
='type_revalorisation',
720 verbose_name
= u
"Type de revalorisation",
721 null
=True, blank
=True)
722 montant
= models
.DecimalField(null
=True, blank
=True,
723 default
=0, max_digits
=9, decimal_places
=2)
724 # Annuel (12 mois, 52 semaines, 364 jours?)
725 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
726 # commentaire = precision
727 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
728 # date_debut = anciennement date_effectif
729 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
730 null
=True, blank
=True)
731 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
732 null
=True, blank
=True)
736 ordering
= ['type__nom', '-date_fin']
738 def __unicode__(self
):
739 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
741 class Remuneration_(RemunerationMixin
):
742 """Structure de rémunération (données budgétaires) en situation normale
743 pour un Dossier. Si un Evenement existe, utiliser la structure de
744 rémunération EvenementRemuneration de cet événement.
747 def montant_mois(self
):
748 return round(self
.montant
/ 12, 2)
750 def taux_devise(self
):
751 if self
.devise
.code
== "EUR":
754 annee
= datetime
.datetime
.now().year
755 if self
.date_debut
is not None:
756 annee
= self
.date_debut
.year
757 if self
.dossier
.poste
.date_debut
is not None:
758 annee
= self
.dossier
.poste
.date_debut
.year
760 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise_id
, annee
=annee
)]
763 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
))
767 def montant_euro(self
):
768 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
770 def montant_euro_mois(self
):
771 return round(self
.montant_euro() / 12, 2)
773 def __unicode__(self
):
775 devise
= self
.devise
.code
778 return "%s %s" % (self
.montant
, devise
)
782 verbose_name
= u
"Rémunération"
783 verbose_name_plural
= u
"Rémunérations"
786 class Remuneration(Remuneration_
):
792 class ContratManager(NoDeleteManager
):
793 def get_query_set(self
):
794 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
797 class Contrat_(AUFMetadata
):
798 """Document juridique qui encadre la relation de travail d'un Employe
799 pour un Poste particulier. Pour un Dossier (qui documente cette
800 relation de travail) plusieurs contrats peuvent être associés.
802 objects
= ContratManager()
803 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
804 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
806 verbose_name
= u
"type de contrat")
807 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
808 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
809 null
=True, blank
=True)
810 fichier
= models
.FileField(verbose_name
= u
"Fichier",
811 upload_to
=contrat_dispatch
,
812 storage
=storage_prive
,
813 null
=True, blank
=True)
817 ordering
= ['dossier__employe__nom']
818 verbose_name
= u
"Contrat"
819 verbose_name_plural
= u
"Contrats"
821 def __unicode__(self
):
822 return u
'%s - %s' % (self
.dossier
, self
.id)
824 class Contrat(Contrat_
):
830 #class Evenement_(AUFMetadata):
831 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
832 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
833 # (ex.: la Remuneration).
835 # Ex.: congé de maternité, maladie...
837 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
838 # différent et une rémunération en conséquence. On souhaite toutefois
839 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
840 # du retour à la normale.
842 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
844 # nom = models.CharField(max_length=255)
845 # date_debut = models.DateField(verbose_name = u"Date de début")
846 # date_fin = models.DateField(verbose_name = u"Date de fin",
847 # null=True, blank=True)
852 # verbose_name = u"Évènement"
853 # verbose_name_plural = u"Évènements"
855 # def __unicode__(self):
856 # return u'%s' % (self.nom)
859 #class Evenement(Evenement_):
860 # __doc__ = Evenement_.__doc__
863 #class EvenementRemuneration_(RemunerationMixin):
864 # """Structure de rémunération liée à un Evenement qui remplace
865 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
868 # evenement = models.ForeignKey("Evenement", db_column='evenement',
870 # verbose_name = u"Évènement")
871 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
872 # # de l'Evenement associé
876 # ordering = ['evenement', 'type__nom', '-date_fin']
877 # verbose_name = u"Évènement - rémunération"
878 # verbose_name_plural = u"Évènements - rémunérations"
881 #class EvenementRemuneration(EvenementRemuneration_):
882 # __doc__ = EvenementRemuneration_.__doc__
888 #class EvenementRemuneration(EvenementRemuneration_):
889 # __doc__ = EvenementRemuneration_.__doc__
890 # TODO? class ContratPiece(models.Model):
895 class FamilleEmploi(AUFMetadata
):
896 """Catégorie utilisée dans la gestion des Postes.
897 Catégorie supérieure à TypePoste.
899 nom
= models
.CharField(max_length
=255)
903 verbose_name
= u
"Famille d'emploi"
904 verbose_name_plural
= u
"Familles d'emploi"
906 def __unicode__(self
):
907 return u
'%s' % (self
.nom
)
909 class TypePoste(AUFMetadata
):
910 """Catégorie de Poste.
912 nom
= models
.CharField(max_length
=255)
913 nom_feminin
= models
.CharField(max_length
=255,
914 verbose_name
= u
"Nom féminin")
916 is_responsable
= models
.BooleanField(default
=False,
917 verbose_name
= u
"Poste de responsabilité")
918 famille_emploi
= models
.ForeignKey('FamilleEmploi',
919 db_column
='famille_emploi',
921 verbose_name
= u
"famille d'emploi")
925 verbose_name
= u
"Type de poste"
926 verbose_name_plural
= u
"Types de poste"
928 def __unicode__(self
):
929 return u
'%s' % (self
.nom
)
932 TYPE_PAIEMENT_CHOICES
= (
933 (u
'Régulier', u
'Régulier'),
934 (u
'Ponctuel', u
'Ponctuel'),
937 NATURE_REMUNERATION_CHOICES
= (
938 (u
'Accessoire', u
'Accessoire'),
939 (u
'Charges', u
'Charges'),
940 (u
'Indemnité', u
'Indemnité'),
941 (u
'RAS', u
'Rémunération autre source'),
942 (u
'Traitement', u
'Traitement'),
945 class TypeRemuneration(AUFMetadata
):
946 """Catégorie de Remuneration.
948 nom
= models
.CharField(max_length
=255)
949 type_paiement
= models
.CharField(max_length
=30,
950 choices
=TYPE_PAIEMENT_CHOICES
,
951 verbose_name
= u
"Type de paiement")
952 nature_remuneration
= models
.CharField(max_length
=30,
953 choices
=NATURE_REMUNERATION_CHOICES
,
954 verbose_name
= u
"Nature de la rémunération")
958 verbose_name
= u
"Type de rémunération"
959 verbose_name_plural
= u
"Types de rémunération"
961 def __unicode__(self
):
962 return u
'%s' % (self
.nom
)
964 class TypeRevalorisation(AUFMetadata
):
965 """Justification du changement de la Remuneration.
966 (Actuellement utilisé dans aucun traitement informatique.)
968 nom
= models
.CharField(max_length
=255)
972 verbose_name
= u
"Type de revalorisation"
973 verbose_name_plural
= u
"Types de revalorisation"
975 def __unicode__(self
):
976 return u
'%s' % (self
.nom
)
978 class Service(AUFMetadata
):
979 """Unité administrative où les Postes sont rattachés.
981 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
982 nom
= models
.CharField(max_length
=255)
986 verbose_name
= u
"Service"
987 verbose_name_plural
= u
"Services"
989 def __unicode__(self
):
990 return u
'%s' % (self
.nom
)
993 TYPE_ORGANISME_CHOICES
= (
994 ('MAD', 'Mise à disposition'),
995 ('DET', 'Détachement'),
998 class OrganismeBstg(AUFMetadata
):
999 """Organisation d'où provient un Employe mis à disposition (MAD) de
1000 ou détaché (DET) à l'AUF à titre gratuit.
1002 (BSTG = bien et service à titre gratuit.)
1004 nom
= models
.CharField(max_length
=255)
1005 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1006 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1008 related_name
='organismes_bstg',
1009 null
=True, blank
=True)
1012 ordering
= ['type', 'nom']
1013 verbose_name
= u
"Organisme BSTG"
1014 verbose_name_plural
= u
"Organismes BSTG"
1016 def __unicode__(self
):
1017 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1019 prefix_implantation
= "pays__region"
1020 def get_regions(self
):
1021 return [self
.pays
.region
]
1024 class Statut(AUFMetadata
):
1025 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1028 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.")
1029 nom
= models
.CharField(max_length
=255)
1033 verbose_name
= u
"Statut d'employé"
1034 verbose_name_plural
= u
"Statuts d'employé"
1036 def __unicode__(self
):
1037 return u
'%s : %s' % (self
.code
, self
.nom
)
1040 TYPE_CLASSEMENT_CHOICES
= (
1041 ('S', 'S -Soutien'),
1042 ('T', 'T - Technicien'),
1043 ('P', 'P - Professionel'),
1045 ('D', 'D - Direction'),
1046 ('SO', 'SO - Sans objet [expatriés]'),
1047 ('HG', 'HG - Hors grille [direction]'),
1051 class Classement_(AUFMetadata
):
1052 """Éléments de classement de la
1053 "Grille générique de classement hiérarchique".
1055 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1056 classement dans la grille. Le classement donne le coefficient utilisé dans:
1058 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1061 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1062 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1063 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1064 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1067 # annee # au lieu de date_debut et date_fin
1068 commentaire
= models
.TextField(null
=True, blank
=True)
1072 ordering
= ['type','echelon','degre','coefficient']
1073 verbose_name
= u
"Classement"
1074 verbose_name_plural
= u
"Classements"
1076 def __unicode__(self
):
1077 return u
'%s.%s.%s (%s)' % (self
.type, self
.echelon
, self
.degre
,
1080 class Classement(Classement_
):
1081 __doc__
= Classement_
.__doc__
1084 class TauxChange_(AUFMetadata
):
1085 """Taux de change de la devise vers l'euro (EUR)
1086 pour chaque année budgétaire.
1089 devise
= models
.ForeignKey('Devise', db_column
='devise')
1090 annee
= models
.IntegerField(verbose_name
= u
"Année")
1091 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1095 ordering
= ['-annee', 'devise__code']
1096 verbose_name
= u
"Taux de change"
1097 verbose_name_plural
= u
"Taux de change"
1099 def __unicode__(self
):
1100 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1103 class TauxChange(TauxChange_
):
1104 __doc__
= TauxChange_
.__doc__
1106 class ValeurPointManager(NoDeleteManager
):
1108 def get_query_set(self
):
1109 now
= datetime
.datetime
.now()
1110 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1113 class ValeurPoint_(AUFMetadata
):
1114 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1115 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1116 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1118 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1121 actuelles
= ValeurPointManager()
1123 valeur
= models
.FloatField(null
=True)
1124 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1125 implantation
= models
.ForeignKey(ref
.Implantation
,
1126 db_column
='implantation',
1127 related_name
='%(app_label)s_valeur_point')
1129 annee
= models
.IntegerField()
1132 ordering
= ['-annee', 'implantation__nom']
1134 verbose_name
= u
"Valeur du point"
1135 verbose_name_plural
= u
"Valeurs du point"
1137 def __unicode__(self
):
1138 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1141 class ValeurPoint(ValeurPoint_
):
1142 __doc__
= ValeurPoint_
.__doc__
1146 class Devise(AUFMetadata
):
1147 """Devise monétaire.
1149 code
= models
.CharField(max_length
=10, unique
=True)
1150 nom
= models
.CharField(max_length
=255)
1154 verbose_name
= u
"Devise"
1155 verbose_name_plural
= u
"Devises"
1157 def __unicode__(self
):
1158 return u
'%s - %s' % (self
.code
, self
.nom
)
1160 class TypeContrat(AUFMetadata
):
1163 nom
= models
.CharField(max_length
=255)
1164 nom_long
= models
.CharField(max_length
=255)
1168 verbose_name
= u
"Type de contrat"
1169 verbose_name_plural
= u
"Types de contrat"
1171 def __unicode__(self
):
1172 return u
'%s' % (self
.nom
)
1177 class ResponsableImplantation(AUFMetadata
):
1178 """Le responsable d'une implantation.
1179 Anciennement géré sur le Dossier du responsable.
1181 employe
= models
.ForeignKey('Employe', db_column
='employe',
1183 null
=True, blank
=True)
1184 implantation
= models
.ForeignKey(ref
.Implantation
,
1185 db_column
='implantation', related_name
='+',
1188 def __unicode__(self
):
1189 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1192 ordering
= ['implantation__nom']
1193 verbose_name
= "Responsable d'implantation"
1194 verbose_name_plural
= "Responsables d'implantation"