1 # -=- encoding: utf-8 -=-
4 from datetime
import date
5 from decimal
import Decimal
7 from django
.core
.files
.storage
import FileSystemStorage
8 from django
.db
import models
9 from django
.conf
import settings
11 from auf
.django
.emploi
.models
import GENRE_CHOICES
, SITUATION_CHOICES
# devrait plutot être dans references
12 from auf
.django
.metadata
.models
import AUFMetadata
13 from auf
.django
.metadata
.managers
import NoDeleteManager
14 import auf
.django
.references
.models
as ref
15 from validators
import validate_date_passee
16 from managers
import PosteManager
, DossierManager
, DossierComparaisonManager
, PosteComparaisonManager
, DeviseManager
19 # Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage.
20 # Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK
23 models_stack
= [s
[1].split('/')[-2] for s
in inspect
.stack() if s
[1].endswith('models.py')]
24 return models_stack
[-1]
28 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
29 REGIME_TRAVAIL_DEFAULT
= Decimal('100.00')
30 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
= Decimal('35.00')
33 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
34 base_url
=settings
.PRIVE_MEDIA_URL
)
36 def poste_piece_dispatch(instance
, filename
):
37 path
= "%s/poste/%s/%s" % (instance
._meta
.app_label
, instance
.poste_id
, filename
)
40 def dossier_piece_dispatch(instance
, filename
):
41 path
= "%s/dossier/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
44 def employe_piece_dispatch(instance
, filename
):
45 path
= "%s/employe/%s/%s" % (instance
._meta
.app_label
, instance
.employe_id
, filename
)
48 def contrat_dispatch(instance
, filename
):
49 path
= "%s/contrat/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
53 class Commentaire(AUFMetadata
):
54 texte
= models
.TextField()
55 owner
= models
.ForeignKey('auth.User', db_column
='owner', related_name
='+', verbose_name
=u
"Commentaire de")
59 ordering
= ['-date_creation']
61 def __unicode__(self
):
62 return u
'%s' % (self
.texte
)
67 POSTE_APPEL_CHOICES
= (
68 ('interne', 'Interne'),
69 ('externe', 'Externe'),
72 class Poste_(AUFMetadata
):
73 """Un Poste est un emploi (job) à combler dans une implantation.
74 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
75 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
78 objects
= PosteManager()
81 nom
= models
.CharField(max_length
=255,
82 verbose_name
= u
"Titre du poste", )
83 nom_feminin
= models
.CharField(max_length
=255,
84 verbose_name
= u
"Titre du poste (au féminin)",
86 implantation
= models
.ForeignKey(ref
.Implantation
, help_text
=u
"Taper le nom de l'implantation ou sa région",
87 db_column
='implantation', related_name
='+')
88 type_poste
= models
.ForeignKey('TypePoste', db_column
='type_poste', help_text
=u
"Taper le nom du type de poste",
91 verbose_name
=u
"type de poste")
92 service
= models
.ForeignKey('Service', db_column
='service',
94 verbose_name
= u
"direction/service/pôle support",
96 responsable
= models
.ForeignKey('Poste', db_column
='responsable',
99 help_text
=u
"Taper le nom du poste ou du type de poste",
100 verbose_name
= u
"Poste du responsable", )
103 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
104 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
105 verbose_name
= u
"Temps de travail",
106 help_text
="% du temps complet")
107 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
108 decimal_places
=2, null
=True,
109 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
110 verbose_name
= u
"Nb. heures par semaine")
113 local
= models
.NullBooleanField(verbose_name
= u
"Local", default
=True,
114 null
=True, blank
=True)
115 expatrie
= models
.NullBooleanField(verbose_name
= u
"Expatrié", default
=False,
116 null
=True, blank
=True)
117 mise_a_disposition
= models
.NullBooleanField(
118 verbose_name
= u
"Mise à disposition",
119 null
=True, default
=False)
120 appel
= models
.CharField(max_length
=10, null
=True,
121 verbose_name
= u
"Appel à candidature",
122 choices
=POSTE_APPEL_CHOICES
,
126 classement_min
= models
.ForeignKey('Classement',
127 db_column
='classement_min', related_name
='+',
128 null
=True, blank
=True)
129 classement_max
= models
.ForeignKey('Classement',
130 db_column
='classement_max', related_name
='+',
131 null
=True, blank
=True)
132 valeur_point_min
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
133 db_column
='valeur_point_min', related_name
='+',
134 null
=True, blank
=True)
135 valeur_point_max
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
136 db_column
='valeur_point_max', related_name
='+',
137 null
=True, blank
=True)
138 devise_min
= models
.ForeignKey('Devise', db_column
='devise_min', null
=True,
140 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
142 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
143 null
=True, default
=0)
144 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
145 null
=True, default
=0)
146 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
147 null
=True, default
=0)
148 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
149 null
=True, default
=0)
150 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
151 null
=True, default
=0)
152 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
153 null
=True, default
=0)
155 # Comparatifs de rémunération
156 devise_comparaison
= models
.ForeignKey('Devise', null
=True, blank
=True,
157 db_column
='devise_comparaison',
159 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
160 null
=True, blank
=True)
161 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
162 null
=True, blank
=True)
163 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
164 null
=True, blank
=True)
165 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
166 null
=True, blank
=True)
167 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
168 null
=True, blank
=True)
169 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
170 null
=True, blank
=True)
171 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
172 null
=True, blank
=True)
173 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
174 null
=True, blank
=True)
175 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
176 null
=True, blank
=True)
177 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
178 null
=True, blank
=True)
181 justification
= models
.TextField(null
=True, blank
=True)
184 date_debut
= models
.DateField(verbose_name
=u
"Date de début", help_text
=HELP_TEXT_DATE
,
185 null
=True, blank
=True)
186 date_fin
= models
.DateField(verbose_name
=u
"Date de fin", help_text
=HELP_TEXT_DATE
,
187 null
=True, blank
=True)
191 ordering
= ['implantation__nom', 'nom']
192 verbose_name
= u
"Poste"
193 verbose_name_plural
= u
"Postes"
196 def __unicode__(self
):
197 representation
= u
'%s - %s [%s]' % (self
.implantation
, self
.nom
,
199 return representation
202 prefix_implantation
= "implantation__region"
203 def get_regions(self
):
204 return [self
.implantation
.region
]
208 __doc__
= Poste_
.__doc__
210 # meta dématérialisation : pour permettre le filtrage
211 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
215 if self
.occupe_par():
219 def occupe_par(self
):
220 """Retourne la liste d'employé occupant ce poste.
221 Généralement, retourne une liste d'un élément.
222 Si poste inoccupé, retourne liste vide.
223 UTILISE pour mettre a jour le flag vacant
225 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
228 POSTE_FINANCEMENT_CHOICES
= (
229 ('A', 'A - Frais de personnel'),
230 ('B', 'B - Projet(s)-Titre(s)'),
235 class PosteFinancement_(models
.Model
):
236 """Pour un Poste, structure d'informations décrivant comment on prévoit
239 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
240 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
241 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
242 help_text
="ex.: 33.33 % (décimale avec point)")
243 commentaire
= models
.TextField(
244 help_text
="Spécifiez la source de financement.")
250 def __unicode__(self
):
251 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
254 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
257 class PosteFinancement(PosteFinancement_
):
261 class PostePiece_(models
.Model
):
262 """Documents relatifs au Poste.
263 Ex.: Description de poste
265 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
266 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
267 fichier
= models
.FileField(verbose_name
= u
"Fichier",
268 upload_to
=poste_piece_dispatch
,
269 storage
=storage_prive
)
275 def __unicode__(self
):
276 return u
'%s' % (self
.nom
)
278 class PostePiece(PostePiece_
):
281 class PosteComparaison_(AUFMetadata
):
283 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
285 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
286 objects
= PosteComparaisonManager()
288 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
289 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
290 montant
= models
.IntegerField(null
=True)
291 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
296 def taux_devise(self
):
297 if self
.devise
is None:
299 if self
.devise
.code
== "EUR":
301 annee
= self
.poste
.date_debut
.year
302 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
305 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
))
309 def montant_euros(self
):
310 taux
= self
.taux_devise()
313 return round(float(self
.montant
) * float(taux
), 2)
315 def __unicode__(self
):
318 class PosteComparaison(PosteComparaison_
):
321 class PosteCommentaire_(Commentaire
):
322 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
327 class PosteCommentaire(PosteCommentaire_
):
332 class Employe(AUFMetadata
):
333 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
334 Dossiers qu'il occupe ou a occupé de Postes.
336 Cette classe aurait pu avantageusement s'appeler Personne car la notion
337 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
340 nom
= models
.CharField(max_length
=255)
341 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
342 nom_affichage
= models
.CharField(max_length
=255,
343 verbose_name
= u
"Nom d'affichage",
344 null
=True, blank
=True)
345 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
346 db_column
='nationalite',
347 related_name
='employes_nationalite',
348 verbose_name
= u
"Nationalité",
349 blank
=True, null
=True)
350 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
351 help_text
=HELP_TEXT_DATE
,
352 validators
=[validate_date_passee
],
353 null
=True, blank
=True)
354 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
357 situation_famille
= models
.CharField(max_length
=1,
358 choices
=SITUATION_CHOICES
,
359 verbose_name
= u
"Situation familiale",
360 null
=True, blank
=True)
361 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
362 help_text
=HELP_TEXT_DATE
,
363 null
=True, blank
=True)
366 tel_domicile
= models
.CharField(max_length
=255,
367 verbose_name
= u
"Tél. domicile",
368 null
=True, blank
=True)
369 tel_cellulaire
= models
.CharField(max_length
=255,
370 verbose_name
= u
"Tél. cellulaire",
371 null
=True, blank
=True)
372 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
373 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
374 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
375 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
376 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
377 related_name
='employes',
378 null
=True, blank
=True)
380 # meta dématérialisation : pour permettre le filtrage
381 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
384 ordering
= ['nom','prenom']
385 verbose_name
= u
"Employé"
386 verbose_name_plural
= u
"Employés"
388 def __unicode__(self
):
389 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
393 if self
.genre
.upper() == u
'M':
395 elif self
.genre
.upper() == u
'F':
400 """Retourne l'URL du service retournant la photo de l'Employe.
401 Équivalent reverse url 'rh_photo' avec id en param.
403 from django
.core
.urlresolvers
import reverse
404 return reverse('rh_photo', kwargs
={'id':self
.id})
406 def dossiers_passes(self
):
408 dossiers_passes
= self
.dossiers
.filter(date_fin__lt
=today
).order_by('-date_fin')
409 for d
in dossiers_passes
:
411 return dossiers_passes
413 def dossiers_futurs(self
):
415 return self
.dossiers
.filter(date_debut__gt
=today
).order_by('-date_fin')
417 def dossiers_encours(self
):
418 dossiers_p_f
= self
.dossiers_passes() | self
.dossiers_futurs()
419 ids_dossiers_p_f
= [d
.id for d
in dossiers_p_f
]
420 dossiers_encours
= self
.dossiers
.exclude(id__in
=ids_dossiers_p_f
).order_by('-date_fin')
422 # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set
423 for d
in dossiers_encours
:
424 d
.remunerations
= Remuneration
.objects
.filter(dossier
=d
.id).order_by('-id')
425 return dossiers_encours
427 def postes_encours(self
):
428 postes_encours
= set()
429 for d
in self
.dossiers_encours():
430 postes_encours
.add(d
.poste
)
431 return postes_encours
433 def poste_principal(self
):
435 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
437 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
439 poste
= Poste
.objects
.none()
441 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
446 prefix_implantation
= "rh_dossiers__poste__implantation__region"
447 def get_regions(self
):
449 for d
in self
.dossiers
.all():
450 regions
.append(d
.poste
.implantation
.region
)
454 class EmployePiece(models
.Model
):
455 """Documents relatifs à un employé.
458 employe
= models
.ForeignKey('Employe', db_column
='employe')
459 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
460 fichier
= models
.FileField(verbose_name
="Fichier",
461 upload_to
=employe_piece_dispatch
,
462 storage
=storage_prive
)
466 verbose_name
= u
"Employé pièce"
467 verbose_name_plural
= u
"Employé pièces"
469 def __unicode__(self
):
470 return u
'%s' % (self
.nom
)
472 class EmployeCommentaire(Commentaire
):
473 employe
= models
.ForeignKey('Employe', db_column
='employe',
477 verbose_name
= u
"Employé commentaire"
478 verbose_name_plural
= u
"Employé commentaires"
481 LIEN_PARENTE_CHOICES
= (
482 ('Conjoint', 'Conjoint'),
483 ('Conjointe', 'Conjointe'),
488 class AyantDroit(AUFMetadata
):
489 """Personne en relation avec un Employe.
492 nom
= models
.CharField(max_length
=255)
493 prenom
= models
.CharField(max_length
=255,
494 verbose_name
= u
"Prénom",)
495 nom_affichage
= models
.CharField(max_length
=255,
496 verbose_name
= u
"Nom d'affichage",
497 null
=True, blank
=True)
498 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
499 db_column
='nationalite',
500 related_name
='ayantdroits_nationalite',
501 verbose_name
= u
"Nationalité",
502 null
=True, blank
=True)
503 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
504 help_text
=HELP_TEXT_DATE
,
505 validators
=[validate_date_passee
],
506 null
=True, blank
=True)
507 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
510 employe
= models
.ForeignKey('Employe', db_column
='employe',
511 related_name
='ayantdroits',
512 verbose_name
= u
"Employé")
513 lien_parente
= models
.CharField(max_length
=10,
514 choices
=LIEN_PARENTE_CHOICES
,
515 verbose_name
= u
"Lien de parenté",
516 null
=True, blank
=True)
520 verbose_name
= u
"Ayant droit"
521 verbose_name_plural
= u
"Ayants droit"
523 def __unicode__(self
):
524 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
526 prefix_implantation
= "employe__dossiers__poste__implantation__region"
527 def get_regions(self
):
529 for d
in self
.employe
.dossiers
.all():
530 regions
.append(d
.poste
.implantation
.region
)
534 class AyantDroitCommentaire(Commentaire
):
535 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
541 STATUT_RESIDENCE_CHOICES
= (
543 ('expat', 'Expatrié'),
546 COMPTE_COMPTA_CHOICES
= (
552 class Dossier_(AUFMetadata
):
553 """Le Dossier regroupe les informations relatives à l'occupation
554 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
557 Plusieurs Contrats peuvent être associés au Dossier.
558 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
559 lequel aucun Dossier n'existe est un poste vacant.
562 objects
= DossierManager()
565 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
566 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
567 db_column
='organisme_bstg',
569 verbose_name
= u
"Organisme",
570 help_text
="Si détaché (DET) ou \
571 mis à disposition (MAD), \
572 préciser l'organisme.",
573 null
=True, blank
=True)
576 remplacement
= models
.BooleanField(default
=False)
577 remplacement_de
= models
.ForeignKey('self', related_name
='+',
578 help_text
=u
"Taper le nom de l'employé",
579 null
=True, blank
=True)
580 statut_residence
= models
.CharField(max_length
=10, default
='local',
581 verbose_name
= u
"Statut", null
=True,
582 choices
=STATUT_RESIDENCE_CHOICES
)
585 classement
= models
.ForeignKey('Classement', db_column
='classement',
587 null
=True, blank
=True)
588 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
590 default
=REGIME_TRAVAIL_DEFAULT
,
591 verbose_name
= u
"Régime de travail",
592 help_text
="% du temps complet")
593 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
594 decimal_places
=2, null
=True,
595 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
596 verbose_name
= u
"Nb. heures par semaine")
598 # Occupation du Poste par cet Employe (anciennement "mandat")
599 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
601 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
603 null
=True, blank
=True)
610 ordering
= ['employe__nom', ]
611 verbose_name
= u
"Dossier"
612 verbose_name_plural
= "Dossiers"
614 def salaire_theorique(self
):
615 annee
= date
.today().year
616 coeff
= self
.classement
.coefficient
617 implantation
= self
.poste
.implantation
618 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
620 montant
= coeff
* point
.valeur
621 devise
= point
.devise
622 return {'montant':montant
, 'devise':devise
}
624 def __unicode__(self
):
625 poste
= self
.poste
.nom
626 if self
.employe
.genre
== 'F':
627 poste
= self
.poste
.nom_feminin
628 return u
'%s - %s' % (self
.employe
, poste
)
630 prefix_implantation
= "poste__implantation__region"
631 def get_regions(self
):
632 return [self
.poste
.implantation
.region
]
635 def remunerations(self
):
636 return self
.rh_remunerations
.all().order_by('date_debut')
638 def remunerations_en_cours(self
):
639 return self
.rh_remunerations
.all().filter(date_fin__exact
=None).order_by('date_debut')
641 def get_salaire(self
):
643 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
648 class Dossier(Dossier_
):
649 __doc__
= Dossier_
.__doc__
650 poste
= models
.ForeignKey('%s.Poste' % app_context(),
652 related_name
='%(app_label)s_dossiers',
653 help_text
=u
"Taper le nom du poste ou du type de poste",
655 employe
= models
.ForeignKey('Employe', db_column
='employe',
656 help_text
=u
"Taper le nom de l'employé",
657 related_name
='%(app_label)s_dossiers',
658 verbose_name
=u
"Employé")
661 class DossierPiece_(models
.Model
):
662 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
663 Ex.: Lettre de motivation.
665 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_dossierpieces')
666 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
667 fichier
= models
.FileField(verbose_name
= u
"Fichier",
668 upload_to
=dossier_piece_dispatch
,
669 storage
=storage_prive
)
675 def __unicode__(self
):
676 return u
'%s' % (self
.nom
)
678 class DossierPiece(DossierPiece_
):
681 class DossierCommentaire_(Commentaire
):
682 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
686 class DossierCommentaire(DossierCommentaire_
):
689 class DossierComparaison_(models
.Model
):
691 Photo d'une comparaison salariale au moment de l'embauche.
693 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
694 objects
= DossierComparaisonManager()
696 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
697 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
698 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
699 montant
= models
.IntegerField(null
=True)
700 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
705 def taux_devise(self
):
706 if self
.devise
is None:
708 annee
= self
.dossier
.contrat_date_debut
.year
709 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise
, annee
=annee
)]
712 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
))
716 def montant_euros(self
):
717 taux
= self
.taux_devise()
720 return round(float(self
.montant
) * float(taux
), 2)
722 class DossierComparaison(DossierComparaison_
):
727 class RemunerationMixin(AUFMetadata
):
728 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
730 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
732 verbose_name
= u
"Type de rémunération")
733 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
734 db_column
='type_revalorisation',
736 verbose_name
= u
"Type de revalorisation",
737 null
=True, blank
=True)
738 montant
= models
.DecimalField(null
=True, blank
=True,
739 default
=0, max_digits
=12, decimal_places
=2)
740 # Annuel (12 mois, 52 semaines, 364 jours?)
741 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
742 # commentaire = precision
743 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
744 # date_debut = anciennement date_effectif
745 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
746 null
=True, blank
=True)
747 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
748 null
=True, blank
=True)
752 ordering
= ['type__nom', '-date_fin']
754 def __unicode__(self
):
755 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
757 class Remuneration_(RemunerationMixin
):
758 """Structure de rémunération (données budgétaires) en situation normale
759 pour un Dossier. Si un Evenement existe, utiliser la structure de
760 rémunération EvenementRemuneration de cet événement.
763 def montant_mois(self
):
764 return round(self
.montant
/ 12, 2)
766 def taux_devise(self
):
767 if self
.devise
.code
== "EUR":
770 annee
= datetime
.datetime
.now().year
771 if self
.dossier
.poste
.date_debut
is not None:
772 annee
= self
.dossier
.poste
.date_debut
.year
773 if self
.dossier
.date_debut
is not None:
774 annee
= self
.dossier
.date_debut
.year
775 if self
.date_debut
is not None:
776 annee
= self
.date_debut
.year
778 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=self
.devise_id
, annee
=annee
)]
781 raise Exception(u
"Le taux de la devise %s n'a pas n'existe pas pour %s ou il existe plusieurs taux pour la même année %s (%s)" % (self
.devise
.code
, annee
, taux
, self
.dossier
))
785 def montant_euro(self
):
786 return round(float(self
.montant
) * float(self
.taux_devise()), 2)
788 def montant_euro_mois(self
):
789 return round(self
.montant_euro() / 12, 2)
791 def __unicode__(self
):
793 devise
= self
.devise
.code
796 return "%s %s" % (self
.montant
, devise
)
800 verbose_name
= u
"Rémunération"
801 verbose_name_plural
= u
"Rémunérations"
804 class Remuneration(Remuneration_
):
810 class ContratManager(NoDeleteManager
):
811 def get_query_set(self
):
812 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
815 class Contrat_(AUFMetadata
):
816 """Document juridique qui encadre la relation de travail d'un Employe
817 pour un Poste particulier. Pour un Dossier (qui documente cette
818 relation de travail) plusieurs contrats peuvent être associés.
820 objects
= ContratManager()
821 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
822 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
824 verbose_name
= u
"type de contrat")
825 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
826 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
827 null
=True, blank
=True)
828 fichier
= models
.FileField(verbose_name
= u
"Fichier",
829 upload_to
=contrat_dispatch
,
830 storage
=storage_prive
,
831 null
=True, blank
=True)
835 ordering
= ['dossier__employe__nom']
836 verbose_name
= u
"Contrat"
837 verbose_name_plural
= u
"Contrats"
839 def __unicode__(self
):
840 return u
'%s - %s' % (self
.dossier
, self
.id)
842 class Contrat(Contrat_
):
848 #class Evenement_(AUFMetadata):
849 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
850 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
851 # (ex.: la Remuneration).
853 # Ex.: congé de maternité, maladie...
855 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
856 # différent et une rémunération en conséquence. On souhaite toutefois
857 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
858 # du retour à la normale.
860 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
862 # nom = models.CharField(max_length=255)
863 # date_debut = models.DateField(verbose_name = u"Date de début")
864 # date_fin = models.DateField(verbose_name = u"Date de fin",
865 # null=True, blank=True)
870 # verbose_name = u"Évènement"
871 # verbose_name_plural = u"Évènements"
873 # def __unicode__(self):
874 # return u'%s' % (self.nom)
877 #class Evenement(Evenement_):
878 # __doc__ = Evenement_.__doc__
881 #class EvenementRemuneration_(RemunerationMixin):
882 # """Structure de rémunération liée à un Evenement qui remplace
883 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
886 # evenement = models.ForeignKey("Evenement", db_column='evenement',
888 # verbose_name = u"Évènement")
889 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
890 # # de l'Evenement associé
894 # ordering = ['evenement', 'type__nom', '-date_fin']
895 # verbose_name = u"Évènement - rémunération"
896 # verbose_name_plural = u"Évènements - rémunérations"
899 #class EvenementRemuneration(EvenementRemuneration_):
900 # __doc__ = EvenementRemuneration_.__doc__
906 #class EvenementRemuneration(EvenementRemuneration_):
907 # __doc__ = EvenementRemuneration_.__doc__
908 # TODO? class ContratPiece(models.Model):
913 class FamilleEmploi(AUFMetadata
):
914 """Catégorie utilisée dans la gestion des Postes.
915 Catégorie supérieure à TypePoste.
917 nom
= models
.CharField(max_length
=255)
921 verbose_name
= u
"Famille d'emploi"
922 verbose_name_plural
= u
"Familles d'emploi"
924 def __unicode__(self
):
925 return u
'%s' % (self
.nom
)
927 class TypePoste(AUFMetadata
):
928 """Catégorie de Poste.
930 nom
= models
.CharField(max_length
=255)
931 nom_feminin
= models
.CharField(max_length
=255,
932 verbose_name
= u
"Nom féminin")
934 is_responsable
= models
.BooleanField(default
=False,
935 verbose_name
= u
"Poste de responsabilité")
936 famille_emploi
= models
.ForeignKey('FamilleEmploi',
937 db_column
='famille_emploi',
939 verbose_name
= u
"famille d'emploi")
943 verbose_name
= u
"Type de poste"
944 verbose_name_plural
= u
"Types de poste"
946 def __unicode__(self
):
947 return u
'%s' % (self
.nom
)
950 TYPE_PAIEMENT_CHOICES
= (
951 (u
'Régulier', u
'Régulier'),
952 (u
'Ponctuel', u
'Ponctuel'),
955 NATURE_REMUNERATION_CHOICES
= (
956 (u
'Accessoire', u
'Accessoire'),
957 (u
'Charges', u
'Charges'),
958 (u
'Indemnité', u
'Indemnité'),
959 (u
'RAS', u
'Rémunération autre source'),
960 (u
'Traitement', u
'Traitement'),
963 class TypeRemuneration(AUFMetadata
):
964 """Catégorie de Remuneration.
966 nom
= models
.CharField(max_length
=255)
967 type_paiement
= models
.CharField(max_length
=30,
968 choices
=TYPE_PAIEMENT_CHOICES
,
969 verbose_name
= u
"Type de paiement")
970 nature_remuneration
= models
.CharField(max_length
=30,
971 choices
=NATURE_REMUNERATION_CHOICES
,
972 verbose_name
= u
"Nature de la rémunération")
976 verbose_name
= u
"Type de rémunération"
977 verbose_name_plural
= u
"Types de rémunération"
979 def __unicode__(self
):
980 return u
'%s' % (self
.nom
)
982 class TypeRevalorisation(AUFMetadata
):
983 """Justification du changement de la Remuneration.
984 (Actuellement utilisé dans aucun traitement informatique.)
986 nom
= models
.CharField(max_length
=255)
990 verbose_name
= u
"Type de revalorisation"
991 verbose_name_plural
= u
"Types de revalorisation"
993 def __unicode__(self
):
994 return u
'%s' % (self
.nom
)
996 class Service(AUFMetadata
):
997 """Unité administrative où les Postes sont rattachés.
999 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1000 nom
= models
.CharField(max_length
=255)
1004 verbose_name
= u
"Service"
1005 verbose_name_plural
= u
"Services"
1007 def __unicode__(self
):
1009 archive
= u
"(archivé)"
1012 return u
'%s %s' % (self
.nom
, archive
)
1015 TYPE_ORGANISME_CHOICES
= (
1016 ('MAD', 'Mise à disposition'),
1017 ('DET', 'Détachement'),
1020 class OrganismeBstg(AUFMetadata
):
1021 """Organisation d'où provient un Employe mis à disposition (MAD) de
1022 ou détaché (DET) à l'AUF à titre gratuit.
1024 (BSTG = bien et service à titre gratuit.)
1026 nom
= models
.CharField(max_length
=255)
1027 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1028 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1030 related_name
='organismes_bstg',
1031 null
=True, blank
=True)
1034 ordering
= ['type', 'nom']
1035 verbose_name
= u
"Organisme BSTG"
1036 verbose_name_plural
= u
"Organismes BSTG"
1038 def __unicode__(self
):
1039 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1041 prefix_implantation
= "pays__region"
1042 def get_regions(self
):
1043 return [self
.pays
.region
]
1046 class Statut(AUFMetadata
):
1047 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1050 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.")
1051 nom
= models
.CharField(max_length
=255)
1055 verbose_name
= u
"Statut d'employé"
1056 verbose_name_plural
= u
"Statuts d'employé"
1058 def __unicode__(self
):
1059 return u
'%s : %s' % (self
.code
, self
.nom
)
1062 TYPE_CLASSEMENT_CHOICES
= (
1063 ('S', 'S -Soutien'),
1064 ('T', 'T - Technicien'),
1065 ('P', 'P - Professionel'),
1067 ('D', 'D - Direction'),
1068 ('SO', 'SO - Sans objet [expatriés]'),
1069 ('HG', 'HG - Hors grille [direction]'),
1072 class ClassementManager(models
.Manager
):
1074 Ordonner les spcéfiquement les classements.
1076 def get_query_set(self
):
1077 qs
= super(self
.__class__
, self
).get_query_set()
1078 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1079 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1083 class Classement_(AUFMetadata
):
1084 """Éléments de classement de la
1085 "Grille générique de classement hiérarchique".
1087 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1088 classement dans la grille. Le classement donne le coefficient utilisé dans:
1090 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1092 objects
= ClassementManager()
1095 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1096 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1097 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1098 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1101 # annee # au lieu de date_debut et date_fin
1102 commentaire
= models
.TextField(null
=True, blank
=True)
1106 ordering
= ['type','echelon','degre','coefficient']
1107 verbose_name
= u
"Classement"
1108 verbose_name_plural
= u
"Classements"
1110 def __unicode__(self
):
1111 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1113 class Classement(Classement_
):
1114 __doc__
= Classement_
.__doc__
1117 class TauxChange_(AUFMetadata
):
1118 """Taux de change de la devise vers l'euro (EUR)
1119 pour chaque année budgétaire.
1122 devise
= models
.ForeignKey('Devise', db_column
='devise')
1123 annee
= models
.IntegerField(verbose_name
= u
"Année")
1124 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1128 ordering
= ['-annee', 'devise__code']
1129 verbose_name
= u
"Taux de change"
1130 verbose_name_plural
= u
"Taux de change"
1132 def __unicode__(self
):
1133 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1136 class TauxChange(TauxChange_
):
1137 __doc__
= TauxChange_
.__doc__
1139 class ValeurPointManager(NoDeleteManager
):
1141 def get_query_set(self
):
1142 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1145 class ValeurPoint_(AUFMetadata
):
1146 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1147 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1148 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1150 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1153 actuelles
= ValeurPointManager()
1155 valeur
= models
.FloatField(null
=True)
1156 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1157 implantation
= models
.ForeignKey(ref
.Implantation
,
1158 db_column
='implantation',
1159 related_name
='%(app_label)s_valeur_point')
1161 annee
= models
.IntegerField()
1164 ordering
= ['-annee', 'implantation__nom']
1166 verbose_name
= u
"Valeur du point"
1167 verbose_name_plural
= u
"Valeurs du point"
1169 def __unicode__(self
):
1170 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1173 class ValeurPoint(ValeurPoint_
):
1174 __doc__
= ValeurPoint_
.__doc__
1178 class Devise(AUFMetadata
):
1179 """Devise monétaire.
1182 objects
= DeviseManager()
1184 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1185 code
= models
.CharField(max_length
=10, unique
=True)
1186 nom
= models
.CharField(max_length
=255)
1190 verbose_name
= u
"Devise"
1191 verbose_name_plural
= u
"Devises"
1193 def __unicode__(self
):
1194 return u
'%s - %s' % (self
.code
, self
.nom
)
1196 class TypeContrat(AUFMetadata
):
1199 nom
= models
.CharField(max_length
=255)
1200 nom_long
= models
.CharField(max_length
=255)
1204 verbose_name
= u
"Type de contrat"
1205 verbose_name_plural
= u
"Types de contrat"
1207 def __unicode__(self
):
1208 return u
'%s' % (self
.nom
)
1213 class ResponsableImplantation(AUFMetadata
):
1214 """Le responsable d'une implantation.
1215 Anciennement géré sur le Dossier du responsable.
1217 employe
= models
.ForeignKey('Employe', db_column
='employe',
1219 null
=True, blank
=True)
1220 implantation
= models
.ForeignKey(ref
.Implantation
,
1221 db_column
='implantation', related_name
='+',
1224 def __unicode__(self
):
1225 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1228 ordering
= ['implantation__nom']
1229 verbose_name
= "Responsable d'implantation"
1230 verbose_name_plural
= "Responsables d'implantation"