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
)
50 def contrat_dispatch(instance
, filename
):
51 path
= "contrat/%s/%s" % (instance
.dossier_id
, filename
)
55 class Commentaire(AUFMetadata
):
56 texte
= models
.TextField()
57 owner
= models
.ForeignKey('auth.User', db_column
='owner', related_name
='+', verbose_name
=u
"Commentaire de")
61 ordering
= ['-date_creation']
63 def __unicode__(self
):
64 return u
'%s' % (self
.texte
)
69 POSTE_APPEL_CHOICES
= (
70 ('interne', 'Interne'),
71 ('externe', 'Externe'),
74 class Poste_(AUFMetadata
):
75 """Un Poste est un emploi (job) à combler dans une implantation.
76 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
77 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
80 objects
= PosteManager()
83 nom
= models
.CharField(max_length
=255,
84 verbose_name
= u
"Titre du poste", )
85 nom_feminin
= models
.CharField(max_length
=255,
86 verbose_name
= u
"Titre du poste (au féminin)",
88 implantation
= models
.ForeignKey(ref
.Implantation
, help_text
=u
"Taper le nom de l'implantation ou sa région",
89 db_column
='implantation', related_name
='+')
90 type_poste
= models
.ForeignKey('TypePoste', db_column
='type_poste', help_text
=u
"Taper le nom du 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,
137 related_name
='+', default
=5)
138 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
139 related_name
='+', default
=5)
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',
158 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
159 null
=True, blank
=True)
160 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
161 null
=True, blank
=True)
162 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
163 null
=True, blank
=True)
164 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
165 null
=True, blank
=True)
166 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
167 null
=True, blank
=True)
168 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
169 null
=True, blank
=True)
170 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
171 null
=True, blank
=True)
172 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
173 null
=True, blank
=True)
174 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
175 null
=True, blank
=True)
176 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
177 null
=True, blank
=True)
180 justification
= models
.TextField(null
=True, blank
=True)
183 date_debut
= models
.DateField(verbose_name
=u
"Date de début",
184 null
=True, blank
=True)
185 date_fin
= models
.DateField(verbose_name
=u
"Date de fin",
186 null
=True, blank
=True)
190 ordering
= ['implantation__nom', 'nom']
191 verbose_name
= u
"Poste"
192 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(actif
=True, supprime
=False) \
224 .exclude(date_fin__lt
=date
.today())]
227 POSTE_FINANCEMENT_CHOICES
= (
228 ('A', 'A - Frais de personnel'),
229 ('B', 'B - Projet(s)-Titre(s)'),
234 class PosteFinancement_(models
.Model
):
235 """Pour un Poste, structure d'informations décrivant comment on prévoit
238 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
239 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
240 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
241 help_text
="ex.: 33.33 % (décimale avec point)")
242 commentaire
= models
.TextField(
243 help_text
="Spécifiez la source de financement.")
249 def __unicode__(self
):
250 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
253 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
256 class PosteFinancement(PosteFinancement_
):
260 class PostePiece_(models
.Model
):
261 """Documents relatifs au Poste.
262 Ex.: Description de poste
264 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
265 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
266 fichier
= models
.FileField(verbose_name
= u
"Fichier",
267 upload_to
=poste_piece_dispatch
,
268 storage
=storage_prive
)
274 def __unicode__(self
):
275 return u
'%s' % (self
.nom
)
277 class PostePiece(PostePiece_
):
280 class PosteComparaison_(AUFMetadata
):
282 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
284 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
285 objects
= PosteComparaisonManager()
287 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
288 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
289 montant
= models
.IntegerField(null
=True)
290 devise
= models
.ForeignKey("Devise", default
=5, related_name
='+', null
=True, blank
=True)
295 def taux_devise(self
):
296 if self
.devise
.code
== "EUR":
298 annee
= self
.poste
.date_debut
.year
299 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
302 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
))
306 def montant_euros(self
):
307 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
309 class PosteComparaison(PosteComparaison_
):
312 class PosteCommentaire_(Commentaire
):
313 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
318 class PosteCommentaire(PosteCommentaire_
):
323 class Employe(AUFMetadata
):
324 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
325 Dossiers qu'il occupe ou a occupé de Postes.
327 Cette classe aurait pu avantageusement s'appeler Personne car la notion
328 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
331 nom
= models
.CharField(max_length
=255)
332 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
333 nom_affichage
= models
.CharField(max_length
=255,
334 verbose_name
= u
"Nom d'affichage",
335 null
=True, blank
=True)
336 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
337 db_column
='nationalite',
338 related_name
='employes_nationalite',
339 verbose_name
= u
"Nationalité",
340 blank
=True, null
=True)
341 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
342 help_text
=HELP_TEXT_DATE
,
343 validators
=[validate_date_passee
],
344 null
=True, blank
=True)
345 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
348 situation_famille
= models
.CharField(max_length
=1,
349 choices
=SITUATION_CHOICES
,
350 verbose_name
= u
"Situation familiale",
351 null
=True, blank
=True)
352 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
353 help_text
=HELP_TEXT_DATE
,
354 null
=True, blank
=True)
357 tel_domicile
= models
.CharField(max_length
=255,
358 verbose_name
= u
"Tél. domicile",
359 null
=True, blank
=True)
360 tel_cellulaire
= models
.CharField(max_length
=255,
361 verbose_name
= u
"Tél. cellulaire",
362 null
=True, blank
=True)
363 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
364 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
365 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
366 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
367 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
368 related_name
='employes',
369 null
=True, blank
=True)
371 # meta dématérialisation : pour permettre le filtrage
372 nb_postes
= models
.IntegerField(verbose_name
= u
"Nombre de postes", null
=True, blank
=True)
375 ordering
= ['nom','prenom']
376 verbose_name
= u
"Employé"
377 verbose_name_plural
= u
"Employés"
379 def __unicode__(self
):
380 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
384 if self
.genre
.upper() == u
'M':
386 elif self
.genre
.upper() == u
'F':
391 """Retourne l'URL du service retournant la photo de l'Employe.
392 Équivalent reverse url 'rh_photo' avec id en param.
394 from django
.core
.urlresolvers
import reverse
395 return reverse('rh_photo', kwargs
={'id':self
.id})
397 def dossiers_passes(self
):
399 dossiers_passes
= self
.dossiers
.filter(date_fin__lt
=today
).order_by('-date_fin')
400 for d
in dossiers_passes
:
402 return dossiers_passes
404 def dossiers_futurs(self
):
406 return self
.dossiers
.filter(date_debut__gt
=today
).order_by('-date_fin')
408 def dossiers_encours(self
):
409 dossiers_p_f
= self
.dossiers_passes() | self
.dossiers_futurs()
410 ids_dossiers_p_f
= [d
.id for d
in dossiers_p_f
]
411 dossiers_encours
= self
.dossiers
.exclude(id__in
=ids_dossiers_p_f
).order_by('-date_fin')
413 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
414 for d
in dossiers_encours
:
415 d
.remunerations
= Remuneration
.objects
.filter(dossier
=d
.id).order_by('-id')
416 return dossiers_encours
418 def postes_encours(self
):
419 postes_encours
= set()
420 for d
in self
.dossiers_encours():
421 postes_encours
.add(d
.poste
)
422 return postes_encours
424 def poste_principal(self
):
426 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
428 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
430 poste
= Poste
.objects
.none()
432 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
437 prefix_implantation
= "rh_dossiers__poste__implantation__region"
438 def get_regions(self
):
440 for d
in self
.dossiers
.all():
441 regions
.append(d
.poste
.implantation
.region
)
445 class EmployePiece(models
.Model
):
446 """Documents relatifs à un employé.
449 employe
= models
.ForeignKey('Employe', db_column
='employe')
450 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
451 fichier
= models
.FileField(verbose_name
="Fichier",
452 upload_to
=employe_piece_dispatch
,
453 storage
=storage_prive
)
457 verbose_name
= u
"Employé pièce"
458 verbose_name_plural
= u
"Employé pièces"
460 def __unicode__(self
):
461 return u
'%s' % (self
.nom
)
463 class EmployeCommentaire(Commentaire
):
464 employe
= models
.ForeignKey('Employe', db_column
='employe',
468 verbose_name
= u
"Employé commentaire"
469 verbose_name_plural
= u
"Employé commentaires"
472 LIEN_PARENTE_CHOICES
= (
473 ('Conjoint', 'Conjoint'),
474 ('Conjointe', 'Conjointe'),
479 class AyantDroit(AUFMetadata
):
480 """Personne en relation avec un Employe.
483 nom
= models
.CharField(max_length
=255)
484 prenom
= models
.CharField(max_length
=255,
485 verbose_name
= u
"Prénom",)
486 nom_affichage
= models
.CharField(max_length
=255,
487 verbose_name
= u
"Nom d'affichage",
488 null
=True, blank
=True)
489 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
490 db_column
='nationalite',
491 related_name
='ayantdroits_nationalite',
492 verbose_name
= u
"Nationalité",
493 null
=True, blank
=True)
494 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
495 help_text
=HELP_TEXT_DATE
,
496 validators
=[validate_date_passee
],
497 null
=True, blank
=True)
498 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
501 employe
= models
.ForeignKey('Employe', db_column
='employe',
502 related_name
='ayantdroits',
503 verbose_name
= u
"Employé")
504 lien_parente
= models
.CharField(max_length
=10,
505 choices
=LIEN_PARENTE_CHOICES
,
506 verbose_name
= u
"Lien de parenté",
507 null
=True, blank
=True)
511 verbose_name
= u
"Ayant droit"
512 verbose_name_plural
= u
"Ayants droit"
514 def __unicode__(self
):
515 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
517 prefix_implantation
= "employe__dossiers__poste__implantation__region"
518 def get_regions(self
):
520 for d
in self
.employe
.dossiers
.all():
521 regions
.append(d
.poste
.implantation
.region
)
525 class AyantDroitCommentaire(Commentaire
):
526 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
532 STATUT_RESIDENCE_CHOICES
= (
534 ('expat', 'Expatrié'),
537 COMPTE_COMPTA_CHOICES
= (
543 class Dossier_(AUFMetadata
):
544 """Le Dossier regroupe les informations relatives à l'occupation
545 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
548 Plusieurs Contrats peuvent être associés au Dossier.
549 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
550 lequel aucun Dossier n'existe est un poste vacant.
553 objects
= DossierManager()
556 statut
= models
.ForeignKey('Statut', related_name
='+', default
=3,
558 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
559 db_column
='organisme_bstg',
561 verbose_name
= u
"Organisme",
562 help_text
="Si détaché (DET) ou \
563 mis à disposition (MAD), \
564 préciser l'organisme.",
565 null
=True, blank
=True)
568 remplacement
= models
.BooleanField(default
=False)
569 remplacement_de
= models
.ForeignKey('self', related_name
='+',
570 help_text
=u
"Taper le nom de l'employé",
571 null
=True, blank
=True)
572 statut_residence
= models
.CharField(max_length
=10, default
='local',
573 verbose_name
= u
"Statut", null
=True,
574 choices
=STATUT_RESIDENCE_CHOICES
)
577 classement
= models
.ForeignKey('Classement', db_column
='classement',
579 null
=True, blank
=True)
580 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
582 default
=REGIME_TRAVAIL_DEFAULT
,
583 verbose_name
= u
"Régime de travail",
584 help_text
="% du temps complet")
585 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
586 decimal_places
=2, null
=True,
587 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
588 verbose_name
= u
"Nb. heures par semaine")
590 # Occupation du Poste par cet Employe (anciennement "mandat")
591 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
593 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
595 null
=True, blank
=True)
602 ordering
= ['employe__nom', ]
603 verbose_name
= u
"Dossier"
604 verbose_name_plural
= "Dossiers"
606 def salaire_theorique(self
):
607 annee
= date
.today().year
608 coeff
= self
.classement
.coefficient
609 implantation
= self
.poste
.implantation
610 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
612 montant
= coeff
* point
.valeur
613 devise
= point
.devise
614 return {'montant':montant
, 'devise':devise
}
616 def __unicode__(self
):
617 poste
= self
.poste
.nom
618 if self
.employe
.genre
== 'F':
619 poste
= self
.poste
.nom_feminin
620 return u
'%s - %s' % (self
.employe
, poste
)
622 prefix_implantation
= "poste__implantation__region"
623 def get_regions(self
):
624 return [self
.poste
.implantation
.region
]
627 def remunerations(self
):
628 return self
.rh_remunerations
.all().order_by('date_debut')
630 def get_salaire(self
):
632 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
636 class Dossier(Dossier_
):
637 __doc__
= Dossier_
.__doc__
638 poste
= models
.ForeignKey('%s.Poste' % app_context(),
640 related_name
='%(app_label)s_dossiers',
641 help_text
=u
"Taper le nom du poste ou du type de poste",
643 employe
= models
.ForeignKey('Employe', db_column
='employe',
644 help_text
=u
"Taper le nom de l'employé",
645 related_name
='%(app_label)s_dossiers',
646 verbose_name
=u
"Employé")
649 class DossierPiece_(models
.Model
):
650 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
651 Ex.: Lettre de motivation.
653 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
654 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
655 fichier
= models
.FileField(verbose_name
= u
"Fichier",
656 upload_to
=dossier_piece_dispatch
,
657 storage
=storage_prive
)
663 def __unicode__(self
):
664 return u
'%s' % (self
.nom
)
666 class DossierPiece(DossierPiece_
):
669 class DossierCommentaire_(Commentaire
):
670 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
674 class DossierCommentaire(DossierCommentaire_
):
677 class DossierComparaison_(models
.Model
):
679 Photo d'une comparaison salariale au moment de l'embauche.
681 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
682 objects
= DossierComparaisonManager()
684 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
685 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
686 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
687 montant
= models
.IntegerField(null
=True)
688 devise
= models
.ForeignKey('Devise', default
=5, related_name
='+', null
=True, blank
=True)
693 def taux_devise(self
):
694 annee
= self
.dossier
.poste
.date_debut
.year
695 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
698 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
))
702 def montant_euros(self
):
703 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
705 class DossierComparaison(DossierComparaison_
):
710 class RemunerationMixin(AUFMetadata
):
711 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
713 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
715 verbose_name
= u
"Type de rémunération")
716 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
717 db_column
='type_revalorisation',
719 verbose_name
= u
"Type de revalorisation",
720 null
=True, blank
=True)
721 montant
= models
.FloatField(null
=True, blank
=True,
723 # Annuel (12 mois, 52 semaines, 364 jours?)
724 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
725 # commentaire = precision
726 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
727 # date_debut = anciennement date_effectif
728 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
729 null
=True, blank
=True)
730 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
731 null
=True, blank
=True)
735 ordering
= ['type__nom', '-date_fin']
737 def __unicode__(self
):
738 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
740 class Remuneration_(RemunerationMixin
):
741 """Structure de rémunération (données budgétaires) en situation normale
742 pour un Dossier. Si un Evenement existe, utiliser la structure de
743 rémunération EvenementRemuneration de cet événement.
746 def montant_mois(self
):
747 return round(self
.montant
/ 12, 2)
749 def taux_devise(self
):
750 if self
.devise
.code
== "EUR":
753 annee
= datetime
.datetime
.now().year
754 if self
.date_debut
is not None:
755 annee
= self
.date_debut
.year
756 if self
.dossier
.poste
.date_debut
is not None:
757 annee
= self
.dossier
.poste
.date_debut
.year
759 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise_id
, annee
=annee
)]
762 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
))
766 def montant_euro(self
):
767 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
769 def montant_euro_mois(self
):
770 return round(self
.montant_euro() / 12, 2)
772 def __unicode__(self
):
774 devise
= self
.devise
.code
777 return "%s %s" % (self
.montant
, devise
)
781 verbose_name
= u
"Rémunération"
782 verbose_name_plural
= u
"Rémunérations"
785 class Remuneration(Remuneration_
):
791 class ContratManager(NoDeleteManager
):
792 def get_query_set(self
):
793 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
796 class Contrat_(AUFMetadata
):
797 """Document juridique qui encadre la relation de travail d'un Employe
798 pour un Poste particulier. Pour un Dossier (qui documente cette
799 relation de travail) plusieurs contrats peuvent être associés.
801 objects
= ContratManager()
802 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
803 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
805 verbose_name
= u
"Type de contrat")
806 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
807 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
808 null
=True, blank
=True)
809 fichier
= models
.FileField(verbose_name
= u
"Fichier",
810 upload_to
=contrat_dispatch
,
811 storage
=storage_prive
,
812 null
=True, blank
=True)
816 ordering
= ['dossier__employe__nom']
817 verbose_name
= u
"Contrat"
818 verbose_name_plural
= u
"Contrats"
820 def __unicode__(self
):
821 return u
'%s - %s' % (self
.dossier
, self
.id)
823 class Contrat(Contrat_
):
829 #class Evenement_(AUFMetadata):
830 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
831 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
832 # (ex.: la Remuneration).
834 # Ex.: congé de maternité, maladie...
836 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
837 # différent et une rémunération en conséquence. On souhaite toutefois
838 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
839 # du retour à la normale.
841 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
843 # nom = models.CharField(max_length=255)
844 # date_debut = models.DateField(verbose_name = u"Date de début")
845 # date_fin = models.DateField(verbose_name = u"Date de fin",
846 # null=True, blank=True)
851 # verbose_name = u"Évènement"
852 # verbose_name_plural = u"Évènements"
854 # def __unicode__(self):
855 # return u'%s' % (self.nom)
858 #class Evenement(Evenement_):
859 # __doc__ = Evenement_.__doc__
862 #class EvenementRemuneration_(RemunerationMixin):
863 # """Structure de rémunération liée à un Evenement qui remplace
864 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
867 # evenement = models.ForeignKey("Evenement", db_column='evenement',
869 # verbose_name = u"Évènement")
870 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
871 # # de l'Evenement associé
875 # ordering = ['evenement', 'type__nom', '-date_fin']
876 # verbose_name = u"Évènement - rémunération"
877 # verbose_name_plural = u"Évènements - rémunérations"
880 #class EvenementRemuneration(EvenementRemuneration_):
881 # __doc__ = EvenementRemuneration_.__doc__
887 #class EvenementRemuneration(EvenementRemuneration_):
888 # __doc__ = EvenementRemuneration_.__doc__
889 # TODO? class ContratPiece(models.Model):
894 class FamilleEmploi(AUFMetadata
):
895 """Catégorie utilisée dans la gestion des Postes.
896 Catégorie supérieure à TypePoste.
898 nom
= models
.CharField(max_length
=255)
902 verbose_name
= u
"Famille d'emploi"
903 verbose_name_plural
= u
"Familles d'emploi"
905 def __unicode__(self
):
906 return u
'%s' % (self
.nom
)
908 class TypePoste(AUFMetadata
):
909 """Catégorie de Poste.
911 nom
= models
.CharField(max_length
=255)
912 nom_feminin
= models
.CharField(max_length
=255,
913 verbose_name
= u
"Nom féminin")
915 is_responsable
= models
.BooleanField(default
=False,
916 verbose_name
= u
"Poste de responsabilité")
917 famille_emploi
= models
.ForeignKey('FamilleEmploi',
918 db_column
='famille_emploi',
920 verbose_name
= u
"Famille d'emploi")
924 verbose_name
= u
"Type de poste"
925 verbose_name_plural
= u
"Types de poste"
927 def __unicode__(self
):
928 return u
'%s' % (self
.nom
)
931 TYPE_PAIEMENT_CHOICES
= (
932 (u
'Régulier', u
'Régulier'),
933 (u
'Ponctuel', u
'Ponctuel'),
936 NATURE_REMUNERATION_CHOICES
= (
937 (u
'Accessoire', u
'Accessoire'),
938 (u
'Charges', u
'Charges'),
939 (u
'Indemnité', u
'Indemnité'),
940 (u
'RAS', u
'Rémunération autre source'),
941 (u
'Traitement', u
'Traitement'),
944 class TypeRemuneration(AUFMetadata
):
945 """Catégorie de Remuneration.
947 nom
= models
.CharField(max_length
=255)
948 type_paiement
= models
.CharField(max_length
=30,
949 choices
=TYPE_PAIEMENT_CHOICES
,
950 verbose_name
= u
"Type de paiement")
951 nature_remuneration
= models
.CharField(max_length
=30,
952 choices
=NATURE_REMUNERATION_CHOICES
,
953 verbose_name
= u
"Nature de la rémunération")
957 verbose_name
= u
"Type de rémunération"
958 verbose_name_plural
= u
"Types de rémunération"
960 def __unicode__(self
):
961 return u
'%s' % (self
.nom
)
963 class TypeRevalorisation(AUFMetadata
):
964 """Justification du changement de la Remuneration.
965 (Actuellement utilisé dans aucun traitement informatique.)
967 nom
= models
.CharField(max_length
=255)
971 verbose_name
= u
"Type de revalorisation"
972 verbose_name_plural
= u
"Types de revalorisation"
974 def __unicode__(self
):
975 return u
'%s' % (self
.nom
)
977 class Service(AUFMetadata
):
978 """Unité administrative où les Postes sont rattachés.
980 nom
= models
.CharField(max_length
=255)
984 verbose_name
= u
"Service"
985 verbose_name_plural
= u
"Services"
987 def __unicode__(self
):
988 return u
'%s' % (self
.nom
)
991 TYPE_ORGANISME_CHOICES
= (
992 ('MAD', 'Mise à disposition'),
993 ('DET', 'Détachement'),
996 class OrganismeBstg(AUFMetadata
):
997 """Organisation d'où provient un Employe mis à disposition (MAD) de
998 ou détaché (DET) à l'AUF à titre gratuit.
1000 (BSTG = bien et service à titre gratuit.)
1002 nom
= models
.CharField(max_length
=255)
1003 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1004 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1006 related_name
='organismes_bstg',
1007 null
=True, blank
=True)
1010 ordering
= ['type', 'nom']
1011 verbose_name
= u
"Organisme BSTG"
1012 verbose_name_plural
= u
"Organismes BSTG"
1014 def __unicode__(self
):
1015 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1017 prefix_implantation
= "pays__region"
1018 def get_regions(self
):
1019 return [self
.pays
.region
]
1022 class Statut(AUFMetadata
):
1023 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1026 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.")
1027 nom
= models
.CharField(max_length
=255)
1031 verbose_name
= u
"Statut d'employé"
1032 verbose_name_plural
= u
"Statuts d'employé"
1034 def __unicode__(self
):
1035 return u
'%s : %s' % (self
.code
, self
.nom
)
1038 TYPE_CLASSEMENT_CHOICES
= (
1039 ('S', 'S -Soutien'),
1040 ('T', 'T - Technicien'),
1041 ('P', 'P - Professionel'),
1043 ('D', 'D - Direction'),
1044 ('SO', 'SO - Sans objet [expatriés]'),
1045 ('HG', 'HG - Hors grille [direction]'),
1049 class Classement_(AUFMetadata
):
1050 """Éléments de classement de la
1051 "Grille générique de classement hiérarchique".
1053 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1054 classement dans la grille. Le classement donne le coefficient utilisé dans:
1056 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1059 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1060 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1061 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1062 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1065 # annee # au lieu de date_debut et date_fin
1066 commentaire
= models
.TextField(null
=True, blank
=True)
1070 ordering
= ['type','echelon','degre','coefficient']
1071 verbose_name
= u
"Classement"
1072 verbose_name_plural
= u
"Classements"
1074 def __unicode__(self
):
1075 return u
'%s.%s.%s (%s)' % (self
.type, self
.echelon
, self
.degre
,
1078 class Classement(Classement_
):
1079 __doc__
= Classement_
.__doc__
1082 class TauxChange_(AUFMetadata
):
1083 """Taux de change de la devise vers l'euro (EUR)
1084 pour chaque année budgétaire.
1087 devise
= models
.ForeignKey('Devise', db_column
='devise')
1088 annee
= models
.IntegerField(verbose_name
= u
"Année")
1089 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1093 ordering
= ['-annee', 'devise__code']
1094 verbose_name
= u
"Taux de change"
1095 verbose_name_plural
= u
"Taux de change"
1097 def __unicode__(self
):
1098 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1101 class TauxChange(TauxChange_
):
1102 __doc__
= TauxChange_
.__doc__
1104 class ValeurPointManager(NoDeleteManager
):
1106 def get_query_set(self
):
1107 now
= datetime
.datetime
.now()
1108 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1111 class ValeurPoint_(AUFMetadata
):
1112 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1113 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1114 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1116 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1119 actuelles
= ValeurPointManager()
1121 valeur
= models
.FloatField(null
=True)
1122 devise
= models
.ForeignKey('Devise', db_column
='devise', null
=True,
1123 related_name
='+', default
=5)
1124 implantation
= models
.ForeignKey(ref
.Implantation
,
1125 db_column
='implantation',
1126 related_name
='%(app_label)s_valeur_point')
1128 annee
= models
.IntegerField()
1131 ordering
= ['-annee', 'implantation__nom']
1133 verbose_name
= u
"Valeur du point"
1134 verbose_name_plural
= u
"Valeurs du point"
1136 def __unicode__(self
):
1137 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1140 class ValeurPoint(ValeurPoint_
):
1141 __doc__
= ValeurPoint_
.__doc__
1145 class Devise(AUFMetadata
):
1146 """Devise monétaire.
1148 code
= models
.CharField(max_length
=10, unique
=True)
1149 nom
= models
.CharField(max_length
=255)
1153 verbose_name
= u
"Devise"
1154 verbose_name_plural
= u
"Devises"
1156 def __unicode__(self
):
1157 return u
'%s - %s' % (self
.code
, self
.nom
)
1159 class TypeContrat(AUFMetadata
):
1162 nom
= models
.CharField(max_length
=255)
1163 nom_long
= models
.CharField(max_length
=255)
1167 verbose_name
= u
"Type de contrat"
1168 verbose_name_plural
= u
"Types de contrat"
1170 def __unicode__(self
):
1171 return u
'%s' % (self
.nom
)
1176 class ResponsableImplantation(AUFMetadata
):
1177 """Le responsable d'une implantation.
1178 Anciennement géré sur le Dossier du responsable.
1180 employe
= models
.ForeignKey('Employe', db_column
='employe',
1182 null
=True, blank
=True)
1183 implantation
= models
.ForeignKey(ref
.Implantation
,
1184 db_column
='implantation', related_name
='+',
1187 def __unicode__(self
):
1188 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1191 ordering
= ['implantation__nom']
1192 verbose_name
= "Responsable d'implantation"
1193 verbose_name_plural
= "Responsables d'implantation"