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
.db
.models
import Q
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
, \
18 PosteComparaisonManager
, DeviseManager
, ServiceManager
, TypeRemunerationManager
19 from change_list
import RechercheTemporelle
, KEY_STATUT
, STATUT_ACTIF
, \
20 STATUT_INACTIF
, STATUT_FUTUR
23 # Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage.
24 # Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK
27 models_stack
= [s
[1].split('/')[-2] for s
in inspect
.stack() if s
[1].endswith('models.py')]
28 return models_stack
[-1]
32 HELP_TEXT_DATE
= "format: jj-mm-aaaa"
33 REGIME_TRAVAIL_DEFAULT
= Decimal('100.00')
34 REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
= Decimal('35.00')
35 REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
= "Saisir le nombre d'heure de travail à temps complet (100%), sans tenir compte du régime de travail"
38 storage_prive
= FileSystemStorage(settings
.PRIVE_MEDIA_ROOT
,
39 base_url
=settings
.PRIVE_MEDIA_URL
)
41 def poste_piece_dispatch(instance
, filename
):
42 path
= "%s/poste/%s/%s" % (instance
._meta
.app_label
, instance
.poste_id
, filename
)
45 def dossier_piece_dispatch(instance
, filename
):
46 path
= "%s/dossier/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
49 def employe_piece_dispatch(instance
, filename
):
50 path
= "%s/employe/%s/%s" % (instance
._meta
.app_label
, instance
.employe_id
, filename
)
53 def contrat_dispatch(instance
, filename
):
54 path
= "%s/contrat/%s/%s" % (instance
._meta
.app_label
, instance
.dossier_id
, filename
)
58 class DevisableMixin(object):
60 def get_annee_pour_taux_devise(self
):
61 return datetime
.datetime
.now().year
64 def taux_devise(self
, devise
=None):
70 if devise
.code
== "EUR":
73 annee
= self
.get_annee_pour_taux_devise()
74 taux
= [tc
.taux
for tc
in TauxChange
.objects
.filter(devise
=devise
, annee
=annee
)]
78 raise Exception(u
"Pas de taux pour %s en %s" % (devise
.code
, annee
))
81 raise Exception(u
"Il existe plusieurs taux de %s en %s" %
86 def montant_euros(self
):
88 taux
= self
.taux_devise()
93 return int(round(float(self
.montant
) * float(taux
), 2))
96 class Commentaire(AUFMetadata
):
97 texte
= models
.TextField()
98 owner
= models
.ForeignKey('auth.User', db_column
='owner', related_name
='+', verbose_name
=u
"Commentaire de")
102 ordering
= ['-date_creation']
104 def __unicode__(self
):
105 return u
'%s' % (self
.texte
)
110 POSTE_APPEL_CHOICES
= (
111 ('interne', 'Interne'),
112 ('externe', 'Externe'),
115 class Poste_(AUFMetadata
):
116 """Un Poste est un emploi (job) à combler dans une implantation.
117 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
118 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
121 objects
= PosteManager()
124 nom
= models
.CharField(max_length
=255,
125 verbose_name
= u
"Titre du poste", )
126 nom_feminin
= models
.CharField(max_length
=255,
127 verbose_name
= u
"Titre du poste (au féminin)",
129 implantation
= models
.ForeignKey(ref
.Implantation
, help_text
=u
"Taper le nom de l'implantation ou sa région",
130 db_column
='implantation', related_name
='+')
131 type_poste
= models
.ForeignKey('TypePoste', db_column
='type_poste', help_text
=u
"Taper le nom du type de poste",
134 verbose_name
=u
"type de poste")
135 service
= models
.ForeignKey('Service', db_column
='service',
137 verbose_name
= u
"direction/service/pôle support",
139 responsable
= models
.ForeignKey('Poste', db_column
='responsable',
142 help_text
=u
"Taper le nom du poste ou du type de poste",
143 verbose_name
= u
"Poste du responsable", )
146 regime_travail
= models
.DecimalField(max_digits
=12, decimal_places
=2,
147 default
=REGIME_TRAVAIL_DEFAULT
, null
=True,
148 verbose_name
= u
"Temps de travail",
149 help_text
="% du temps complet")
150 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
151 decimal_places
=2, null
=True,
152 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
153 verbose_name
= u
"Nb. heures par semaine",
154 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
)
157 local
= models
.NullBooleanField(verbose_name
= u
"Local", default
=True,
158 null
=True, blank
=True)
159 expatrie
= models
.NullBooleanField(verbose_name
= u
"Expatrié", default
=False,
160 null
=True, blank
=True)
161 mise_a_disposition
= models
.NullBooleanField(
162 verbose_name
= u
"Mise à disposition",
163 null
=True, default
=False)
164 appel
= models
.CharField(max_length
=10, null
=True,
165 verbose_name
= u
"Appel à candidature",
166 choices
=POSTE_APPEL_CHOICES
,
170 classement_min
= models
.ForeignKey('Classement',
171 db_column
='classement_min', related_name
='+',
172 null
=True, blank
=True)
173 classement_max
= models
.ForeignKey('Classement',
174 db_column
='classement_max', related_name
='+',
175 null
=True, blank
=True)
176 valeur_point_min
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
177 db_column
='valeur_point_min', related_name
='+',
178 null
=True, blank
=True)
179 valeur_point_max
= models
.ForeignKey('ValeurPoint', help_text
=u
"Taper le code ou le nom de l'implantation",
180 db_column
='valeur_point_max', related_name
='+',
181 null
=True, blank
=True)
182 devise_min
= models
.ForeignKey('Devise', db_column
='devise_min', null
=True,
184 devise_max
= models
.ForeignKey('Devise', db_column
='devise_max', null
=True,
186 salaire_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
187 null
=True, default
=0)
188 salaire_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
189 null
=True, default
=0)
190 indemn_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
191 null
=True, default
=0)
192 indemn_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
193 null
=True, default
=0)
194 autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
195 null
=True, default
=0)
196 autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
197 null
=True, default
=0)
199 # Comparatifs de rémunération
200 devise_comparaison
= models
.ForeignKey('Devise', null
=True, blank
=True,
201 db_column
='devise_comparaison',
203 comp_locale_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
204 null
=True, blank
=True)
205 comp_locale_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
206 null
=True, blank
=True)
207 comp_universite_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
208 null
=True, blank
=True)
209 comp_universite_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
210 null
=True, blank
=True)
211 comp_fonctionpub_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
212 null
=True, blank
=True)
213 comp_fonctionpub_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
214 null
=True, blank
=True)
215 comp_ong_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
216 null
=True, blank
=True)
217 comp_ong_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
218 null
=True, blank
=True)
219 comp_autre_min
= models
.DecimalField(max_digits
=12, decimal_places
=2,
220 null
=True, blank
=True)
221 comp_autre_max
= models
.DecimalField(max_digits
=12, decimal_places
=2,
222 null
=True, blank
=True)
225 justification
= models
.TextField(null
=True, blank
=True)
228 date_debut
= models
.DateField(verbose_name
=u
"Date de début", help_text
=HELP_TEXT_DATE
,
229 null
=True, blank
=True)
230 date_fin
= models
.DateField(verbose_name
=u
"Date de fin", help_text
=HELP_TEXT_DATE
,
231 null
=True, blank
=True)
235 ordering
= ['implantation__nom', 'nom']
236 verbose_name
= u
"Poste"
237 verbose_name_plural
= u
"Postes"
240 def __unicode__(self
):
241 representation
= u
'%s - %s [%s]' % (self
.implantation
, self
.nom
,
243 return representation
246 prefix_implantation
= "implantation__region"
247 def get_regions(self
):
248 return [self
.implantation
.region
]
252 __doc__
= Poste_
.__doc__
254 # meta dématérialisation : pour permettre le filtrage
255 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
259 if self
.occupe_par():
263 def occupe_par(self
):
264 """Retourne la liste d'employé occupant ce poste.
265 Généralement, retourne une liste d'un élément.
266 Si poste inoccupé, retourne liste vide.
267 UTILISE pour mettre a jour le flag vacant
269 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
272 POSTE_FINANCEMENT_CHOICES
= (
273 ('A', 'A - Frais de personnel'),
274 ('B', 'B - Projet(s)-Titre(s)'),
279 class PosteFinancement_(models
.Model
):
280 """Pour un Poste, structure d'informations décrivant comment on prévoit
283 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
284 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
285 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
286 help_text
="ex.: 33.33 % (décimale avec point)")
287 commentaire
= models
.TextField(
288 help_text
="Spécifiez la source de financement.")
294 def __unicode__(self
):
295 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
298 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
301 class PosteFinancement(PosteFinancement_
):
305 class PostePiece_(models
.Model
):
306 """Documents relatifs au Poste.
307 Ex.: Description de poste
309 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
310 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
311 fichier
= models
.FileField(verbose_name
= u
"Fichier",
312 upload_to
=poste_piece_dispatch
,
313 storage
=storage_prive
)
319 def __unicode__(self
):
320 return u
'%s' % (self
.nom
)
322 class PostePiece(PostePiece_
):
325 class PosteComparaison_(AUFMetadata
, DevisableMixin
):
327 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
329 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
330 objects
= PosteComparaisonManager()
332 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
333 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
334 montant
= models
.IntegerField(null
=True)
335 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
341 def __unicode__(self
):
344 class PosteComparaison(PosteComparaison_
):
345 objects
= NoDeleteManager()
347 class PosteCommentaire_(Commentaire
):
348 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
353 class PosteCommentaire(PosteCommentaire_
):
358 class Employe(AUFMetadata
):
359 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
360 Dossiers qu'il occupe ou a occupé de Postes.
362 Cette classe aurait pu avantageusement s'appeler Personne car la notion
363 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
366 nom
= models
.CharField(max_length
=255)
367 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
368 nom_affichage
= models
.CharField(max_length
=255,
369 verbose_name
= u
"Nom d'affichage",
370 null
=True, blank
=True)
371 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
372 db_column
='nationalite',
373 related_name
='employes_nationalite',
374 verbose_name
= u
"Nationalité",
375 blank
=True, null
=True)
376 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
377 help_text
=HELP_TEXT_DATE
,
378 validators
=[validate_date_passee
],
379 null
=True, blank
=True)
380 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
383 situation_famille
= models
.CharField(max_length
=1,
384 choices
=SITUATION_CHOICES
,
385 verbose_name
= u
"Situation familiale",
386 null
=True, blank
=True)
387 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
388 help_text
=HELP_TEXT_DATE
,
389 null
=True, blank
=True)
392 tel_domicile
= models
.CharField(max_length
=255,
393 verbose_name
= u
"Tél. domicile",
394 null
=True, blank
=True)
395 tel_cellulaire
= models
.CharField(max_length
=255,
396 verbose_name
= u
"Tél. cellulaire",
397 null
=True, blank
=True)
398 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
399 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
400 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
401 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
402 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
403 related_name
='employes',
404 null
=True, blank
=True)
406 # meta dématérialisation : pour permettre le filtrage
407 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
410 ordering
= ['nom','prenom']
411 verbose_name
= u
"Employé"
412 verbose_name_plural
= u
"Employés"
414 def __unicode__(self
):
415 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
419 if self
.genre
.upper() == u
'M':
421 elif self
.genre
.upper() == u
'F':
426 """Retourne l'URL du service retournant la photo de l'Employe.
427 Équivalent reverse url 'rh_photo' avec id en param.
429 from django
.core
.urlresolvers
import reverse
430 return reverse('rh_photo', kwargs
={'id':self
.id})
432 def dossiers_passes(self
):
433 params
= {KEY_STATUT
: STATUT_INACTIF
, }
434 search
= RechercheTemporelle(params
, self
.__class__
)
435 search
.purge_params(params
)
436 q
= search
.get_q_temporel(self
.rh_dossiers
)
437 return self
.rh_dossiers
.filter(q
)
439 def dossiers_futurs(self
):
440 params
= {KEY_STATUT
: STATUT_FUTUR
, }
441 search
= RechercheTemporelle(params
, self
.__class__
)
442 search
.purge_params(params
)
443 q
= search
.get_q_temporel(self
.rh_dossiers
)
444 return self
.rh_dossiers
.filter(q
)
446 def dossiers_encours(self
):
447 params
= {KEY_STATUT
: STATUT_ACTIF
, }
448 search
= RechercheTemporelle(params
, self
.__class__
)
449 search
.purge_params(params
)
450 q
= search
.get_q_temporel(self
.rh_dossiers
)
451 return self
.rh_dossiers
.filter(q
)
453 def postes_encours(self
):
454 postes_encours
= set()
455 for d
in self
.dossiers_encours():
456 postes_encours
.add(d
.poste
)
457 return postes_encours
459 def poste_principal(self
):
461 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
463 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
465 poste
= Poste
.objects
.none()
467 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
472 prefix_implantation
= "rh_dossiers__poste__implantation__region"
473 def get_regions(self
):
475 for d
in self
.dossiers
.all():
476 regions
.append(d
.poste
.implantation
.region
)
480 class EmployePiece(models
.Model
):
481 """Documents relatifs à un employé.
484 employe
= models
.ForeignKey('Employe', db_column
='employe')
485 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
486 fichier
= models
.FileField(verbose_name
="Fichier",
487 upload_to
=employe_piece_dispatch
,
488 storage
=storage_prive
)
492 verbose_name
= u
"Employé pièce"
493 verbose_name_plural
= u
"Employé pièces"
495 def __unicode__(self
):
496 return u
'%s' % (self
.nom
)
498 class EmployeCommentaire(Commentaire
):
499 employe
= models
.ForeignKey('Employe', db_column
='employe',
503 verbose_name
= u
"Employé commentaire"
504 verbose_name_plural
= u
"Employé commentaires"
507 LIEN_PARENTE_CHOICES
= (
508 ('Conjoint', 'Conjoint'),
509 ('Conjointe', 'Conjointe'),
514 class AyantDroit(AUFMetadata
):
515 """Personne en relation avec un Employe.
518 nom
= models
.CharField(max_length
=255)
519 prenom
= models
.CharField(max_length
=255,
520 verbose_name
= u
"Prénom",)
521 nom_affichage
= models
.CharField(max_length
=255,
522 verbose_name
= u
"Nom d'affichage",
523 null
=True, blank
=True)
524 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
525 db_column
='nationalite',
526 related_name
='ayantdroits_nationalite',
527 verbose_name
= u
"Nationalité",
528 null
=True, blank
=True)
529 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
530 help_text
=HELP_TEXT_DATE
,
531 validators
=[validate_date_passee
],
532 null
=True, blank
=True)
533 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
536 employe
= models
.ForeignKey('Employe', db_column
='employe',
537 related_name
='ayantdroits',
538 verbose_name
= u
"Employé")
539 lien_parente
= models
.CharField(max_length
=10,
540 choices
=LIEN_PARENTE_CHOICES
,
541 verbose_name
= u
"Lien de parenté",
542 null
=True, blank
=True)
546 verbose_name
= u
"Ayant droit"
547 verbose_name_plural
= u
"Ayants droit"
549 def __unicode__(self
):
550 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
552 prefix_implantation
= "employe__dossiers__poste__implantation__region"
553 def get_regions(self
):
555 for d
in self
.employe
.dossiers
.all():
556 regions
.append(d
.poste
.implantation
.region
)
560 class AyantDroitCommentaire(Commentaire
):
561 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
567 STATUT_RESIDENCE_CHOICES
= (
569 ('expat', 'Expatrié'),
572 COMPTE_COMPTA_CHOICES
= (
578 class Dossier_(AUFMetadata
, DevisableMixin
):
579 """Le Dossier regroupe les informations relatives à l'occupation
580 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
583 Plusieurs Contrats peuvent être associés au Dossier.
584 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
585 lequel aucun Dossier n'existe est un poste vacant.
588 objects
= DossierManager()
591 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
592 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
593 db_column
='organisme_bstg',
595 verbose_name
= u
"Organisme",
596 help_text
="Si détaché (DET) ou \
597 mis à disposition (MAD), \
598 préciser l'organisme.",
599 null
=True, blank
=True)
602 remplacement
= models
.BooleanField(default
=False)
603 remplacement_de
= models
.ForeignKey('self', related_name
='+',
604 help_text
=u
"Taper le nom de l'employé",
605 null
=True, blank
=True)
606 statut_residence
= models
.CharField(max_length
=10, default
='local',
607 verbose_name
= u
"Statut", null
=True,
608 choices
=STATUT_RESIDENCE_CHOICES
)
611 classement
= models
.ForeignKey('Classement', db_column
='classement',
613 null
=True, blank
=True)
614 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
616 default
=REGIME_TRAVAIL_DEFAULT
,
617 verbose_name
= u
"Régime de travail",
618 help_text
="% du temps complet")
619 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
620 decimal_places
=2, null
=True,
621 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
622 verbose_name
=u
"Nb. heures par semaine",
623 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
)
625 # Occupation du Poste par cet Employe (anciennement "mandat")
626 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
628 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
630 null
=True, blank
=True)
637 ordering
= ['employe__nom', ]
638 verbose_name
= u
"Dossier"
639 verbose_name_plural
= "Dossiers"
641 def salaire_theorique(self
):
642 annee
= date
.today().year
643 coeff
= self
.classement
.coefficient
644 implantation
= self
.poste
.implantation
645 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
647 montant
= coeff
* point
.valeur
648 devise
= point
.devise
649 return {'montant':montant
, 'devise':devise
}
651 def __unicode__(self
):
652 poste
= self
.poste
.nom
653 if self
.employe
.genre
== 'F':
654 poste
= self
.poste
.nom_feminin
655 return u
'%s - %s' % (self
.employe
, poste
)
657 prefix_implantation
= "poste__implantation__region"
658 def get_regions(self
):
659 return [self
.poste
.implantation
.region
]
662 def remunerations(self
):
663 key
= "%s_remunerations" % self
._meta
.app_label
664 remunerations
= getattr(self
, key
)
665 return remunerations
.all().order_by('-date_debut')
667 def remunerations_en_cours(self
):
668 q
= Q(date_fin__exact
=None) |
Q(date_fin__gt
=datetime
.date
.today())
669 return self
.remunerations().all().filter(q
).order_by('date_debut')
671 def get_salaire(self
):
673 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
677 def get_salaire_euros(self
):
678 tx
= self
.taux_devise()
679 return (float)(tx
) * (float)(self
.salaire
)
681 def get_remunerations_brutes(self
):
685 4 Indemnité d'expatriation
686 5 Indemnité pour frais
687 6 Indemnité de logement
688 7 Indemnité de fonction
689 8 Indemnité de responsabilité
690 9 Indemnité de transport
691 10 Indemnité compensatrice
692 11 Indemnité de subsistance
693 12 Indemnité différentielle
694 13 Prime d'installation
697 16 Indemnité de départ
698 18 Prime de 13ième mois
701 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
702 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
704 def get_charges_salariales(self
):
706 20 Charges salariales ?
709 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
711 def get_total_charges_salariales(self
):
713 for r
in self
.get_charges_salariales():
714 total
+= r
.montant_euros()
717 def get_charges_patronales(self
):
719 17 Charges patronales
722 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
724 def get_total_charges_patronales(self
):
726 for r
in self
.get_charges_patronales():
727 total
+= r
.montant_euros()
730 def get_salaire_brut(self
):
732 somme des rémuérations brutes
735 for r
in self
.get_remunerations_brutes():
736 total
+= r
.montant_euros()
739 def get_salaire_net(self
):
741 salaire brut - charges salariales
744 for r
in self
.get_charges_salariales():
745 total_charges
+= r
.montant_euros()
746 return self
.get_salaire_brut() - total_charges
748 def get_couts_auf(self
):
750 salaire net + charges patronales
753 for r
in self
.get_charges_patronales():
754 total_charges
+= r
.montant_euros()
755 return self
.get_salaire_net() + total_charges
757 def get_remunerations_tierces(self
):
761 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in (2, )]
763 def get_total_remunerations_tierces(self
):
765 for r
in self
.get_remunerations_tierces():
766 total
+= r
.montant_euros()
770 class Dossier(Dossier_
):
771 __doc__
= Dossier_
.__doc__
772 poste
= models
.ForeignKey('%s.Poste' % app_context(),
774 related_name
='%(app_label)s_dossiers',
775 help_text
=u
"Taper le nom du poste ou du type de poste",
777 employe
= models
.ForeignKey('Employe', db_column
='employe',
778 help_text
=u
"Taper le nom de l'employé",
779 related_name
='%(app_label)s_dossiers',
780 verbose_name
=u
"Employé")
781 principal
= models
.BooleanField(verbose_name
=u
"Principal?", default
=True,
782 help_text
=u
"Ce Dossier est pour le principal Poste occupé par l'Employé")
785 class DossierPiece_(models
.Model
):
786 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
787 Ex.: Lettre de motivation.
789 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_dossierpieces')
790 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
791 fichier
= models
.FileField(verbose_name
= u
"Fichier",
792 upload_to
=dossier_piece_dispatch
,
793 storage
=storage_prive
)
799 def __unicode__(self
):
800 return u
'%s' % (self
.nom
)
802 class DossierPiece(DossierPiece_
):
805 class DossierCommentaire_(Commentaire
):
806 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
810 class DossierCommentaire(DossierCommentaire_
):
813 class DossierComparaison_(models
.Model
, DevisableMixin
):
815 Photo d'une comparaison salariale au moment de l'embauche.
817 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
818 objects
= DossierComparaisonManager()
820 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
821 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
822 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
823 montant
= models
.IntegerField(null
=True)
824 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
829 def __unicode__(self
):
830 return "%s (%s)" % (self
.poste
, self
.personne
)
833 class DossierComparaison(DossierComparaison_
):
838 class RemunerationMixin(AUFMetadata
):
839 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
841 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
843 verbose_name
= u
"Type de rémunération")
844 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
845 db_column
='type_revalorisation',
847 verbose_name
= u
"Type de revalorisation",
848 null
=True, blank
=True)
849 montant
= models
.DecimalField(null
=True, blank
=True,
850 default
=0, max_digits
=12, decimal_places
=2)
851 # Annuel (12 mois, 52 semaines, 364 jours?)
852 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
853 # commentaire = precision
854 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
855 # date_debut = anciennement date_effectif
856 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
857 null
=True, blank
=True)
858 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
859 null
=True, blank
=True)
863 ordering
= ['type__nom', '-date_fin']
865 def __unicode__(self
):
866 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
868 class Remuneration_(RemunerationMixin
, DevisableMixin
):
869 """Structure de rémunération (données budgétaires) en situation normale
870 pour un Dossier. Si un Evenement existe, utiliser la structure de
871 rémunération EvenementRemuneration de cet événement.
874 def montant_mois(self
):
875 return round(self
.montant
/ 12, 2)
877 def montant_avec_regime(self
):
878 return round(self
.montant
* (self
.dossier
.regime_travail
/100), 2)
880 def montant_euro_mois(self
):
881 return round(self
.montant_euros() / 12, 2)
883 def __unicode__(self
):
885 devise
= self
.devise
.code
888 return "%s %s" % (self
.montant
, devise
)
892 verbose_name
= u
"Rémunération"
893 verbose_name_plural
= u
"Rémunérations"
896 class Remuneration(Remuneration_
):
902 class ContratManager(NoDeleteManager
):
903 def get_query_set(self
):
904 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
907 class Contrat_(AUFMetadata
):
908 """Document juridique qui encadre la relation de travail d'un Employe
909 pour un Poste particulier. Pour un Dossier (qui documente cette
910 relation de travail) plusieurs contrats peuvent être associés.
912 objects
= ContratManager()
913 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
914 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
916 verbose_name
= u
"type de contrat")
917 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
918 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
919 null
=True, blank
=True)
920 fichier
= models
.FileField(verbose_name
= u
"Fichier",
921 upload_to
=contrat_dispatch
,
922 storage
=storage_prive
,
923 null
=True, blank
=True)
927 ordering
= ['dossier__employe__nom']
928 verbose_name
= u
"Contrat"
929 verbose_name_plural
= u
"Contrats"
931 def __unicode__(self
):
932 return u
'%s - %s' % (self
.dossier
, self
.id)
934 class Contrat(Contrat_
):
940 #class Evenement_(AUFMetadata):
941 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
942 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
943 # (ex.: la Remuneration).
945 # Ex.: congé de maternité, maladie...
947 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
948 # différent et une rémunération en conséquence. On souhaite toutefois
949 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
950 # du retour à la normale.
952 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
954 # nom = models.CharField(max_length=255)
955 # date_debut = models.DateField(verbose_name = u"Date de début")
956 # date_fin = models.DateField(verbose_name = u"Date de fin",
957 # null=True, blank=True)
962 # verbose_name = u"Évènement"
963 # verbose_name_plural = u"Évènements"
965 # def __unicode__(self):
966 # return u'%s' % (self.nom)
969 #class Evenement(Evenement_):
970 # __doc__ = Evenement_.__doc__
973 #class EvenementRemuneration_(RemunerationMixin):
974 # """Structure de rémunération liée à un Evenement qui remplace
975 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
978 # evenement = models.ForeignKey("Evenement", db_column='evenement',
980 # verbose_name = u"Évènement")
981 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
982 # # de l'Evenement associé
986 # ordering = ['evenement', 'type__nom', '-date_fin']
987 # verbose_name = u"Évènement - rémunération"
988 # verbose_name_plural = u"Évènements - rémunérations"
991 #class EvenementRemuneration(EvenementRemuneration_):
992 # __doc__ = EvenementRemuneration_.__doc__
998 #class EvenementRemuneration(EvenementRemuneration_):
999 # __doc__ = EvenementRemuneration_.__doc__
1000 # TODO? class ContratPiece(models.Model):
1005 class FamilleEmploi(AUFMetadata
):
1006 """Catégorie utilisée dans la gestion des Postes.
1007 Catégorie supérieure à TypePoste.
1009 nom
= models
.CharField(max_length
=255)
1013 verbose_name
= u
"Famille d'emploi"
1014 verbose_name_plural
= u
"Familles d'emploi"
1016 def __unicode__(self
):
1017 return u
'%s' % (self
.nom
)
1019 class TypePoste(AUFMetadata
):
1020 """Catégorie de Poste.
1022 nom
= models
.CharField(max_length
=255)
1023 nom_feminin
= models
.CharField(max_length
=255,
1024 verbose_name
= u
"Nom féminin")
1026 is_responsable
= models
.BooleanField(default
=False,
1027 verbose_name
= u
"Poste de responsabilité")
1028 famille_emploi
= models
.ForeignKey('FamilleEmploi',
1029 db_column
='famille_emploi',
1031 verbose_name
= u
"famille d'emploi")
1035 verbose_name
= u
"Type de poste"
1036 verbose_name_plural
= u
"Types de poste"
1038 def __unicode__(self
):
1039 return u
'%s' % (self
.nom
)
1042 TYPE_PAIEMENT_CHOICES
= (
1043 (u
'Régulier', u
'Régulier'),
1044 (u
'Ponctuel', u
'Ponctuel'),
1047 NATURE_REMUNERATION_CHOICES
= (
1048 (u
'Accessoire', u
'Accessoire'),
1049 (u
'Charges', u
'Charges'),
1050 (u
'Indemnité', u
'Indemnité'),
1051 (u
'RAS', u
'Rémunération autre source'),
1052 (u
'Traitement', u
'Traitement'),
1055 class TypeRemuneration(AUFMetadata
):
1056 """Catégorie de Remuneration.
1058 objects
= TypeRemunerationManager()
1060 nom
= models
.CharField(max_length
=255)
1061 type_paiement
= models
.CharField(max_length
=30,
1062 choices
=TYPE_PAIEMENT_CHOICES
,
1063 verbose_name
= u
"Type de paiement")
1064 nature_remuneration
= models
.CharField(max_length
=30,
1065 choices
=NATURE_REMUNERATION_CHOICES
,
1066 verbose_name
= u
"Nature de la rémunération")
1067 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1071 verbose_name
= u
"Type de rémunération"
1072 verbose_name_plural
= u
"Types de rémunération"
1074 def __unicode__(self
):
1076 archive
= u
"(archivé)"
1079 return u
'%s %s' % (self
.nom
, archive
)
1081 class TypeRevalorisation(AUFMetadata
):
1082 """Justification du changement de la Remuneration.
1083 (Actuellement utilisé dans aucun traitement informatique.)
1085 nom
= models
.CharField(max_length
=255)
1089 verbose_name
= u
"Type de revalorisation"
1090 verbose_name_plural
= u
"Types de revalorisation"
1092 def __unicode__(self
):
1093 return u
'%s' % (self
.nom
)
1095 class Service(AUFMetadata
):
1096 """Unité administrative où les Postes sont rattachés.
1098 objects
= ServiceManager()
1100 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1101 nom
= models
.CharField(max_length
=255)
1105 verbose_name
= u
"Service"
1106 verbose_name_plural
= u
"Services"
1108 def __unicode__(self
):
1110 archive
= u
"(archivé)"
1113 return u
'%s %s' % (self
.nom
, archive
)
1116 TYPE_ORGANISME_CHOICES
= (
1117 ('MAD', 'Mise à disposition'),
1118 ('DET', 'Détachement'),
1121 class OrganismeBstg(AUFMetadata
):
1122 """Organisation d'où provient un Employe mis à disposition (MAD) de
1123 ou détaché (DET) à l'AUF à titre gratuit.
1125 (BSTG = bien et service à titre gratuit.)
1127 nom
= models
.CharField(max_length
=255)
1128 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1129 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1131 related_name
='organismes_bstg',
1132 null
=True, blank
=True)
1135 ordering
= ['type', 'nom']
1136 verbose_name
= u
"Organisme BSTG"
1137 verbose_name_plural
= u
"Organismes BSTG"
1139 def __unicode__(self
):
1140 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1142 prefix_implantation
= "pays__region"
1143 def get_regions(self
):
1144 return [self
.pays
.region
]
1147 class Statut(AUFMetadata
):
1148 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1151 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.")
1152 nom
= models
.CharField(max_length
=255)
1156 verbose_name
= u
"Statut d'employé"
1157 verbose_name_plural
= u
"Statuts d'employé"
1159 def __unicode__(self
):
1160 return u
'%s : %s' % (self
.code
, self
.nom
)
1163 TYPE_CLASSEMENT_CHOICES
= (
1164 ('S', 'S -Soutien'),
1165 ('T', 'T - Technicien'),
1166 ('P', 'P - Professionel'),
1168 ('D', 'D - Direction'),
1169 ('SO', 'SO - Sans objet [expatriés]'),
1170 ('HG', 'HG - Hors grille [direction]'),
1173 class ClassementManager(models
.Manager
):
1175 Ordonner les spcéfiquement les classements.
1177 def get_query_set(self
):
1178 qs
= super(self
.__class__
, self
).get_query_set()
1179 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1180 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1184 class Classement_(AUFMetadata
):
1185 """Éléments de classement de la
1186 "Grille générique de classement hiérarchique".
1188 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1189 classement dans la grille. Le classement donne le coefficient utilisé dans:
1191 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1193 objects
= ClassementManager()
1196 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1197 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1198 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1199 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1202 # annee # au lieu de date_debut et date_fin
1203 commentaire
= models
.TextField(null
=True, blank
=True)
1207 ordering
= ['type','echelon','degre','coefficient']
1208 verbose_name
= u
"Classement"
1209 verbose_name_plural
= u
"Classements"
1211 def __unicode__(self
):
1212 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1214 class Classement(Classement_
):
1215 __doc__
= Classement_
.__doc__
1218 class TauxChange_(AUFMetadata
):
1219 """Taux de change de la devise vers l'euro (EUR)
1220 pour chaque année budgétaire.
1223 devise
= models
.ForeignKey('Devise', db_column
='devise')
1224 annee
= models
.IntegerField(verbose_name
= u
"Année")
1225 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1229 ordering
= ['-annee', 'devise__code']
1230 verbose_name
= u
"Taux de change"
1231 verbose_name_plural
= u
"Taux de change"
1233 def __unicode__(self
):
1234 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1237 class TauxChange(TauxChange_
):
1238 __doc__
= TauxChange_
.__doc__
1240 class ValeurPointManager(NoDeleteManager
):
1242 def get_query_set(self
):
1243 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1246 class ValeurPoint_(AUFMetadata
):
1247 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1248 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1249 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1251 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1254 actuelles
= ValeurPointManager()
1256 valeur
= models
.FloatField(null
=True)
1257 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1258 implantation
= models
.ForeignKey(ref
.Implantation
,
1259 db_column
='implantation',
1260 related_name
='%(app_label)s_valeur_point')
1262 annee
= models
.IntegerField()
1265 ordering
= ['-annee', 'implantation__nom']
1267 verbose_name
= u
"Valeur du point"
1268 verbose_name_plural
= u
"Valeurs du point"
1270 def __unicode__(self
):
1271 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1274 class ValeurPoint(ValeurPoint_
):
1275 __doc__
= ValeurPoint_
.__doc__
1279 class Devise(AUFMetadata
):
1280 """Devise monétaire.
1283 objects
= DeviseManager()
1285 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1286 code
= models
.CharField(max_length
=10, unique
=True)
1287 nom
= models
.CharField(max_length
=255)
1291 verbose_name
= u
"Devise"
1292 verbose_name_plural
= u
"Devises"
1294 def __unicode__(self
):
1295 return u
'%s - %s' % (self
.code
, self
.nom
)
1297 class TypeContrat(AUFMetadata
):
1300 nom
= models
.CharField(max_length
=255)
1301 nom_long
= models
.CharField(max_length
=255)
1305 verbose_name
= u
"Type de contrat"
1306 verbose_name_plural
= u
"Types de contrat"
1308 def __unicode__(self
):
1309 return u
'%s' % (self
.nom
)
1314 class ResponsableImplantation(AUFMetadata
):
1315 """Le responsable d'une implantation.
1316 Anciennement géré sur le Dossier du responsable.
1318 employe
= models
.ForeignKey('Employe', db_column
='employe',
1320 null
=True, blank
=True)
1321 implantation
= models
.ForeignKey(ref
.Implantation
,
1322 db_column
='implantation', related_name
='+',
1325 def __unicode__(self
):
1326 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1329 ordering
= ['implantation__nom']
1330 verbose_name
= "Responsable d'implantation"
1331 verbose_name_plural
= "Responsables d'implantation"