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
]
250 def get_devise(self
):
251 vp
= ValeurPoint
.objects
.filter(implantation
=self
.implantation
, devise__archive
=False).order_by('annee')
255 return Devise
.objects
.get(code
='EUR')
258 __doc__
= Poste_
.__doc__
260 # meta dématérialisation : pour permettre le filtrage
261 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
265 if self
.occupe_par():
269 def occupe_par(self
):
270 """Retourne la liste d'employé occupant ce poste.
271 Généralement, retourne une liste d'un élément.
272 Si poste inoccupé, retourne liste vide.
273 UTILISE pour mettre a jour le flag vacant
275 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
278 POSTE_FINANCEMENT_CHOICES
= (
279 ('A', 'A - Frais de personnel'),
280 ('B', 'B - Projet(s)-Titre(s)'),
285 class PosteFinancement_(models
.Model
):
286 """Pour un Poste, structure d'informations décrivant comment on prévoit
289 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
290 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
291 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
292 help_text
="ex.: 33.33 % (décimale avec point)")
293 commentaire
= models
.TextField(
294 help_text
="Spécifiez la source de financement.")
300 def __unicode__(self
):
301 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
304 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
307 class PosteFinancement(PosteFinancement_
):
311 class PostePiece_(models
.Model
):
312 """Documents relatifs au Poste.
313 Ex.: Description de poste
315 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
316 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
317 fichier
= models
.FileField(verbose_name
= u
"Fichier",
318 upload_to
=poste_piece_dispatch
,
319 storage
=storage_prive
)
325 def __unicode__(self
):
326 return u
'%s' % (self
.nom
)
328 class PostePiece(PostePiece_
):
331 class PosteComparaison_(AUFMetadata
, DevisableMixin
):
333 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
335 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
336 objects
= PosteComparaisonManager()
338 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
339 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
340 montant
= models
.IntegerField(null
=True)
341 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
347 def __unicode__(self
):
350 class PosteComparaison(PosteComparaison_
):
351 objects
= NoDeleteManager()
353 class PosteCommentaire_(Commentaire
):
354 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
359 class PosteCommentaire(PosteCommentaire_
):
364 class Employe(AUFMetadata
):
365 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
366 Dossiers qu'il occupe ou a occupé de Postes.
368 Cette classe aurait pu avantageusement s'appeler Personne car la notion
369 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
372 nom
= models
.CharField(max_length
=255)
373 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
374 nom_affichage
= models
.CharField(max_length
=255,
375 verbose_name
= u
"Nom d'affichage",
376 null
=True, blank
=True)
377 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
378 db_column
='nationalite',
379 related_name
='employes_nationalite',
380 verbose_name
= u
"Nationalité",
381 blank
=True, null
=True)
382 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
383 help_text
=HELP_TEXT_DATE
,
384 validators
=[validate_date_passee
],
385 null
=True, blank
=True)
386 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
389 situation_famille
= models
.CharField(max_length
=1,
390 choices
=SITUATION_CHOICES
,
391 verbose_name
= u
"Situation familiale",
392 null
=True, blank
=True)
393 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
394 help_text
=HELP_TEXT_DATE
,
395 null
=True, blank
=True)
398 tel_domicile
= models
.CharField(max_length
=255,
399 verbose_name
= u
"Tél. domicile",
400 null
=True, blank
=True)
401 tel_cellulaire
= models
.CharField(max_length
=255,
402 verbose_name
= u
"Tél. cellulaire",
403 null
=True, blank
=True)
404 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
405 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
406 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
407 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
408 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
409 related_name
='employes',
410 null
=True, blank
=True)
412 # meta dématérialisation : pour permettre le filtrage
413 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
416 ordering
= ['nom','prenom']
417 verbose_name
= u
"Employé"
418 verbose_name_plural
= u
"Employés"
420 def __unicode__(self
):
421 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
425 if self
.genre
.upper() == u
'M':
427 elif self
.genre
.upper() == u
'F':
432 """Retourne l'URL du service retournant la photo de l'Employe.
433 Équivalent reverse url 'rh_photo' avec id en param.
435 from django
.core
.urlresolvers
import reverse
436 return reverse('rh_photo', kwargs
={'id':self
.id})
438 def dossiers_passes(self
):
439 params
= {KEY_STATUT
: STATUT_INACTIF
, }
440 search
= RechercheTemporelle(params
, self
.__class__
)
441 search
.purge_params(params
)
442 q
= search
.get_q_temporel(self
.rh_dossiers
)
443 return self
.rh_dossiers
.filter(q
)
445 def dossiers_futurs(self
):
446 params
= {KEY_STATUT
: STATUT_FUTUR
, }
447 search
= RechercheTemporelle(params
, self
.__class__
)
448 search
.purge_params(params
)
449 q
= search
.get_q_temporel(self
.rh_dossiers
)
450 return self
.rh_dossiers
.filter(q
)
452 def dossiers_encours(self
):
453 params
= {KEY_STATUT
: STATUT_ACTIF
, }
454 search
= RechercheTemporelle(params
, self
.__class__
)
455 search
.purge_params(params
)
456 q
= search
.get_q_temporel(self
.rh_dossiers
)
457 return self
.rh_dossiers
.filter(q
)
459 def postes_encours(self
):
460 postes_encours
= set()
461 for d
in self
.dossiers_encours():
462 postes_encours
.add(d
.poste
)
463 return postes_encours
465 def poste_principal(self
):
467 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
469 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
471 poste
= Poste
.objects
.none()
473 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
478 prefix_implantation
= "rh_dossiers__poste__implantation__region"
479 def get_regions(self
):
481 for d
in self
.dossiers
.all():
482 regions
.append(d
.poste
.implantation
.region
)
486 class EmployePiece(models
.Model
):
487 """Documents relatifs à un employé.
490 employe
= models
.ForeignKey('Employe', db_column
='employe')
491 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
492 fichier
= models
.FileField(verbose_name
="Fichier",
493 upload_to
=employe_piece_dispatch
,
494 storage
=storage_prive
)
498 verbose_name
= u
"Employé pièce"
499 verbose_name_plural
= u
"Employé pièces"
501 def __unicode__(self
):
502 return u
'%s' % (self
.nom
)
504 class EmployeCommentaire(Commentaire
):
505 employe
= models
.ForeignKey('Employe', db_column
='employe',
509 verbose_name
= u
"Employé commentaire"
510 verbose_name_plural
= u
"Employé commentaires"
513 LIEN_PARENTE_CHOICES
= (
514 ('Conjoint', 'Conjoint'),
515 ('Conjointe', 'Conjointe'),
520 class AyantDroit(AUFMetadata
):
521 """Personne en relation avec un Employe.
524 nom
= models
.CharField(max_length
=255)
525 prenom
= models
.CharField(max_length
=255,
526 verbose_name
= u
"Prénom",)
527 nom_affichage
= models
.CharField(max_length
=255,
528 verbose_name
= u
"Nom d'affichage",
529 null
=True, blank
=True)
530 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
531 db_column
='nationalite',
532 related_name
='ayantdroits_nationalite',
533 verbose_name
= u
"Nationalité",
534 null
=True, blank
=True)
535 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
536 help_text
=HELP_TEXT_DATE
,
537 validators
=[validate_date_passee
],
538 null
=True, blank
=True)
539 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
542 employe
= models
.ForeignKey('Employe', db_column
='employe',
543 related_name
='ayantdroits',
544 verbose_name
= u
"Employé")
545 lien_parente
= models
.CharField(max_length
=10,
546 choices
=LIEN_PARENTE_CHOICES
,
547 verbose_name
= u
"Lien de parenté",
548 null
=True, blank
=True)
552 verbose_name
= u
"Ayant droit"
553 verbose_name_plural
= u
"Ayants droit"
555 def __unicode__(self
):
556 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
558 prefix_implantation
= "employe__dossiers__poste__implantation__region"
559 def get_regions(self
):
561 for d
in self
.employe
.dossiers
.all():
562 regions
.append(d
.poste
.implantation
.region
)
566 class AyantDroitCommentaire(Commentaire
):
567 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
573 STATUT_RESIDENCE_CHOICES
= (
575 ('expat', 'Expatrié'),
578 COMPTE_COMPTA_CHOICES
= (
584 class Dossier_(AUFMetadata
, DevisableMixin
):
585 """Le Dossier regroupe les informations relatives à l'occupation
586 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
589 Plusieurs Contrats peuvent être associés au Dossier.
590 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
591 lequel aucun Dossier n'existe est un poste vacant.
594 objects
= DossierManager()
597 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
598 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
599 db_column
='organisme_bstg',
601 verbose_name
= u
"Organisme",
602 help_text
="Si détaché (DET) ou \
603 mis à disposition (MAD), \
604 préciser l'organisme.",
605 null
=True, blank
=True)
608 remplacement
= models
.BooleanField(default
=False)
609 remplacement_de
= models
.ForeignKey('self', related_name
='+',
610 help_text
=u
"Taper le nom de l'employé",
611 null
=True, blank
=True)
612 statut_residence
= models
.CharField(max_length
=10, default
='local',
613 verbose_name
= u
"Statut", null
=True,
614 choices
=STATUT_RESIDENCE_CHOICES
)
617 classement
= models
.ForeignKey('Classement', db_column
='classement',
619 null
=True, blank
=True)
620 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
622 default
=REGIME_TRAVAIL_DEFAULT
,
623 verbose_name
= u
"Régime de travail",
624 help_text
="% du temps complet")
625 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
626 decimal_places
=2, null
=True,
627 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
628 verbose_name
=u
"Nb. heures par semaine",
629 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
)
631 # Occupation du Poste par cet Employe (anciennement "mandat")
632 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
634 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
636 null
=True, blank
=True)
643 ordering
= ['employe__nom', ]
644 verbose_name
= u
"Dossier"
645 verbose_name_plural
= "Dossiers"
647 def salaire_theorique(self
):
648 annee
= date
.today().year
649 coeff
= self
.classement
.coefficient
650 implantation
= self
.poste
.implantation
651 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
653 montant
= coeff
* point
.valeur
654 devise
= point
.devise
655 return {'montant':montant
, 'devise':devise
}
657 def __unicode__(self
):
658 poste
= self
.poste
.nom
659 if self
.employe
.genre
== 'F':
660 poste
= self
.poste
.nom_feminin
661 return u
'%s - %s' % (self
.employe
, poste
)
663 prefix_implantation
= "poste__implantation__region"
664 def get_regions(self
):
665 return [self
.poste
.implantation
.region
]
668 def remunerations(self
):
669 key
= "%s_remunerations" % self
._meta
.app_label
670 remunerations
= getattr(self
, key
)
671 return remunerations
.all().order_by('-date_debut')
673 def remunerations_en_cours(self
):
674 q
= Q(date_fin__exact
=None) |
Q(date_fin__gt
=datetime
.date
.today())
675 return self
.remunerations().all().filter(q
).order_by('date_debut')
677 def get_salaire(self
):
679 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
683 def get_salaire_euros(self
):
684 tx
= self
.taux_devise()
685 return (float)(tx
) * (float)(self
.salaire
)
687 def get_remunerations_brutes(self
):
691 4 Indemnité d'expatriation
692 5 Indemnité pour frais
693 6 Indemnité de logement
694 7 Indemnité de fonction
695 8 Indemnité de responsabilité
696 9 Indemnité de transport
697 10 Indemnité compensatrice
698 11 Indemnité de subsistance
699 12 Indemnité différentielle
700 13 Prime d'installation
703 16 Indemnité de départ
704 18 Prime de 13ième mois
707 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
708 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
710 def get_charges_salariales(self
):
712 20 Charges salariales ?
715 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
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_remunerations_tierces(self
):
728 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in (2, )]
732 def get_total_local_charges_salariales(self
):
733 devise
= self
.poste
.get_devise()
735 for r
in self
.get_charges_salariales():
736 if r
.devise
!= devise
:
738 total
+= float(r
.montant
)
741 def get_total_local_charges_patronales(self
):
742 devise
= self
.poste
.get_devise()
744 for r
in self
.get_charges_patronales():
745 if r
.devise
!= devise
:
747 total
+= float(r
.montant
)
750 def get_local_salaire_brut(self
):
752 somme des rémuérations brutes
754 devise
= self
.poste
.get_devise()
756 for r
in self
.get_remunerations_brutes():
757 if r
.devise
!= devise
:
759 total
+= float(r
.montant
)
762 def get_local_salaire_net(self
):
764 salaire brut - charges salariales
766 devise
= self
.poste
.get_devise()
768 for r
in self
.get_charges_salariales():
769 if r
.devise
!= devise
:
771 total_charges
+= float(r
.montant
)
772 return self
.get_local_salaire_brut() - total_charges
774 def get_local_couts_auf(self
):
776 salaire net + charges patronales
778 devise
= self
.poste
.get_devise()
780 for r
in self
.get_charges_patronales():
781 if r
.devise
!= devise
:
783 total_charges
+= float(r
.montant
)
784 return self
.get_local_salaire_net() + total_charges
786 def get_total_local_remunerations_tierces(self
):
787 devise
= self
.poste
.get_devise()
789 for r
in self
.get_remunerations_tierces():
790 if r
.devise
!= devise
:
792 total
+= float(r
.montant
)
797 def get_total_charges_salariales(self
):
799 for r
in self
.get_charges_salariales():
800 total
+= r
.montant_euros()
803 def get_total_charges_patronales(self
):
805 for r
in self
.get_charges_patronales():
806 total
+= r
.montant_euros()
809 def get_salaire_brut(self
):
811 somme des rémuérations brutes
814 for r
in self
.get_remunerations_brutes():
815 total
+= r
.montant_euros()
818 def get_salaire_net(self
):
820 salaire brut - charges salariales
823 for r
in self
.get_charges_salariales():
824 total_charges
+= r
.montant_euros()
825 return self
.get_salaire_brut() - total_charges
827 def get_couts_auf(self
):
829 salaire net + charges patronales
832 for r
in self
.get_charges_patronales():
833 total_charges
+= r
.montant_euros()
834 return self
.get_salaire_net() + total_charges
836 def get_total_remunerations_tierces(self
):
838 for r
in self
.get_remunerations_tierces():
839 total
+= r
.montant_euros()
843 class Dossier(Dossier_
):
844 __doc__
= Dossier_
.__doc__
845 poste
= models
.ForeignKey('%s.Poste' % app_context(),
847 related_name
='%(app_label)s_dossiers',
848 help_text
=u
"Taper le nom du poste ou du type de poste",
850 employe
= models
.ForeignKey('Employe', db_column
='employe',
851 help_text
=u
"Taper le nom de l'employé",
852 related_name
='%(app_label)s_dossiers',
853 verbose_name
=u
"Employé")
854 principal
= models
.BooleanField(verbose_name
=u
"Principal?", default
=True,
855 help_text
=u
"Ce Dossier est pour le principal Poste occupé par l'Employé")
858 class DossierPiece_(models
.Model
):
859 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
860 Ex.: Lettre de motivation.
862 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_dossierpieces')
863 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
864 fichier
= models
.FileField(verbose_name
= u
"Fichier",
865 upload_to
=dossier_piece_dispatch
,
866 storage
=storage_prive
)
872 def __unicode__(self
):
873 return u
'%s' % (self
.nom
)
875 class DossierPiece(DossierPiece_
):
878 class DossierCommentaire_(Commentaire
):
879 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
883 class DossierCommentaire(DossierCommentaire_
):
886 class DossierComparaison_(models
.Model
, DevisableMixin
):
888 Photo d'une comparaison salariale au moment de l'embauche.
890 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
891 objects
= DossierComparaisonManager()
893 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
894 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
895 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
896 montant
= models
.IntegerField(null
=True)
897 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
902 def __unicode__(self
):
903 return "%s (%s)" % (self
.poste
, self
.personne
)
906 class DossierComparaison(DossierComparaison_
):
911 class RemunerationMixin(AUFMetadata
):
912 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
914 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
916 verbose_name
= u
"Type de rémunération")
917 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
918 db_column
='type_revalorisation',
920 verbose_name
= u
"Type de revalorisation",
921 null
=True, blank
=True)
922 montant
= models
.DecimalField(null
=True, blank
=True,
923 default
=0, max_digits
=12, decimal_places
=2)
924 # Annuel (12 mois, 52 semaines, 364 jours?)
925 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
926 # commentaire = precision
927 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
928 # date_debut = anciennement date_effectif
929 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
930 null
=True, blank
=True)
931 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
932 null
=True, blank
=True)
936 ordering
= ['type__nom', '-date_fin']
938 def __unicode__(self
):
939 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
941 class Remuneration_(RemunerationMixin
, DevisableMixin
):
942 """Structure de rémunération (données budgétaires) en situation normale
943 pour un Dossier. Si un Evenement existe, utiliser la structure de
944 rémunération EvenementRemuneration de cet événement.
947 def montant_mois(self
):
948 return round(self
.montant
/ 12, 2)
950 def montant_avec_regime(self
):
951 return round(self
.montant
* (self
.dossier
.regime_travail
/100), 2)
953 def montant_euro_mois(self
):
954 return round(self
.montant_euros() / 12, 2)
956 def __unicode__(self
):
958 devise
= self
.devise
.code
961 return "%s %s" % (self
.montant
, devise
)
965 verbose_name
= u
"Rémunération"
966 verbose_name_plural
= u
"Rémunérations"
969 class Remuneration(Remuneration_
):
975 class ContratManager(NoDeleteManager
):
976 def get_query_set(self
):
977 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
980 class Contrat_(AUFMetadata
):
981 """Document juridique qui encadre la relation de travail d'un Employe
982 pour un Poste particulier. Pour un Dossier (qui documente cette
983 relation de travail) plusieurs contrats peuvent être associés.
985 objects
= ContratManager()
986 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
987 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
989 verbose_name
= u
"type de contrat")
990 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
991 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
992 null
=True, blank
=True)
993 fichier
= models
.FileField(verbose_name
= u
"Fichier",
994 upload_to
=contrat_dispatch
,
995 storage
=storage_prive
,
996 null
=True, blank
=True)
1000 ordering
= ['dossier__employe__nom']
1001 verbose_name
= u
"Contrat"
1002 verbose_name_plural
= u
"Contrats"
1004 def __unicode__(self
):
1005 return u
'%s - %s' % (self
.dossier
, self
.id)
1007 class Contrat(Contrat_
):
1013 #class Evenement_(AUFMetadata):
1014 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
1015 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
1016 # (ex.: la Remuneration).
1018 # Ex.: congé de maternité, maladie...
1020 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
1021 # différent et une rémunération en conséquence. On souhaite toutefois
1022 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
1023 # du retour à la normale.
1025 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
1027 # nom = models.CharField(max_length=255)
1028 # date_debut = models.DateField(verbose_name = u"Date de début")
1029 # date_fin = models.DateField(verbose_name = u"Date de fin",
1030 # null=True, blank=True)
1034 # ordering = ['nom']
1035 # verbose_name = u"Évènement"
1036 # verbose_name_plural = u"Évènements"
1038 # def __unicode__(self):
1039 # return u'%s' % (self.nom)
1042 #class Evenement(Evenement_):
1043 # __doc__ = Evenement_.__doc__
1046 #class EvenementRemuneration_(RemunerationMixin):
1047 # """Structure de rémunération liée à un Evenement qui remplace
1048 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
1051 # evenement = models.ForeignKey("Evenement", db_column='evenement',
1053 # verbose_name = u"Évènement")
1054 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
1055 # # de l'Evenement associé
1059 # ordering = ['evenement', 'type__nom', '-date_fin']
1060 # verbose_name = u"Évènement - rémunération"
1061 # verbose_name_plural = u"Évènements - rémunérations"
1064 #class EvenementRemuneration(EvenementRemuneration_):
1065 # __doc__ = EvenementRemuneration_.__doc__
1071 #class EvenementRemuneration(EvenementRemuneration_):
1072 # __doc__ = EvenementRemuneration_.__doc__
1073 # TODO? class ContratPiece(models.Model):
1078 class FamilleEmploi(AUFMetadata
):
1079 """Catégorie utilisée dans la gestion des Postes.
1080 Catégorie supérieure à TypePoste.
1082 nom
= models
.CharField(max_length
=255)
1086 verbose_name
= u
"Famille d'emploi"
1087 verbose_name_plural
= u
"Familles d'emploi"
1089 def __unicode__(self
):
1090 return u
'%s' % (self
.nom
)
1092 class TypePoste(AUFMetadata
):
1093 """Catégorie de Poste.
1095 nom
= models
.CharField(max_length
=255)
1096 nom_feminin
= models
.CharField(max_length
=255,
1097 verbose_name
= u
"Nom féminin")
1099 is_responsable
= models
.BooleanField(default
=False,
1100 verbose_name
= u
"Poste de responsabilité")
1101 famille_emploi
= models
.ForeignKey('FamilleEmploi',
1102 db_column
='famille_emploi',
1104 verbose_name
= u
"famille d'emploi")
1108 verbose_name
= u
"Type de poste"
1109 verbose_name_plural
= u
"Types de poste"
1111 def __unicode__(self
):
1112 return u
'%s' % (self
.nom
)
1115 TYPE_PAIEMENT_CHOICES
= (
1116 (u
'Régulier', u
'Régulier'),
1117 (u
'Ponctuel', u
'Ponctuel'),
1120 NATURE_REMUNERATION_CHOICES
= (
1121 (u
'Accessoire', u
'Accessoire'),
1122 (u
'Charges', u
'Charges'),
1123 (u
'Indemnité', u
'Indemnité'),
1124 (u
'RAS', u
'Rémunération autre source'),
1125 (u
'Traitement', u
'Traitement'),
1128 class TypeRemuneration(AUFMetadata
):
1129 """Catégorie de Remuneration.
1131 objects
= TypeRemunerationManager()
1133 nom
= models
.CharField(max_length
=255)
1134 type_paiement
= models
.CharField(max_length
=30,
1135 choices
=TYPE_PAIEMENT_CHOICES
,
1136 verbose_name
= u
"Type de paiement")
1137 nature_remuneration
= models
.CharField(max_length
=30,
1138 choices
=NATURE_REMUNERATION_CHOICES
,
1139 verbose_name
= u
"Nature de la rémunération")
1140 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1144 verbose_name
= u
"Type de rémunération"
1145 verbose_name_plural
= u
"Types de rémunération"
1147 def __unicode__(self
):
1149 archive
= u
"(archivé)"
1152 return u
'%s %s' % (self
.nom
, archive
)
1154 class TypeRevalorisation(AUFMetadata
):
1155 """Justification du changement de la Remuneration.
1156 (Actuellement utilisé dans aucun traitement informatique.)
1158 nom
= models
.CharField(max_length
=255)
1162 verbose_name
= u
"Type de revalorisation"
1163 verbose_name_plural
= u
"Types de revalorisation"
1165 def __unicode__(self
):
1166 return u
'%s' % (self
.nom
)
1168 class Service(AUFMetadata
):
1169 """Unité administrative où les Postes sont rattachés.
1171 objects
= ServiceManager()
1173 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1174 nom
= models
.CharField(max_length
=255)
1178 verbose_name
= u
"Service"
1179 verbose_name_plural
= u
"Services"
1181 def __unicode__(self
):
1183 archive
= u
"(archivé)"
1186 return u
'%s %s' % (self
.nom
, archive
)
1189 TYPE_ORGANISME_CHOICES
= (
1190 ('MAD', 'Mise à disposition'),
1191 ('DET', 'Détachement'),
1194 class OrganismeBstg(AUFMetadata
):
1195 """Organisation d'où provient un Employe mis à disposition (MAD) de
1196 ou détaché (DET) à l'AUF à titre gratuit.
1198 (BSTG = bien et service à titre gratuit.)
1200 nom
= models
.CharField(max_length
=255)
1201 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1202 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1204 related_name
='organismes_bstg',
1205 null
=True, blank
=True)
1208 ordering
= ['type', 'nom']
1209 verbose_name
= u
"Organisme BSTG"
1210 verbose_name_plural
= u
"Organismes BSTG"
1212 def __unicode__(self
):
1213 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1215 prefix_implantation
= "pays__region"
1216 def get_regions(self
):
1217 return [self
.pays
.region
]
1220 class Statut(AUFMetadata
):
1221 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1224 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.")
1225 nom
= models
.CharField(max_length
=255)
1229 verbose_name
= u
"Statut d'employé"
1230 verbose_name_plural
= u
"Statuts d'employé"
1232 def __unicode__(self
):
1233 return u
'%s : %s' % (self
.code
, self
.nom
)
1236 TYPE_CLASSEMENT_CHOICES
= (
1237 ('S', 'S -Soutien'),
1238 ('T', 'T - Technicien'),
1239 ('P', 'P - Professionel'),
1241 ('D', 'D - Direction'),
1242 ('SO', 'SO - Sans objet [expatriés]'),
1243 ('HG', 'HG - Hors grille [direction]'),
1246 class ClassementManager(models
.Manager
):
1248 Ordonner les spcéfiquement les classements.
1250 def get_query_set(self
):
1251 qs
= super(self
.__class__
, self
).get_query_set()
1252 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1253 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1257 class Classement_(AUFMetadata
):
1258 """Éléments de classement de la
1259 "Grille générique de classement hiérarchique".
1261 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1262 classement dans la grille. Le classement donne le coefficient utilisé dans:
1264 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1266 objects
= ClassementManager()
1269 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1270 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1271 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1272 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1275 # annee # au lieu de date_debut et date_fin
1276 commentaire
= models
.TextField(null
=True, blank
=True)
1280 ordering
= ['type','echelon','degre','coefficient']
1281 verbose_name
= u
"Classement"
1282 verbose_name_plural
= u
"Classements"
1284 def __unicode__(self
):
1285 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1287 class Classement(Classement_
):
1288 __doc__
= Classement_
.__doc__
1291 class TauxChange_(AUFMetadata
):
1292 """Taux de change de la devise vers l'euro (EUR)
1293 pour chaque année budgétaire.
1296 devise
= models
.ForeignKey('Devise', db_column
='devise')
1297 annee
= models
.IntegerField(verbose_name
= u
"Année")
1298 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1302 ordering
= ['-annee', 'devise__code']
1303 verbose_name
= u
"Taux de change"
1304 verbose_name_plural
= u
"Taux de change"
1306 def __unicode__(self
):
1307 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1310 class TauxChange(TauxChange_
):
1311 __doc__
= TauxChange_
.__doc__
1313 class ValeurPointManager(NoDeleteManager
):
1315 def get_query_set(self
):
1316 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1319 class ValeurPoint_(AUFMetadata
):
1320 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1321 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1322 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1324 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1327 actuelles
= ValeurPointManager()
1329 valeur
= models
.FloatField(null
=True)
1330 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1331 implantation
= models
.ForeignKey(ref
.Implantation
,
1332 db_column
='implantation',
1333 related_name
='%(app_label)s_valeur_point')
1335 annee
= models
.IntegerField()
1338 ordering
= ['-annee', 'implantation__nom']
1340 verbose_name
= u
"Valeur du point"
1341 verbose_name_plural
= u
"Valeurs du point"
1343 def __unicode__(self
):
1344 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1347 class ValeurPoint(ValeurPoint_
):
1348 __doc__
= ValeurPoint_
.__doc__
1352 class Devise(AUFMetadata
):
1353 """Devise monétaire.
1356 objects
= DeviseManager()
1358 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1359 code
= models
.CharField(max_length
=10, unique
=True)
1360 nom
= models
.CharField(max_length
=255)
1364 verbose_name
= u
"Devise"
1365 verbose_name_plural
= u
"Devises"
1367 def __unicode__(self
):
1368 return u
'%s - %s' % (self
.code
, self
.nom
)
1370 class TypeContrat(AUFMetadata
):
1373 nom
= models
.CharField(max_length
=255)
1374 nom_long
= models
.CharField(max_length
=255)
1378 verbose_name
= u
"Type de contrat"
1379 verbose_name_plural
= u
"Types de contrat"
1381 def __unicode__(self
):
1382 return u
'%s' % (self
.nom
)
1387 class ResponsableImplantationProxy(ref
.Implantation
):
1390 verbose_name
= u
"Responsable d'implantation"
1391 verbose_name_plural
= u
"Responsables d'implantation"
1394 class ResponsableImplantation(models
.Model
):
1395 """Le responsable d'une implantation.
1396 Anciennement géré sur le Dossier du responsable.
1398 employe
= models
.ForeignKey('Employe', db_column
='employe',
1400 null
=True, blank
=True)
1401 implantation
= models
.OneToOneField("ResponsableImplantationProxy",
1402 db_column
='implantation', related_name
='responsable',
1405 def __unicode__(self
):
1406 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1409 ordering
= ['implantation__nom']
1410 verbose_name
= "Responsable d'implantation"
1411 verbose_name_plural
= "Responsables d'implantation"