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 class PosteFinancement(PosteFinancement_
):
257 class PostePiece_(models
.Model
):
258 """Documents relatifs au Poste.
259 Ex.: Description de poste
261 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
262 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
263 fichier
= models
.FileField(verbose_name
= u
"Fichier",
264 upload_to
=poste_piece_dispatch
,
265 storage
=storage_prive
)
271 def __unicode__(self
):
272 return u
'%s' % (self
.nom
)
274 class PostePiece(PostePiece_
):
277 class PosteComparaison_(AUFMetadata
):
279 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
281 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
282 objects
= PosteComparaisonManager()
284 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
285 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
286 montant
= models
.IntegerField(null
=True)
287 devise
= models
.ForeignKey("Devise", default
=5, related_name
='+', null
=True, blank
=True)
292 def taux_devise(self
):
293 if self
.devise
.code
== "EUR":
295 annee
= self
.poste
.date_debut
.year
296 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
299 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
))
303 def montant_euros(self
):
304 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
306 class PosteComparaison(PosteComparaison_
):
309 class PosteCommentaire_(Commentaire
):
310 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
315 class PosteCommentaire(PosteCommentaire_
):
320 class Employe(AUFMetadata
):
321 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
322 Dossiers qu'il occupe ou a occupé de Postes.
324 Cette classe aurait pu avantageusement s'appeler Personne car la notion
325 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
328 nom
= models
.CharField(max_length
=255)
329 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
330 nom_affichage
= models
.CharField(max_length
=255,
331 verbose_name
= u
"Nom d'affichage",
332 null
=True, blank
=True)
333 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
334 db_column
='nationalite',
335 related_name
='employes_nationalite',
336 verbose_name
= u
"Nationalité",
337 blank
=True, null
=True)
338 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
339 help_text
=HELP_TEXT_DATE
,
340 validators
=[validate_date_passee
],
341 null
=True, blank
=True)
342 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
345 situation_famille
= models
.CharField(max_length
=1,
346 choices
=SITUATION_CHOICES
,
347 verbose_name
= u
"Situation familiale",
348 null
=True, blank
=True)
349 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
350 help_text
=HELP_TEXT_DATE
,
351 null
=True, blank
=True)
354 tel_domicile
= models
.CharField(max_length
=255,
355 verbose_name
= u
"Tél. domicile",
356 null
=True, blank
=True)
357 tel_cellulaire
= models
.CharField(max_length
=255,
358 verbose_name
= u
"Tél. cellulaire",
359 null
=True, blank
=True)
360 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
361 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
362 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
363 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
364 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
365 related_name
='employes',
366 null
=True, blank
=True)
368 # meta dématérialisation : pour permettre le filtrage
369 nb_postes
= models
.IntegerField(verbose_name
= u
"Nombre de postes", null
=True, blank
=True)
372 ordering
= ['nom','prenom']
373 verbose_name
= u
"Employé"
374 verbose_name_plural
= u
"Employés"
376 def __unicode__(self
):
377 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
381 if self
.genre
.upper() == u
'M':
383 elif self
.genre
.upper() == u
'F':
388 """Retourne l'URL du service retournant la photo de l'Employe.
389 Équivalent reverse url 'rh_photo' avec id en param.
391 from django
.core
.urlresolvers
import reverse
392 return reverse('rh_photo', kwargs
={'id':self
.id})
394 def dossiers_passes(self
):
396 dossiers_passes
= self
.dossiers
.filter(date_fin__lt
=today
).order_by('-date_fin')
397 for d
in dossiers_passes
:
399 return dossiers_passes
401 def dossiers_futurs(self
):
403 return self
.dossiers
.filter(date_debut__gt
=today
).order_by('-date_fin')
405 def dossiers_encours(self
):
406 dossiers_p_f
= self
.dossiers_passes() | self
.dossiers_futurs()
407 ids_dossiers_p_f
= [d
.id for d
in dossiers_p_f
]
408 dossiers_encours
= self
.dossiers
.exclude(id__in
=ids_dossiers_p_f
).order_by('-date_fin')
410 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
411 for d
in dossiers_encours
:
412 d
.remunerations
= Remuneration
.objects
.filter(dossier
=d
.id).order_by('-id')
413 return dossiers_encours
415 def postes_encours(self
):
416 postes_encours
= set()
417 for d
in self
.dossiers_encours():
418 postes_encours
.add(d
.poste
)
419 return postes_encours
421 def poste_principal(self
):
423 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
425 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
427 poste
= Poste
.objects
.none()
429 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
434 prefix_implantation
= "dossiers__poste__implantation__region"
435 def get_regions(self
):
437 for d
in self
.dossiers
.all():
438 regions
.append(d
.poste
.implantation
.region
)
442 class EmployePiece(models
.Model
):
443 """Documents relatifs à un employé.
446 employe
= models
.ForeignKey('Employe', db_column
='employe')
447 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
448 fichier
= models
.FileField(verbose_name
="Fichier",
449 upload_to
=employe_piece_dispatch
,
450 storage
=storage_prive
)
454 verbose_name
= u
"Employé pièce"
455 verbose_name_plural
= u
"Employé pièces"
457 def __unicode__(self
):
458 return u
'%s' % (self
.nom
)
460 class EmployeCommentaire(Commentaire
):
461 employe
= models
.ForeignKey('Employe', db_column
='employe',
465 verbose_name
= u
"Employé commentaire"
466 verbose_name_plural
= u
"Employé commentaires"
469 LIEN_PARENTE_CHOICES
= (
470 ('Conjoint', 'Conjoint'),
471 ('Conjointe', 'Conjointe'),
476 class AyantDroit(AUFMetadata
):
477 """Personne en relation avec un Employe.
480 nom
= models
.CharField(max_length
=255)
481 prenom
= models
.CharField(max_length
=255,
482 verbose_name
= u
"Prénom",)
483 nom_affichage
= models
.CharField(max_length
=255,
484 verbose_name
= u
"Nom d'affichage",
485 null
=True, blank
=True)
486 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
487 db_column
='nationalite',
488 related_name
='ayantdroits_nationalite',
489 verbose_name
= u
"Nationalité",
490 null
=True, blank
=True)
491 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
492 help_text
=HELP_TEXT_DATE
,
493 validators
=[validate_date_passee
],
494 null
=True, blank
=True)
495 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
498 employe
= models
.ForeignKey('Employe', db_column
='employe',
499 related_name
='ayantdroits',
500 verbose_name
= u
"Employé")
501 lien_parente
= models
.CharField(max_length
=10,
502 choices
=LIEN_PARENTE_CHOICES
,
503 verbose_name
= u
"Lien de parenté",
504 null
=True, blank
=True)
508 verbose_name
= u
"Ayant droit"
509 verbose_name_plural
= u
"Ayants droit"
511 def __unicode__(self
):
512 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
514 prefix_implantation
= "employe__dossiers__poste__implantation__region"
515 def get_regions(self
):
517 for d
in self
.employe
.dossiers
.all():
518 regions
.append(d
.poste
.implantation
.region
)
522 class AyantDroitCommentaire(Commentaire
):
523 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
529 STATUT_RESIDENCE_CHOICES
= (
531 ('expat', 'Expatrié'),
534 COMPTE_COMPTA_CHOICES
= (
540 class Dossier_(AUFMetadata
):
541 """Le Dossier regroupe les informations relatives à l'occupation
542 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
545 Plusieurs Contrats peuvent être associés au Dossier.
546 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
547 lequel aucun Dossier n'existe est un poste vacant.
550 objects
= DossierManager()
553 statut
= models
.ForeignKey('Statut', related_name
='+', default
=3,
555 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
556 db_column
='organisme_bstg',
558 verbose_name
= u
"Organisme",
559 help_text
="Si détaché (DET) ou \
560 mis à disposition (MAD), \
561 préciser l'organisme.",
562 null
=True, blank
=True)
565 remplacement
= models
.BooleanField(default
=False)
566 remplacement_de
= models
.ForeignKey('self', related_name
='+',
567 help_text
=u
"Taper le nom de l'employé",
568 null
=True, blank
=True)
569 statut_residence
= models
.CharField(max_length
=10, default
='local',
570 verbose_name
= u
"Statut", null
=True,
571 choices
=STATUT_RESIDENCE_CHOICES
)
574 classement
= models
.ForeignKey('Classement', db_column
='classement',
576 null
=True, blank
=True)
577 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
579 default
=REGIME_TRAVAIL_DEFAULT
,
580 verbose_name
= u
"Régime de travail",
581 help_text
="% du temps complet")
582 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
583 decimal_places
=2, null
=True,
584 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
585 verbose_name
= u
"Nb. heures par semaine")
587 # Occupation du Poste par cet Employe (anciennement "mandat")
588 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
590 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
592 null
=True, blank
=True)
599 ordering
= ['employe__nom', ]
600 verbose_name
= u
"Dossier"
601 verbose_name_plural
= "Dossiers"
603 def salaire_theorique(self
):
604 annee
= date
.today().year
605 coeff
= self
.classement
.coefficient
606 implantation
= self
.poste
.implantation
607 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
609 montant
= coeff
* point
.valeur
610 devise
= point
.devise
611 return {'montant':montant
, 'devise':devise
}
613 def __unicode__(self
):
614 poste
= self
.poste
.nom
615 if self
.employe
.genre
== 'F':
616 poste
= self
.poste
.nom_feminin
617 return u
'%s - %s' % (self
.employe
, poste
)
619 prefix_implantation
= "poste__implantation__region"
620 def get_regions(self
):
621 return [self
.poste
.implantation
.region
]
624 def remunerations(self
):
625 return self
.rh_remunerations
.all().order_by('date_debut')
627 def get_salaire(self
):
629 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
633 class Dossier(Dossier_
):
634 __doc__
= Dossier_
.__doc__
635 poste
= models
.ForeignKey('%s.Poste' % app_context(),
637 related_name
='%(app_label)s_dossiers',
638 help_text
=u
"Taper le nom du poste ou du type de poste",
640 employe
= models
.ForeignKey('Employe', db_column
='employe',
641 help_text
=u
"Taper le nom de l'employé",
642 related_name
='%(app_label)s_dossiers',
643 verbose_name
=u
"Employé")
646 class DossierPiece_(models
.Model
):
647 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
648 Ex.: Lettre de motivation.
650 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
651 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
652 fichier
= models
.FileField(verbose_name
= u
"Fichier",
653 upload_to
=dossier_piece_dispatch
,
654 storage
=storage_prive
)
660 def __unicode__(self
):
661 return u
'%s' % (self
.nom
)
663 class DossierPiece(DossierPiece_
):
666 class DossierCommentaire_(Commentaire
):
667 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
671 class DossierCommentaire(DossierCommentaire_
):
674 class DossierComparaison_(models
.Model
):
676 Photo d'une comparaison salariale au moment de l'embauche.
678 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
679 objects
= DossierComparaisonManager()
681 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
682 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
683 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
684 montant
= models
.IntegerField(null
=True)
685 devise
= models
.ForeignKey('Devise', default
=5, related_name
='+', null
=True, blank
=True)
690 def taux_devise(self
):
691 annee
= self
.dossier
.poste
.date_debut
.year
692 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
695 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
))
699 def montant_euros(self
):
700 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
702 class DossierComparaison(DossierComparaison_
):
707 class RemunerationMixin(AUFMetadata
):
708 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
710 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
712 verbose_name
= u
"Type de rémunération")
713 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
714 db_column
='type_revalorisation',
716 verbose_name
= u
"Type de revalorisation",
717 null
=True, blank
=True)
718 montant
= models
.FloatField(null
=True, blank
=True,
720 # Annuel (12 mois, 52 semaines, 364 jours?)
721 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
722 # commentaire = precision
723 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
724 # date_debut = anciennement date_effectif
725 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
726 null
=True, blank
=True)
727 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
728 null
=True, blank
=True)
732 ordering
= ['type__nom', '-date_fin']
734 def __unicode__(self
):
735 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
737 class Remuneration_(RemunerationMixin
):
738 """Structure de rémunération (données budgétaires) en situation normale
739 pour un Dossier. Si un Evenement existe, utiliser la structure de
740 rémunération EvenementRemuneration de cet événement.
743 def montant_mois(self
):
744 return round(self
.montant
/ 12, 2)
746 def taux_devise(self
):
747 if self
.devise
.code
== "EUR":
750 annee
= datetime
.datetime
.now().year
751 if self
.date_debut
is not None:
752 annee
= self
.date_debut
.year
753 if self
.dossier
.poste
.date_debut
is not None:
754 annee
= self
.dossier
.poste
.date_debut
.year
756 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise_id
, annee
=annee
)]
759 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
))
763 def montant_euro(self
):
764 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
766 def montant_euro_mois(self
):
767 return round(self
.montant_euro() / 12, 2)
769 def __unicode__(self
):
771 devise
= self
.devise
.code
774 return "%s %s" % (self
.montant
, devise
)
778 verbose_name
= u
"Rémunération"
779 verbose_name_plural
= u
"Rémunérations"
782 class Remuneration(Remuneration_
):
788 class ContratManager(NoDeleteManager
):
789 def get_query_set(self
):
790 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
793 class Contrat_(AUFMetadata
):
794 """Document juridique qui encadre la relation de travail d'un Employe
795 pour un Poste particulier. Pour un Dossier (qui documente cette
796 relation de travail) plusieurs contrats peuvent être associés.
798 objects
= ContratManager()
799 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
800 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
802 verbose_name
= u
"Type de contrat")
803 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
804 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
805 null
=True, blank
=True)
806 fichier
= models
.FileField(verbose_name
= u
"Fichier",
807 upload_to
=contrat_dispatch
,
808 storage
=storage_prive
,
809 null
=True, blank
=True)
813 ordering
= ['dossier__employe__nom']
814 verbose_name
= u
"Contrat"
815 verbose_name_plural
= u
"Contrats"
817 def __unicode__(self
):
818 return u
'%s - %s' % (self
.dossier
, self
.id)
820 class Contrat(Contrat_
):
826 #class Evenement_(AUFMetadata):
827 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
828 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
829 # (ex.: la Remuneration).
831 # Ex.: congé de maternité, maladie...
833 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
834 # différent et une rémunération en conséquence. On souhaite toutefois
835 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
836 # du retour à la normale.
838 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
840 # nom = models.CharField(max_length=255)
841 # date_debut = models.DateField(verbose_name = u"Date de début")
842 # date_fin = models.DateField(verbose_name = u"Date de fin",
843 # null=True, blank=True)
848 # verbose_name = u"Évènement"
849 # verbose_name_plural = u"Évènements"
851 # def __unicode__(self):
852 # return u'%s' % (self.nom)
855 #class Evenement(Evenement_):
856 # __doc__ = Evenement_.__doc__
859 #class EvenementRemuneration_(RemunerationMixin):
860 # """Structure de rémunération liée à un Evenement qui remplace
861 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
864 # evenement = models.ForeignKey("Evenement", db_column='evenement',
866 # verbose_name = u"Évènement")
867 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
868 # # de l'Evenement associé
872 # ordering = ['evenement', 'type__nom', '-date_fin']
873 # verbose_name = u"Évènement - rémunération"
874 # verbose_name_plural = u"Évènements - rémunérations"
877 #class EvenementRemuneration(EvenementRemuneration_):
878 # __doc__ = EvenementRemuneration_.__doc__
884 #class EvenementRemuneration(EvenementRemuneration_):
885 # __doc__ = EvenementRemuneration_.__doc__
886 # TODO? class ContratPiece(models.Model):
891 class FamilleEmploi(AUFMetadata
):
892 """Catégorie utilisée dans la gestion des Postes.
893 Catégorie supérieure à TypePoste.
895 nom
= models
.CharField(max_length
=255)
899 verbose_name
= u
"Famille d'emploi"
900 verbose_name_plural
= u
"Familles d'emploi"
902 def __unicode__(self
):
903 return u
'%s' % (self
.nom
)
905 class TypePoste(AUFMetadata
):
906 """Catégorie de Poste.
908 nom
= models
.CharField(max_length
=255)
909 nom_feminin
= models
.CharField(max_length
=255,
910 verbose_name
= u
"Nom féminin")
912 is_responsable
= models
.BooleanField(default
=False,
913 verbose_name
= u
"Poste de responsabilité")
914 famille_emploi
= models
.ForeignKey('FamilleEmploi',
915 db_column
='famille_emploi',
917 verbose_name
= u
"Famille d'emploi")
921 verbose_name
= u
"Type de poste"
922 verbose_name_plural
= u
"Types de poste"
924 def __unicode__(self
):
925 return u
'%s' % (self
.nom
)
928 TYPE_PAIEMENT_CHOICES
= (
929 (u
'Régulier', u
'Régulier'),
930 (u
'Ponctuel', u
'Ponctuel'),
933 NATURE_REMUNERATION_CHOICES
= (
934 (u
'Accessoire', u
'Accessoire'),
935 (u
'Charges', u
'Charges'),
936 (u
'Indemnité', u
'Indemnité'),
937 (u
'RAS', u
'Rémunération autre source'),
938 (u
'Traitement', u
'Traitement'),
941 class TypeRemuneration(AUFMetadata
):
942 """Catégorie de Remuneration.
944 nom
= models
.CharField(max_length
=255)
945 type_paiement
= models
.CharField(max_length
=30,
946 choices
=TYPE_PAIEMENT_CHOICES
,
947 verbose_name
= u
"Type de paiement")
948 nature_remuneration
= models
.CharField(max_length
=30,
949 choices
=NATURE_REMUNERATION_CHOICES
,
950 verbose_name
= u
"Nature de la rémunération")
954 verbose_name
= u
"Type de rémunération"
955 verbose_name_plural
= u
"Types de rémunération"
957 def __unicode__(self
):
958 return u
'%s' % (self
.nom
)
960 class TypeRevalorisation(AUFMetadata
):
961 """Justification du changement de la Remuneration.
962 (Actuellement utilisé dans aucun traitement informatique.)
964 nom
= models
.CharField(max_length
=255)
968 verbose_name
= u
"Type de revalorisation"
969 verbose_name_plural
= u
"Types de revalorisation"
971 def __unicode__(self
):
972 return u
'%s' % (self
.nom
)
974 class Service(AUFMetadata
):
975 """Unité administrative où les Postes sont rattachés.
977 nom
= models
.CharField(max_length
=255)
981 verbose_name
= u
"Service"
982 verbose_name_plural
= u
"Services"
984 def __unicode__(self
):
985 return u
'%s' % (self
.nom
)
988 TYPE_ORGANISME_CHOICES
= (
989 ('MAD', 'Mise à disposition'),
990 ('DET', 'Détachement'),
993 class OrganismeBstg(AUFMetadata
):
994 """Organisation d'où provient un Employe mis à disposition (MAD) de
995 ou détaché (DET) à l'AUF à titre gratuit.
997 (BSTG = bien et service à titre gratuit.)
999 nom
= models
.CharField(max_length
=255)
1000 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1001 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1003 related_name
='organismes_bstg',
1004 null
=True, blank
=True)
1007 ordering
= ['type', 'nom']
1008 verbose_name
= u
"Organisme BSTG"
1009 verbose_name_plural
= u
"Organismes BSTG"
1011 def __unicode__(self
):
1012 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1014 prefix_implantation
= "pays__region"
1015 def get_regions(self
):
1016 return [self
.pays
.region
]
1019 class Statut(AUFMetadata
):
1020 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1023 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.")
1024 nom
= models
.CharField(max_length
=255)
1028 verbose_name
= u
"Statut d'employé"
1029 verbose_name_plural
= u
"Statuts d'employé"
1031 def __unicode__(self
):
1032 return u
'%s : %s' % (self
.code
, self
.nom
)
1035 TYPE_CLASSEMENT_CHOICES
= (
1036 ('S', 'S -Soutien'),
1037 ('T', 'T - Technicien'),
1038 ('P', 'P - Professionel'),
1040 ('D', 'D - Direction'),
1041 ('SO', 'SO - Sans objet [expatriés]'),
1042 ('HG', 'HG - Hors grille [direction]'),
1046 class Classement_(AUFMetadata
):
1047 """Éléments de classement de la
1048 "Grille générique de classement hiérarchique".
1050 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1051 classement dans la grille. Le classement donne le coefficient utilisé dans:
1053 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1056 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1057 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1058 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1059 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1062 # annee # au lieu de date_debut et date_fin
1063 commentaire
= models
.TextField(null
=True, blank
=True)
1067 ordering
= ['type','echelon','degre','coefficient']
1068 verbose_name
= u
"Classement"
1069 verbose_name_plural
= u
"Classements"
1071 def __unicode__(self
):
1072 return u
'%s.%s.%s (%s)' % (self
.type, self
.echelon
, self
.degre
,
1075 class Classement(Classement_
):
1076 __doc__
= Classement_
.__doc__
1079 class TauxChange_(AUFMetadata
):
1080 """Taux de change de la devise vers l'euro (EUR)
1081 pour chaque année budgétaire.
1084 devise
= models
.ForeignKey('Devise', db_column
='devise')
1085 annee
= models
.IntegerField(verbose_name
= u
"Année")
1086 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1090 ordering
= ['-annee', 'devise__code']
1091 verbose_name
= u
"Taux de change"
1092 verbose_name_plural
= u
"Taux de change"
1094 def __unicode__(self
):
1095 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1098 class TauxChange(TauxChange_
):
1099 __doc__
= TauxChange_
.__doc__
1101 class ValeurPointManager(NoDeleteManager
):
1103 def get_query_set(self
):
1104 now
= datetime
.datetime
.now()
1105 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1108 class ValeurPoint_(AUFMetadata
):
1109 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1110 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1111 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1113 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1116 actuelles
= ValeurPointManager()
1118 valeur
= models
.FloatField(null
=True)
1119 devise
= models
.ForeignKey('Devise', db_column
='devise', null
=True,
1120 related_name
='+', default
=5)
1121 implantation
= models
.ForeignKey(ref
.Implantation
,
1122 db_column
='implantation',
1123 related_name
='%(app_label)s_valeur_point')
1125 annee
= models
.IntegerField()
1128 ordering
= ['-annee', 'implantation__nom']
1130 verbose_name
= u
"Valeur du point"
1131 verbose_name_plural
= u
"Valeurs du point"
1133 def __unicode__(self
):
1134 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1137 class ValeurPoint(ValeurPoint_
):
1138 __doc__
= ValeurPoint_
.__doc__
1142 class Devise(AUFMetadata
):
1143 """Devise monétaire.
1145 code
= models
.CharField(max_length
=10, unique
=True)
1146 nom
= models
.CharField(max_length
=255)
1150 verbose_name
= u
"Devise"
1151 verbose_name_plural
= u
"Devises"
1153 def __unicode__(self
):
1154 return u
'%s - %s' % (self
.code
, self
.nom
)
1156 class TypeContrat(AUFMetadata
):
1159 nom
= models
.CharField(max_length
=255)
1160 nom_long
= models
.CharField(max_length
=255)
1164 verbose_name
= u
"Type de contrat"
1165 verbose_name_plural
= u
"Types de contrat"
1167 def __unicode__(self
):
1168 return u
'%s' % (self
.nom
)
1173 class ResponsableImplantation(AUFMetadata
):
1174 """Le responsable d'une implantation.
1175 Anciennement géré sur le Dossier du responsable.
1177 employe
= models
.ForeignKey('Employe', db_column
='employe',
1179 null
=True, blank
=True)
1180 implantation
= models
.ForeignKey(ref
.Implantation
,
1181 db_column
='implantation', related_name
='+',
1184 def __unicode__(self
):
1185 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1188 ordering
= ['implantation__nom']
1189 verbose_name
= "Responsable d'implantation"
1190 verbose_name_plural
= "Responsables d'implantation"