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
, DeviseManager
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",
97 responsable
= models
.ForeignKey('Poste', db_column
='responsable',
100 help_text
=u
"Taper le nom du poste ou du type de poste",
101 verbose_name
= u
"Poste du responsable", )
104 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
105 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
106 verbose_name
= u
"Temps de travail",
107 help_text
="% du temps complet")
108 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
109 decimal_places
=2, null
=True,
110 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
111 verbose_name
= u
"Nb. heures par semaine")
114 local
= models
.NullBooleanField(verbose_name
= u
"Local", default
=True,
115 null
=True, blank
=True)
116 expatrie
= models
.NullBooleanField(verbose_name
= u
"Expatrié", default
=False,
117 null
=True, blank
=True)
118 mise_a_disposition
= models
.NullBooleanField(
119 verbose_name
= u
"Mise à disposition",
120 null
=True, default
=False)
121 appel
= models
.CharField(max_length
=10, null
=True,
122 verbose_name
= u
"Appel à candidature",
123 choices
=POSTE_APPEL_CHOICES
,
127 classement_min
= models
.ForeignKey('Classement',
128 db_column
='classement_min', related_name
='+',
129 null
=True, blank
=True)
130 classement_max
= models
.ForeignKey('Classement',
131 db_column
='classement_max', related_name
='+',
132 null
=True, blank
=True)
133 valeur_point_min
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
134 db_column
='valeur_point_min', related_name
='+',
135 null
=True, blank
=True)
136 valeur_point_max
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
137 db_column
='valeur_point_max', related_name
='+',
138 null
=True, blank
=True)
139 devise_min
= models
.ForeignKey('Devise', db_column
='devise_min', null
=True,
141 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
143 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
144 null
=True, default
=0)
145 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
146 null
=True, default
=0)
147 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
148 null
=True, default
=0)
149 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
150 null
=True, default
=0)
151 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
152 null
=True, default
=0)
153 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
154 null
=True, default
=0)
156 # Comparatifs de rémunération
157 devise_comparaison
= models
.ForeignKey('Devise', null
=True,
158 db_column
='devise_comparaison',
160 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
161 null
=True, blank
=True)
162 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
163 null
=True, blank
=True)
164 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
165 null
=True, blank
=True)
166 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
167 null
=True, blank
=True)
168 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
169 null
=True, blank
=True)
170 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
171 null
=True, blank
=True)
172 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
173 null
=True, blank
=True)
174 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
175 null
=True, blank
=True)
176 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
177 null
=True, blank
=True)
178 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
179 null
=True, blank
=True)
182 justification
= models
.TextField(null
=True, blank
=True)
185 date_debut
= models
.DateField(verbose_name
=u
"Date de début", help_text
=HELP_TEXT_DATE
,
186 null
=True, blank
=True)
187 date_fin
= models
.DateField(verbose_name
=u
"Date de fin", help_text
=HELP_TEXT_DATE
,
188 null
=True, blank
=True)
192 ordering
= ['implantation__nom', 'nom']
193 verbose_name
= u
"Poste"
194 verbose_name_plural
= u
"Postes"
197 def __unicode__(self
):
198 representation
= u
'%s - %s [%s]' % (self
.implantation
, self
.nom
,
200 return representation
203 prefix_implantation
= "implantation__region"
204 def get_regions(self
):
205 return [self
.implantation
.region
]
209 __doc__
= Poste_
.__doc__
211 # meta dématérialisation : pour permettre le filtrage
212 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
216 if self
.occupe_par():
220 def occupe_par(self
):
221 """Retourne la liste d'employé occupant ce poste.
222 Généralement, retourne une liste d'un élément.
223 Si poste inoccupé, retourne liste vide.
224 UTILISE pour mettre a jour le flag vacant
226 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
229 POSTE_FINANCEMENT_CHOICES
= (
230 ('A', 'A - Frais de personnel'),
231 ('B', 'B - Projet(s)-Titre(s)'),
236 class PosteFinancement_(models
.Model
):
237 """Pour un Poste, structure d'informations décrivant comment on prévoit
240 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
241 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
242 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
243 help_text
="ex.: 33.33 % (décimale avec point)")
244 commentaire
= models
.TextField(
245 help_text
="Spécifiez la source de financement.")
251 def __unicode__(self
):
252 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
255 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
258 class PosteFinancement(PosteFinancement_
):
262 class PostePiece_(models
.Model
):
263 """Documents relatifs au Poste.
264 Ex.: Description de poste
266 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
267 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
268 fichier
= models
.FileField(verbose_name
= u
"Fichier",
269 upload_to
=poste_piece_dispatch
,
270 storage
=storage_prive
)
276 def __unicode__(self
):
277 return u
'%s' % (self
.nom
)
279 class PostePiece(PostePiece_
):
282 class PosteComparaison_(AUFMetadata
):
284 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
286 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
287 objects
= PosteComparaisonManager()
289 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
290 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
291 montant
= models
.IntegerField(null
=True)
292 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
297 def taux_devise(self
):
298 if self
.devise
.code
== "EUR":
300 annee
= self
.poste
.date_debut
.year
301 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
304 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
))
308 def montant_euros(self
):
309 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
311 def __unicode__(self
):
314 class PosteComparaison(PosteComparaison_
):
317 class PosteCommentaire_(Commentaire
):
318 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
323 class PosteCommentaire(PosteCommentaire_
):
328 class Employe(AUFMetadata
):
329 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
330 Dossiers qu'il occupe ou a occupé de Postes.
332 Cette classe aurait pu avantageusement s'appeler Personne car la notion
333 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
336 nom
= models
.CharField(max_length
=255)
337 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
338 nom_affichage
= models
.CharField(max_length
=255,
339 verbose_name
= u
"Nom d'affichage",
340 null
=True, blank
=True)
341 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
342 db_column
='nationalite',
343 related_name
='employes_nationalite',
344 verbose_name
= u
"Nationalité",
345 blank
=True, null
=True)
346 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
347 help_text
=HELP_TEXT_DATE
,
348 validators
=[validate_date_passee
],
349 null
=True, blank
=True)
350 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
353 situation_famille
= models
.CharField(max_length
=1,
354 choices
=SITUATION_CHOICES
,
355 verbose_name
= u
"Situation familiale",
356 null
=True, blank
=True)
357 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
358 help_text
=HELP_TEXT_DATE
,
359 null
=True, blank
=True)
362 tel_domicile
= models
.CharField(max_length
=255,
363 verbose_name
= u
"Tél. domicile",
364 null
=True, blank
=True)
365 tel_cellulaire
= models
.CharField(max_length
=255,
366 verbose_name
= u
"Tél. cellulaire",
367 null
=True, blank
=True)
368 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
369 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
370 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
371 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
372 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
373 related_name
='employes',
374 null
=True, blank
=True)
376 # meta dématérialisation : pour permettre le filtrage
377 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
380 ordering
= ['nom','prenom']
381 verbose_name
= u
"Employé"
382 verbose_name_plural
= u
"Employés"
384 def __unicode__(self
):
385 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
389 if self
.genre
.upper() == u
'M':
391 elif self
.genre
.upper() == u
'F':
396 """Retourne l'URL du service retournant la photo de l'Employe.
397 Équivalent reverse url 'rh_photo' avec id en param.
399 from django
.core
.urlresolvers
import reverse
400 return reverse('rh_photo', kwargs
={'id':self
.id})
402 def dossiers_passes(self
):
404 dossiers_passes
= self
.dossiers
.filter(date_fin__lt
=today
).order_by('-date_fin')
405 for d
in dossiers_passes
:
407 return dossiers_passes
409 def dossiers_futurs(self
):
411 return self
.dossiers
.filter(date_debut__gt
=today
).order_by('-date_fin')
413 def dossiers_encours(self
):
414 dossiers_p_f
= self
.dossiers_passes() | self
.dossiers_futurs()
415 ids_dossiers_p_f
= [d
.id for d
in dossiers_p_f
]
416 dossiers_encours
= self
.dossiers
.exclude(id__in
=ids_dossiers_p_f
).order_by('-date_fin')
418 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
419 for d
in dossiers_encours
:
420 d
.remunerations
= Remuneration
.objects
.filter(dossier
=d
.id).order_by('-id')
421 return dossiers_encours
423 def postes_encours(self
):
424 postes_encours
= set()
425 for d
in self
.dossiers_encours():
426 postes_encours
.add(d
.poste
)
427 return postes_encours
429 def poste_principal(self
):
431 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
433 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
435 poste
= Poste
.objects
.none()
437 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
442 prefix_implantation
= "rh_dossiers__poste__implantation__region"
443 def get_regions(self
):
445 for d
in self
.dossiers
.all():
446 regions
.append(d
.poste
.implantation
.region
)
450 class EmployePiece(models
.Model
):
451 """Documents relatifs à un employé.
454 employe
= models
.ForeignKey('Employe', db_column
='employe')
455 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
456 fichier
= models
.FileField(verbose_name
="Fichier",
457 upload_to
=employe_piece_dispatch
,
458 storage
=storage_prive
)
462 verbose_name
= u
"Employé pièce"
463 verbose_name_plural
= u
"Employé pièces"
465 def __unicode__(self
):
466 return u
'%s' % (self
.nom
)
468 class EmployeCommentaire(Commentaire
):
469 employe
= models
.ForeignKey('Employe', db_column
='employe',
473 verbose_name
= u
"Employé commentaire"
474 verbose_name_plural
= u
"Employé commentaires"
477 LIEN_PARENTE_CHOICES
= (
478 ('Conjoint', 'Conjoint'),
479 ('Conjointe', 'Conjointe'),
484 class AyantDroit(AUFMetadata
):
485 """Personne en relation avec un Employe.
488 nom
= models
.CharField(max_length
=255)
489 prenom
= models
.CharField(max_length
=255,
490 verbose_name
= u
"Prénom",)
491 nom_affichage
= models
.CharField(max_length
=255,
492 verbose_name
= u
"Nom d'affichage",
493 null
=True, blank
=True)
494 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
495 db_column
='nationalite',
496 related_name
='ayantdroits_nationalite',
497 verbose_name
= u
"Nationalité",
498 null
=True, blank
=True)
499 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
500 help_text
=HELP_TEXT_DATE
,
501 validators
=[validate_date_passee
],
502 null
=True, blank
=True)
503 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
506 employe
= models
.ForeignKey('Employe', db_column
='employe',
507 related_name
='ayantdroits',
508 verbose_name
= u
"Employé")
509 lien_parente
= models
.CharField(max_length
=10,
510 choices
=LIEN_PARENTE_CHOICES
,
511 verbose_name
= u
"Lien de parenté",
512 null
=True, blank
=True)
516 verbose_name
= u
"Ayant droit"
517 verbose_name_plural
= u
"Ayants droit"
519 def __unicode__(self
):
520 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
522 prefix_implantation
= "employe__dossiers__poste__implantation__region"
523 def get_regions(self
):
525 for d
in self
.employe
.dossiers
.all():
526 regions
.append(d
.poste
.implantation
.region
)
530 class AyantDroitCommentaire(Commentaire
):
531 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
537 STATUT_RESIDENCE_CHOICES
= (
539 ('expat', 'Expatrié'),
542 COMPTE_COMPTA_CHOICES
= (
548 class Dossier_(AUFMetadata
):
549 """Le Dossier regroupe les informations relatives à l'occupation
550 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
553 Plusieurs Contrats peuvent être associés au Dossier.
554 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
555 lequel aucun Dossier n'existe est un poste vacant.
558 objects
= DossierManager()
561 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
562 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
563 db_column
='organisme_bstg',
565 verbose_name
= u
"Organisme",
566 help_text
="Si détaché (DET) ou \
567 mis à disposition (MAD), \
568 préciser l'organisme.",
569 null
=True, blank
=True)
572 remplacement
= models
.BooleanField(default
=False)
573 remplacement_de
= models
.ForeignKey('self', related_name
='+',
574 help_text
=u
"Taper le nom de l'employé",
575 null
=True, blank
=True)
576 statut_residence
= models
.CharField(max_length
=10, default
='local',
577 verbose_name
= u
"Statut", null
=True,
578 choices
=STATUT_RESIDENCE_CHOICES
)
581 classement
= models
.ForeignKey('Classement', db_column
='classement',
583 null
=True, blank
=True)
584 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
586 default
=REGIME_TRAVAIL_DEFAULT
,
587 verbose_name
= u
"Régime de travail",
588 help_text
="% du temps complet")
589 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
590 decimal_places
=2, null
=True,
591 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
592 verbose_name
= u
"Nb. heures par semaine")
594 # Occupation du Poste par cet Employe (anciennement "mandat")
595 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
597 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
599 null
=True, blank
=True)
606 ordering
= ['employe__nom', ]
607 verbose_name
= u
"Dossier"
608 verbose_name_plural
= "Dossiers"
610 def salaire_theorique(self
):
611 annee
= date
.today().year
612 coeff
= self
.classement
.coefficient
613 implantation
= self
.poste
.implantation
614 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
616 montant
= coeff
* point
.valeur
617 devise
= point
.devise
618 return {'montant':montant
, 'devise':devise
}
620 def __unicode__(self
):
621 poste
= self
.poste
.nom
622 if self
.employe
.genre
== 'F':
623 poste
= self
.poste
.nom_feminin
624 return u
'%s - %s' % (self
.employe
, poste
)
626 prefix_implantation
= "poste__implantation__region"
627 def get_regions(self
):
628 return [self
.poste
.implantation
.region
]
631 def remunerations(self
):
632 return self
.rh_remunerations
.all().order_by('date_debut')
634 def remunerations_en_cours(self
):
635 return self
.rh_remunerations
.all().filter(date_fin__exact
=None).order_by('date_debut')
637 def get_salaire(self
):
639 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
643 class Dossier(Dossier_
):
644 __doc__
= Dossier_
.__doc__
645 poste
= models
.ForeignKey('%s.Poste' % app_context(),
647 related_name
='%(app_label)s_dossiers',
648 help_text
=u
"Taper le nom du poste ou du type de poste",
650 employe
= models
.ForeignKey('Employe', db_column
='employe',
651 help_text
=u
"Taper le nom de l'employé",
652 related_name
='%(app_label)s_dossiers',
653 verbose_name
=u
"Employé")
656 class DossierPiece_(models
.Model
):
657 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
658 Ex.: Lettre de motivation.
660 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
661 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
662 fichier
= models
.FileField(verbose_name
= u
"Fichier",
663 upload_to
=dossier_piece_dispatch
,
664 storage
=storage_prive
)
670 def __unicode__(self
):
671 return u
'%s' % (self
.nom
)
673 class DossierPiece(DossierPiece_
):
676 class DossierCommentaire_(Commentaire
):
677 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
681 class DossierCommentaire(DossierCommentaire_
):
684 class DossierComparaison_(models
.Model
):
686 Photo d'une comparaison salariale au moment de l'embauche.
688 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
689 objects
= DossierComparaisonManager()
691 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
692 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
693 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
694 montant
= models
.IntegerField(null
=True)
695 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
700 def taux_devise(self
):
701 annee
= self
.dossier
.poste
.date_debut
.year
702 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
705 raise Exception(u
"Le taux de la devise %s n'a pas n'existe pas pour %s ou il existe plusieurs taux pour la même année" % (self
.devise
.id, annee
))
709 def montant_euros(self
):
710 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
712 class DossierComparaison(DossierComparaison_
):
717 class RemunerationMixin(AUFMetadata
):
718 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
720 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
722 verbose_name
= u
"Type de rémunération")
723 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
724 db_column
='type_revalorisation',
726 verbose_name
= u
"Type de revalorisation",
727 null
=True, blank
=True)
728 montant
= models
.DecimalField(null
=True, blank
=True,
729 default
=0, max_digits
=12, decimal_places
=2)
730 # Annuel (12 mois, 52 semaines, 364 jours?)
731 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
732 # commentaire = precision
733 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
734 # date_debut = anciennement date_effectif
735 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
736 null
=True, blank
=True)
737 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
738 null
=True, blank
=True)
742 ordering
= ['type__nom', '-date_fin']
744 def __unicode__(self
):
745 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
747 class Remuneration_(RemunerationMixin
):
748 """Structure de rémunération (données budgétaires) en situation normale
749 pour un Dossier. Si un Evenement existe, utiliser la structure de
750 rémunération EvenementRemuneration de cet événement.
753 def montant_mois(self
):
754 return round(self
.montant
/ 12, 2)
756 def taux_devise(self
):
757 if self
.devise
.code
== "EUR":
760 annee
= datetime
.datetime
.now().year
761 if self
.date_debut
is not None:
762 annee
= self
.date_debut
.year
763 if self
.dossier
.poste
.date_debut
is not None:
764 annee
= self
.dossier
.poste
.date_debut
.year
766 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise_id
, annee
=annee
)]
769 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
))
773 def montant_euro(self
):
774 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
776 def montant_euro_mois(self
):
777 return round(self
.montant_euro() / 12, 2)
779 def __unicode__(self
):
781 devise
= self
.devise
.code
784 return "%s %s" % (self
.montant
, devise
)
788 verbose_name
= u
"Rémunération"
789 verbose_name_plural
= u
"Rémunérations"
792 class Remuneration(Remuneration_
):
798 class ContratManager(NoDeleteManager
):
799 def get_query_set(self
):
800 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
803 class Contrat_(AUFMetadata
):
804 """Document juridique qui encadre la relation de travail d'un Employe
805 pour un Poste particulier. Pour un Dossier (qui documente cette
806 relation de travail) plusieurs contrats peuvent être associés.
808 objects
= ContratManager()
809 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
810 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
812 verbose_name
= u
"type de contrat")
813 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
814 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
815 null
=True, blank
=True)
816 fichier
= models
.FileField(verbose_name
= u
"Fichier",
817 upload_to
=contrat_dispatch
,
818 storage
=storage_prive
,
819 null
=True, blank
=True)
823 ordering
= ['dossier__employe__nom']
824 verbose_name
= u
"Contrat"
825 verbose_name_plural
= u
"Contrats"
827 def __unicode__(self
):
828 return u
'%s - %s' % (self
.dossier
, self
.id)
830 class Contrat(Contrat_
):
836 #class Evenement_(AUFMetadata):
837 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
838 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
839 # (ex.: la Remuneration).
841 # Ex.: congé de maternité, maladie...
843 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
844 # différent et une rémunération en conséquence. On souhaite toutefois
845 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
846 # du retour à la normale.
848 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
850 # nom = models.CharField(max_length=255)
851 # date_debut = models.DateField(verbose_name = u"Date de début")
852 # date_fin = models.DateField(verbose_name = u"Date de fin",
853 # null=True, blank=True)
858 # verbose_name = u"Évènement"
859 # verbose_name_plural = u"Évènements"
861 # def __unicode__(self):
862 # return u'%s' % (self.nom)
865 #class Evenement(Evenement_):
866 # __doc__ = Evenement_.__doc__
869 #class EvenementRemuneration_(RemunerationMixin):
870 # """Structure de rémunération liée à un Evenement qui remplace
871 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
874 # evenement = models.ForeignKey("Evenement", db_column='evenement',
876 # verbose_name = u"Évènement")
877 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
878 # # de l'Evenement associé
882 # ordering = ['evenement', 'type__nom', '-date_fin']
883 # verbose_name = u"Évènement - rémunération"
884 # verbose_name_plural = u"Évènements - rémunérations"
887 #class EvenementRemuneration(EvenementRemuneration_):
888 # __doc__ = EvenementRemuneration_.__doc__
894 #class EvenementRemuneration(EvenementRemuneration_):
895 # __doc__ = EvenementRemuneration_.__doc__
896 # TODO? class ContratPiece(models.Model):
901 class FamilleEmploi(AUFMetadata
):
902 """Catégorie utilisée dans la gestion des Postes.
903 Catégorie supérieure à TypePoste.
905 nom
= models
.CharField(max_length
=255)
909 verbose_name
= u
"Famille d'emploi"
910 verbose_name_plural
= u
"Familles d'emploi"
912 def __unicode__(self
):
913 return u
'%s' % (self
.nom
)
915 class TypePoste(AUFMetadata
):
916 """Catégorie de Poste.
918 nom
= models
.CharField(max_length
=255)
919 nom_feminin
= models
.CharField(max_length
=255,
920 verbose_name
= u
"Nom féminin")
922 is_responsable
= models
.BooleanField(default
=False,
923 verbose_name
= u
"Poste de responsabilité")
924 famille_emploi
= models
.ForeignKey('FamilleEmploi',
925 db_column
='famille_emploi',
927 verbose_name
= u
"famille d'emploi")
931 verbose_name
= u
"Type de poste"
932 verbose_name_plural
= u
"Types de poste"
934 def __unicode__(self
):
935 return u
'%s' % (self
.nom
)
938 TYPE_PAIEMENT_CHOICES
= (
939 (u
'Régulier', u
'Régulier'),
940 (u
'Ponctuel', u
'Ponctuel'),
943 NATURE_REMUNERATION_CHOICES
= (
944 (u
'Accessoire', u
'Accessoire'),
945 (u
'Charges', u
'Charges'),
946 (u
'Indemnité', u
'Indemnité'),
947 (u
'RAS', u
'Rémunération autre source'),
948 (u
'Traitement', u
'Traitement'),
951 class TypeRemuneration(AUFMetadata
):
952 """Catégorie de Remuneration.
954 nom
= models
.CharField(max_length
=255)
955 type_paiement
= models
.CharField(max_length
=30,
956 choices
=TYPE_PAIEMENT_CHOICES
,
957 verbose_name
= u
"Type de paiement")
958 nature_remuneration
= models
.CharField(max_length
=30,
959 choices
=NATURE_REMUNERATION_CHOICES
,
960 verbose_name
= u
"Nature de la rémunération")
964 verbose_name
= u
"Type de rémunération"
965 verbose_name_plural
= u
"Types de rémunération"
967 def __unicode__(self
):
968 return u
'%s' % (self
.nom
)
970 class TypeRevalorisation(AUFMetadata
):
971 """Justification du changement de la Remuneration.
972 (Actuellement utilisé dans aucun traitement informatique.)
974 nom
= models
.CharField(max_length
=255)
978 verbose_name
= u
"Type de revalorisation"
979 verbose_name_plural
= u
"Types de revalorisation"
981 def __unicode__(self
):
982 return u
'%s' % (self
.nom
)
984 class Service(AUFMetadata
):
985 """Unité administrative où les Postes sont rattachés.
987 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
988 nom
= models
.CharField(max_length
=255)
992 verbose_name
= u
"Service"
993 verbose_name_plural
= u
"Services"
995 def __unicode__(self
):
997 archive
= u
"(archivé)"
1000 return u
'%s %s' % (self
.nom
, archive
)
1003 TYPE_ORGANISME_CHOICES
= (
1004 ('MAD', 'Mise à disposition'),
1005 ('DET', 'Détachement'),
1008 class OrganismeBstg(AUFMetadata
):
1009 """Organisation d'où provient un Employe mis à disposition (MAD) de
1010 ou détaché (DET) à l'AUF à titre gratuit.
1012 (BSTG = bien et service à titre gratuit.)
1014 nom
= models
.CharField(max_length
=255)
1015 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1016 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1018 related_name
='organismes_bstg',
1019 null
=True, blank
=True)
1022 ordering
= ['type', 'nom']
1023 verbose_name
= u
"Organisme BSTG"
1024 verbose_name_plural
= u
"Organismes BSTG"
1026 def __unicode__(self
):
1027 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1029 prefix_implantation
= "pays__region"
1030 def get_regions(self
):
1031 return [self
.pays
.region
]
1034 class Statut(AUFMetadata
):
1035 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1038 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.")
1039 nom
= models
.CharField(max_length
=255)
1043 verbose_name
= u
"Statut d'employé"
1044 verbose_name_plural
= u
"Statuts d'employé"
1046 def __unicode__(self
):
1047 return u
'%s : %s' % (self
.code
, self
.nom
)
1050 TYPE_CLASSEMENT_CHOICES
= (
1051 ('S', 'S -Soutien'),
1052 ('T', 'T - Technicien'),
1053 ('P', 'P - Professionel'),
1055 ('D', 'D - Direction'),
1056 ('SO', 'SO - Sans objet [expatriés]'),
1057 ('HG', 'HG - Hors grille [direction]'),
1060 class ClassementManager(models
.Manager
):
1062 Ordonner les spcéfiquement les classements.
1064 def get_query_set(self
):
1065 qs
= super(self
.__class__
, self
).get_query_set()
1066 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1067 qs
= qs
.extra(order_by
=('ponderation', ))
1071 class Classement_(AUFMetadata
):
1072 """Éléments de classement de la
1073 "Grille générique de classement hiérarchique".
1075 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1076 classement dans la grille. Le classement donne le coefficient utilisé dans:
1078 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1080 objects
= ClassementManager()
1083 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1084 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1085 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1086 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1089 # annee # au lieu de date_debut et date_fin
1090 commentaire
= models
.TextField(null
=True, blank
=True)
1094 ordering
= ['type','echelon','degre','coefficient']
1095 verbose_name
= u
"Classement"
1096 verbose_name_plural
= u
"Classements"
1098 def __unicode__(self
):
1099 return u
'%s.%s.%s (%s)' % (self
.type, self
.echelon
, self
.degre
,
1102 class Classement(Classement_
):
1103 __doc__
= Classement_
.__doc__
1106 class TauxChange_(AUFMetadata
):
1107 """Taux de change de la devise vers l'euro (EUR)
1108 pour chaque année budgétaire.
1111 devise
= models
.ForeignKey('Devise', db_column
='devise')
1112 annee
= models
.IntegerField(verbose_name
= u
"Année")
1113 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1117 ordering
= ['-annee', 'devise__code']
1118 verbose_name
= u
"Taux de change"
1119 verbose_name_plural
= u
"Taux de change"
1121 def __unicode__(self
):
1122 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1125 class TauxChange(TauxChange_
):
1126 __doc__
= TauxChange_
.__doc__
1128 class ValeurPointManager(NoDeleteManager
):
1130 def get_query_set(self
):
1131 now
= datetime
.datetime
.now()
1132 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1135 class ValeurPoint_(AUFMetadata
):
1136 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1137 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1138 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1140 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1143 actuelles
= ValeurPointManager()
1145 valeur
= models
.FloatField(null
=True)
1146 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1147 implantation
= models
.ForeignKey(ref
.Implantation
,
1148 db_column
='implantation',
1149 related_name
='%(app_label)s_valeur_point')
1151 annee
= models
.IntegerField()
1154 ordering
= ['-annee', 'implantation__nom']
1156 verbose_name
= u
"Valeur du point"
1157 verbose_name_plural
= u
"Valeurs du point"
1159 def __unicode__(self
):
1160 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1163 class ValeurPoint(ValeurPoint_
):
1164 __doc__
= ValeurPoint_
.__doc__
1168 class Devise(AUFMetadata
):
1169 """Devise monétaire.
1172 objects
= DeviseManager()
1174 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1175 code
= models
.CharField(max_length
=10, unique
=True)
1176 nom
= models
.CharField(max_length
=255)
1180 verbose_name
= u
"Devise"
1181 verbose_name_plural
= u
"Devises"
1183 def __unicode__(self
):
1184 return u
'%s - %s' % (self
.code
, self
.nom
)
1186 class TypeContrat(AUFMetadata
):
1189 nom
= models
.CharField(max_length
=255)
1190 nom_long
= models
.CharField(max_length
=255)
1194 verbose_name
= u
"Type de contrat"
1195 verbose_name_plural
= u
"Types de contrat"
1197 def __unicode__(self
):
1198 return u
'%s' % (self
.nom
)
1203 class ResponsableImplantation(AUFMetadata
):
1204 """Le responsable d'une implantation.
1205 Anciennement géré sur le Dossier du responsable.
1207 employe
= models
.ForeignKey('Employe', db_column
='employe',
1209 null
=True, blank
=True)
1210 implantation
= models
.ForeignKey(ref
.Implantation
,
1211 db_column
='implantation', related_name
='+',
1214 def __unicode__(self
):
1215 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1218 ordering
= ['implantation__nom']
1219 verbose_name
= "Responsable d'implantation"
1220 verbose_name_plural
= "Responsables d'implantation"