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 return ValeurPoint
.objects
.filter(implantation
=self
.implantation
, devise__archive
=False).order_by('annee')[0].devise
254 __doc__
= Poste_
.__doc__
256 # meta dématérialisation : pour permettre le filtrage
257 vacant
= models
.NullBooleanField(verbose_name
= u
"vacant", null
=True, blank
=True)
261 if self
.occupe_par():
265 def occupe_par(self
):
266 """Retourne la liste d'employé occupant ce poste.
267 Généralement, retourne une liste d'un élément.
268 Si poste inoccupé, retourne liste vide.
269 UTILISE pour mettre a jour le flag vacant
271 return [d
.employe
for d
in self
.rh_dossiers
.filter(supprime
=False).exclude(date_fin__lt
=date
.today())]
274 POSTE_FINANCEMENT_CHOICES
= (
275 ('A', 'A - Frais de personnel'),
276 ('B', 'B - Projet(s)-Titre(s)'),
281 class PosteFinancement_(models
.Model
):
282 """Pour un Poste, structure d'informations décrivant comment on prévoit
285 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_financements')
286 type = models
.CharField(max_length
=1, choices
=POSTE_FINANCEMENT_CHOICES
)
287 pourcentage
= models
.DecimalField(max_digits
=12, decimal_places
=2,
288 help_text
="ex.: 33.33 % (décimale avec point)")
289 commentaire
= models
.TextField(
290 help_text
="Spécifiez la source de financement.")
296 def __unicode__(self
):
297 return u
'%s : %s %%' % (self
.type, self
.pourcentage
)
300 return u
"%s" % dict(POSTE_FINANCEMENT_CHOICES
)[self
.type]
303 class PosteFinancement(PosteFinancement_
):
307 class PostePiece_(models
.Model
):
308 """Documents relatifs au Poste.
309 Ex.: Description de poste
311 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='%(app_label)s_pieces')
312 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
313 fichier
= models
.FileField(verbose_name
= u
"Fichier",
314 upload_to
=poste_piece_dispatch
,
315 storage
=storage_prive
)
321 def __unicode__(self
):
322 return u
'%s' % (self
.nom
)
324 class PostePiece(PostePiece_
):
327 class PosteComparaison_(AUFMetadata
, DevisableMixin
):
329 De la même manière qu'un dossier, un poste peut-être comparé à un autre poste.
331 poste
= models
.ForeignKey('%s.Poste' % app_context(), related_name
='%(app_label)s_comparaisons_internes')
332 objects
= PosteComparaisonManager()
334 implantation
= models
.ForeignKey(ref
.Implantation
, null
=True, blank
=True, related_name
="+")
335 nom
= models
.CharField(verbose_name
= u
"Poste", max_length
=255, null
=True, blank
=True)
336 montant
= models
.IntegerField(null
=True)
337 devise
= models
.ForeignKey("Devise", related_name
='+', null
=True, blank
=True)
343 def __unicode__(self
):
346 class PosteComparaison(PosteComparaison_
):
347 objects
= NoDeleteManager()
349 class PosteCommentaire_(Commentaire
):
350 poste
= models
.ForeignKey('%s.Poste' % app_context(), db_column
='poste', related_name
='+')
355 class PosteCommentaire(PosteCommentaire_
):
360 class Employe(AUFMetadata
):
361 """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
362 Dossiers qu'il occupe ou a occupé de Postes.
364 Cette classe aurait pu avantageusement s'appeler Personne car la notion
365 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
368 nom
= models
.CharField(max_length
=255)
369 prenom
= models
.CharField(max_length
=255, verbose_name
= u
"Prénom")
370 nom_affichage
= models
.CharField(max_length
=255,
371 verbose_name
= u
"Nom d'affichage",
372 null
=True, blank
=True)
373 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
374 db_column
='nationalite',
375 related_name
='employes_nationalite',
376 verbose_name
= u
"Nationalité",
377 blank
=True, null
=True)
378 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
379 help_text
=HELP_TEXT_DATE
,
380 validators
=[validate_date_passee
],
381 null
=True, blank
=True)
382 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
385 situation_famille
= models
.CharField(max_length
=1,
386 choices
=SITUATION_CHOICES
,
387 verbose_name
= u
"Situation familiale",
388 null
=True, blank
=True)
389 date_entree
= models
.DateField(verbose_name
= u
"Date d'entrée à l'AUF",
390 help_text
=HELP_TEXT_DATE
,
391 null
=True, blank
=True)
394 tel_domicile
= models
.CharField(max_length
=255,
395 verbose_name
= u
"Tél. domicile",
396 null
=True, blank
=True)
397 tel_cellulaire
= models
.CharField(max_length
=255,
398 verbose_name
= u
"Tél. cellulaire",
399 null
=True, blank
=True)
400 adresse
= models
.CharField(max_length
=255, null
=True, blank
=True)
401 ville
= models
.CharField(max_length
=255, null
=True, blank
=True)
402 province
= models
.CharField(max_length
=255, null
=True, blank
=True)
403 code_postal
= models
.CharField(max_length
=255, null
=True, blank
=True)
404 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code', db_column
='pays',
405 related_name
='employes',
406 null
=True, blank
=True)
408 # meta dématérialisation : pour permettre le filtrage
409 nb_postes
= models
.IntegerField(verbose_name
= u
"nombre de postes", null
=True, blank
=True)
412 ordering
= ['nom','prenom']
413 verbose_name
= u
"Employé"
414 verbose_name_plural
= u
"Employés"
416 def __unicode__(self
):
417 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
421 if self
.genre
.upper() == u
'M':
423 elif self
.genre
.upper() == u
'F':
428 """Retourne l'URL du service retournant la photo de l'Employe.
429 Équivalent reverse url 'rh_photo' avec id en param.
431 from django
.core
.urlresolvers
import reverse
432 return reverse('rh_photo', kwargs
={'id':self
.id})
434 def dossiers_passes(self
):
435 params
= {KEY_STATUT
: STATUT_INACTIF
, }
436 search
= RechercheTemporelle(params
, self
.__class__
)
437 search
.purge_params(params
)
438 q
= search
.get_q_temporel(self
.rh_dossiers
)
439 return self
.rh_dossiers
.filter(q
)
441 def dossiers_futurs(self
):
442 params
= {KEY_STATUT
: STATUT_FUTUR
, }
443 search
= RechercheTemporelle(params
, self
.__class__
)
444 search
.purge_params(params
)
445 q
= search
.get_q_temporel(self
.rh_dossiers
)
446 return self
.rh_dossiers
.filter(q
)
448 def dossiers_encours(self
):
449 params
= {KEY_STATUT
: STATUT_ACTIF
, }
450 search
= RechercheTemporelle(params
, self
.__class__
)
451 search
.purge_params(params
)
452 q
= search
.get_q_temporel(self
.rh_dossiers
)
453 return self
.rh_dossiers
.filter(q
)
455 def postes_encours(self
):
456 postes_encours
= set()
457 for d
in self
.dossiers_encours():
458 postes_encours
.add(d
.poste
)
459 return postes_encours
461 def poste_principal(self
):
463 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
465 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
467 poste
= Poste
.objects
.none()
469 poste
= self
.dossiers_encours().order_by('date_debut')[0].poste
474 prefix_implantation
= "rh_dossiers__poste__implantation__region"
475 def get_regions(self
):
477 for d
in self
.dossiers
.all():
478 regions
.append(d
.poste
.implantation
.region
)
482 class EmployePiece(models
.Model
):
483 """Documents relatifs à un employé.
486 employe
= models
.ForeignKey('Employe', db_column
='employe')
487 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
488 fichier
= models
.FileField(verbose_name
="Fichier",
489 upload_to
=employe_piece_dispatch
,
490 storage
=storage_prive
)
494 verbose_name
= u
"Employé pièce"
495 verbose_name_plural
= u
"Employé pièces"
497 def __unicode__(self
):
498 return u
'%s' % (self
.nom
)
500 class EmployeCommentaire(Commentaire
):
501 employe
= models
.ForeignKey('Employe', db_column
='employe',
505 verbose_name
= u
"Employé commentaire"
506 verbose_name_plural
= u
"Employé commentaires"
509 LIEN_PARENTE_CHOICES
= (
510 ('Conjoint', 'Conjoint'),
511 ('Conjointe', 'Conjointe'),
516 class AyantDroit(AUFMetadata
):
517 """Personne en relation avec un Employe.
520 nom
= models
.CharField(max_length
=255)
521 prenom
= models
.CharField(max_length
=255,
522 verbose_name
= u
"Prénom",)
523 nom_affichage
= models
.CharField(max_length
=255,
524 verbose_name
= u
"Nom d'affichage",
525 null
=True, blank
=True)
526 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
527 db_column
='nationalite',
528 related_name
='ayantdroits_nationalite',
529 verbose_name
= u
"Nationalité",
530 null
=True, blank
=True)
531 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
532 help_text
=HELP_TEXT_DATE
,
533 validators
=[validate_date_passee
],
534 null
=True, blank
=True)
535 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
538 employe
= models
.ForeignKey('Employe', db_column
='employe',
539 related_name
='ayantdroits',
540 verbose_name
= u
"Employé")
541 lien_parente
= models
.CharField(max_length
=10,
542 choices
=LIEN_PARENTE_CHOICES
,
543 verbose_name
= u
"Lien de parenté",
544 null
=True, blank
=True)
548 verbose_name
= u
"Ayant droit"
549 verbose_name_plural
= u
"Ayants droit"
551 def __unicode__(self
):
552 return u
'%s %s [%s]' % (self
.nom
.upper(), self
.prenom
, self
.id)
554 prefix_implantation
= "employe__dossiers__poste__implantation__region"
555 def get_regions(self
):
557 for d
in self
.employe
.dossiers
.all():
558 regions
.append(d
.poste
.implantation
.region
)
562 class AyantDroitCommentaire(Commentaire
):
563 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
569 STATUT_RESIDENCE_CHOICES
= (
571 ('expat', 'Expatrié'),
574 COMPTE_COMPTA_CHOICES
= (
580 class Dossier_(AUFMetadata
, DevisableMixin
):
581 """Le Dossier regroupe les informations relatives à l'occupation
582 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
585 Plusieurs Contrats peuvent être associés au Dossier.
586 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
587 lequel aucun Dossier n'existe est un poste vacant.
590 objects
= DossierManager()
593 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
594 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
595 db_column
='organisme_bstg',
597 verbose_name
= u
"Organisme",
598 help_text
="Si détaché (DET) ou \
599 mis à disposition (MAD), \
600 préciser l'organisme.",
601 null
=True, blank
=True)
604 remplacement
= models
.BooleanField(default
=False)
605 remplacement_de
= models
.ForeignKey('self', related_name
='+',
606 help_text
=u
"Taper le nom de l'employé",
607 null
=True, blank
=True)
608 statut_residence
= models
.CharField(max_length
=10, default
='local',
609 verbose_name
= u
"Statut", null
=True,
610 choices
=STATUT_RESIDENCE_CHOICES
)
613 classement
= models
.ForeignKey('Classement', db_column
='classement',
615 null
=True, blank
=True)
616 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
618 default
=REGIME_TRAVAIL_DEFAULT
,
619 verbose_name
= u
"Régime de travail",
620 help_text
="% du temps complet")
621 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
622 decimal_places
=2, null
=True,
623 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
624 verbose_name
=u
"Nb. heures par semaine",
625 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
)
627 # Occupation du Poste par cet Employe (anciennement "mandat")
628 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
630 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
632 null
=True, blank
=True)
639 ordering
= ['employe__nom', ]
640 verbose_name
= u
"Dossier"
641 verbose_name_plural
= "Dossiers"
643 def salaire_theorique(self
):
644 annee
= date
.today().year
645 coeff
= self
.classement
.coefficient
646 implantation
= self
.poste
.implantation
647 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
649 montant
= coeff
* point
.valeur
650 devise
= point
.devise
651 return {'montant':montant
, 'devise':devise
}
653 def __unicode__(self
):
654 poste
= self
.poste
.nom
655 if self
.employe
.genre
== 'F':
656 poste
= self
.poste
.nom_feminin
657 return u
'%s - %s' % (self
.employe
, poste
)
659 prefix_implantation
= "poste__implantation__region"
660 def get_regions(self
):
661 return [self
.poste
.implantation
.region
]
664 def remunerations(self
):
665 key
= "%s_remunerations" % self
._meta
.app_label
666 remunerations
= getattr(self
, key
)
667 return remunerations
.all().order_by('-date_debut')
669 def remunerations_en_cours(self
):
670 q
= Q(date_fin__exact
=None) |
Q(date_fin__gt
=datetime
.date
.today())
671 return self
.remunerations().all().filter(q
).order_by('date_debut')
673 def get_salaire(self
):
675 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
679 def get_salaire_euros(self
):
680 tx
= self
.taux_devise()
681 return (float)(tx
) * (float)(self
.salaire
)
683 def get_remunerations_brutes(self
):
687 4 Indemnité d'expatriation
688 5 Indemnité pour frais
689 6 Indemnité de logement
690 7 Indemnité de fonction
691 8 Indemnité de responsabilité
692 9 Indemnité de transport
693 10 Indemnité compensatrice
694 11 Indemnité de subsistance
695 12 Indemnité différentielle
696 13 Prime d'installation
699 16 Indemnité de départ
700 18 Prime de 13ième mois
703 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
704 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
706 def get_charges_salariales(self
):
708 20 Charges salariales ?
711 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
713 def get_charges_patronales(self
):
715 17 Charges patronales
718 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
720 def get_remunerations_tierces(self
):
724 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in (2, )]
728 def get_total_local_charges_salariales(self
):
729 devise
= self
.poste
.get_devise()
731 for r
in self
.get_charges_salariales():
732 if r
.devise
!= devise
:
734 total
+= float(r
.montant
)
737 def get_total_local_charges_patronales(self
):
738 devise
= self
.poste
.get_devise()
740 for r
in self
.get_charges_patronales():
741 if r
.devise
!= devise
:
743 total
+= float(r
.montant
)
746 def get_local_salaire_brut(self
):
748 somme des rémuérations brutes
750 devise
= self
.poste
.get_devise()
752 for r
in self
.get_remunerations_brutes():
753 if r
.devise
!= devise
:
755 total
+= float(r
.montant
)
758 def get_local_salaire_net(self
):
760 salaire brut - charges salariales
762 devise
= self
.poste
.get_devise()
764 for r
in self
.get_charges_salariales():
765 if r
.devise
!= devise
:
767 total_charges
+= float(r
.montant
)
768 return self
.get_local_salaire_brut() - total_charges
770 def get_local_couts_auf(self
):
772 salaire net + charges patronales
774 devise
= self
.poste
.get_devise()
776 for r
in self
.get_charges_patronales():
777 if r
.devise
!= devise
:
779 total_charges
+= float(r
.montant
)
780 return self
.get_local_salaire_net() + total_charges
782 def get_total_local_remunerations_tierces(self
):
783 devise
= self
.poste
.get_devise()
785 for r
in self
.get_remunerations_tierces():
786 if r
.devise
!= devise
:
788 total
+= float(r
.montant
)
793 def get_total_charges_salariales(self
):
795 for r
in self
.get_charges_salariales():
796 total
+= r
.montant_euros()
799 def get_total_charges_patronales(self
):
801 for r
in self
.get_charges_patronales():
802 total
+= r
.montant_euros()
805 def get_salaire_brut(self
):
807 somme des rémuérations brutes
810 for r
in self
.get_remunerations_brutes():
811 total
+= r
.montant_euros()
814 def get_salaire_net(self
):
816 salaire brut - charges salariales
819 for r
in self
.get_charges_salariales():
820 total_charges
+= r
.montant_euros()
821 return self
.get_salaire_brut() - total_charges
823 def get_couts_auf(self
):
825 salaire net + charges patronales
828 for r
in self
.get_charges_patronales():
829 total_charges
+= r
.montant_euros()
830 return self
.get_salaire_net() + total_charges
832 def get_total_remunerations_tierces(self
):
834 for r
in self
.get_remunerations_tierces():
835 total
+= r
.montant_euros()
839 class Dossier(Dossier_
):
840 __doc__
= Dossier_
.__doc__
841 poste
= models
.ForeignKey('%s.Poste' % app_context(),
843 related_name
='%(app_label)s_dossiers',
844 help_text
=u
"Taper le nom du poste ou du type de poste",
846 employe
= models
.ForeignKey('Employe', db_column
='employe',
847 help_text
=u
"Taper le nom de l'employé",
848 related_name
='%(app_label)s_dossiers',
849 verbose_name
=u
"Employé")
850 principal
= models
.BooleanField(verbose_name
=u
"Principal?", default
=True,
851 help_text
=u
"Ce Dossier est pour le principal Poste occupé par l'Employé")
854 class DossierPiece_(models
.Model
):
855 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
856 Ex.: Lettre de motivation.
858 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_dossierpieces')
859 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
860 fichier
= models
.FileField(verbose_name
= u
"Fichier",
861 upload_to
=dossier_piece_dispatch
,
862 storage
=storage_prive
)
868 def __unicode__(self
):
869 return u
'%s' % (self
.nom
)
871 class DossierPiece(DossierPiece_
):
874 class DossierCommentaire_(Commentaire
):
875 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
879 class DossierCommentaire(DossierCommentaire_
):
882 class DossierComparaison_(models
.Model
, DevisableMixin
):
884 Photo d'une comparaison salariale au moment de l'embauche.
886 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
887 objects
= DossierComparaisonManager()
889 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
890 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
891 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
892 montant
= models
.IntegerField(null
=True)
893 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
898 def __unicode__(self
):
899 return "%s (%s)" % (self
.poste
, self
.personne
)
902 class DossierComparaison(DossierComparaison_
):
907 class RemunerationMixin(AUFMetadata
):
908 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
910 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
912 verbose_name
= u
"Type de rémunération")
913 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
914 db_column
='type_revalorisation',
916 verbose_name
= u
"Type de revalorisation",
917 null
=True, blank
=True)
918 montant
= models
.DecimalField(null
=True, blank
=True,
919 default
=0, max_digits
=12, decimal_places
=2)
920 # Annuel (12 mois, 52 semaines, 364 jours?)
921 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
922 # commentaire = precision
923 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
924 # date_debut = anciennement date_effectif
925 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
926 null
=True, blank
=True)
927 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
928 null
=True, blank
=True)
932 ordering
= ['type__nom', '-date_fin']
934 def __unicode__(self
):
935 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
937 class Remuneration_(RemunerationMixin
, DevisableMixin
):
938 """Structure de rémunération (données budgétaires) en situation normale
939 pour un Dossier. Si un Evenement existe, utiliser la structure de
940 rémunération EvenementRemuneration de cet événement.
943 def montant_mois(self
):
944 return round(self
.montant
/ 12, 2)
946 def montant_avec_regime(self
):
947 return round(self
.montant
* (self
.dossier
.regime_travail
/100), 2)
949 def montant_euro_mois(self
):
950 return round(self
.montant_euros() / 12, 2)
952 def __unicode__(self
):
954 devise
= self
.devise
.code
957 return "%s %s" % (self
.montant
, devise
)
961 verbose_name
= u
"Rémunération"
962 verbose_name_plural
= u
"Rémunérations"
965 class Remuneration(Remuneration_
):
971 class ContratManager(NoDeleteManager
):
972 def get_query_set(self
):
973 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
976 class Contrat_(AUFMetadata
):
977 """Document juridique qui encadre la relation de travail d'un Employe
978 pour un Poste particulier. Pour un Dossier (qui documente cette
979 relation de travail) plusieurs contrats peuvent être associés.
981 objects
= ContratManager()
982 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
983 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
985 verbose_name
= u
"type de contrat")
986 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
987 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
988 null
=True, blank
=True)
989 fichier
= models
.FileField(verbose_name
= u
"Fichier",
990 upload_to
=contrat_dispatch
,
991 storage
=storage_prive
,
992 null
=True, blank
=True)
996 ordering
= ['dossier__employe__nom']
997 verbose_name
= u
"Contrat"
998 verbose_name_plural
= u
"Contrats"
1000 def __unicode__(self
):
1001 return u
'%s - %s' % (self
.dossier
, self
.id)
1003 class Contrat(Contrat_
):
1009 #class Evenement_(AUFMetadata):
1010 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
1011 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
1012 # (ex.: la Remuneration).
1014 # Ex.: congé de maternité, maladie...
1016 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
1017 # différent et une rémunération en conséquence. On souhaite toutefois
1018 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
1019 # du retour à la normale.
1021 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
1023 # nom = models.CharField(max_length=255)
1024 # date_debut = models.DateField(verbose_name = u"Date de début")
1025 # date_fin = models.DateField(verbose_name = u"Date de fin",
1026 # null=True, blank=True)
1030 # ordering = ['nom']
1031 # verbose_name = u"Évènement"
1032 # verbose_name_plural = u"Évènements"
1034 # def __unicode__(self):
1035 # return u'%s' % (self.nom)
1038 #class Evenement(Evenement_):
1039 # __doc__ = Evenement_.__doc__
1042 #class EvenementRemuneration_(RemunerationMixin):
1043 # """Structure de rémunération liée à un Evenement qui remplace
1044 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
1047 # evenement = models.ForeignKey("Evenement", db_column='evenement',
1049 # verbose_name = u"Évènement")
1050 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
1051 # # de l'Evenement associé
1055 # ordering = ['evenement', 'type__nom', '-date_fin']
1056 # verbose_name = u"Évènement - rémunération"
1057 # verbose_name_plural = u"Évènements - rémunérations"
1060 #class EvenementRemuneration(EvenementRemuneration_):
1061 # __doc__ = EvenementRemuneration_.__doc__
1067 #class EvenementRemuneration(EvenementRemuneration_):
1068 # __doc__ = EvenementRemuneration_.__doc__
1069 # TODO? class ContratPiece(models.Model):
1074 class FamilleEmploi(AUFMetadata
):
1075 """Catégorie utilisée dans la gestion des Postes.
1076 Catégorie supérieure à TypePoste.
1078 nom
= models
.CharField(max_length
=255)
1082 verbose_name
= u
"Famille d'emploi"
1083 verbose_name_plural
= u
"Familles d'emploi"
1085 def __unicode__(self
):
1086 return u
'%s' % (self
.nom
)
1088 class TypePoste(AUFMetadata
):
1089 """Catégorie de Poste.
1091 nom
= models
.CharField(max_length
=255)
1092 nom_feminin
= models
.CharField(max_length
=255,
1093 verbose_name
= u
"Nom féminin")
1095 is_responsable
= models
.BooleanField(default
=False,
1096 verbose_name
= u
"Poste de responsabilité")
1097 famille_emploi
= models
.ForeignKey('FamilleEmploi',
1098 db_column
='famille_emploi',
1100 verbose_name
= u
"famille d'emploi")
1104 verbose_name
= u
"Type de poste"
1105 verbose_name_plural
= u
"Types de poste"
1107 def __unicode__(self
):
1108 return u
'%s' % (self
.nom
)
1111 TYPE_PAIEMENT_CHOICES
= (
1112 (u
'Régulier', u
'Régulier'),
1113 (u
'Ponctuel', u
'Ponctuel'),
1116 NATURE_REMUNERATION_CHOICES
= (
1117 (u
'Accessoire', u
'Accessoire'),
1118 (u
'Charges', u
'Charges'),
1119 (u
'Indemnité', u
'Indemnité'),
1120 (u
'RAS', u
'Rémunération autre source'),
1121 (u
'Traitement', u
'Traitement'),
1124 class TypeRemuneration(AUFMetadata
):
1125 """Catégorie de Remuneration.
1127 objects
= TypeRemunerationManager()
1129 nom
= models
.CharField(max_length
=255)
1130 type_paiement
= models
.CharField(max_length
=30,
1131 choices
=TYPE_PAIEMENT_CHOICES
,
1132 verbose_name
= u
"Type de paiement")
1133 nature_remuneration
= models
.CharField(max_length
=30,
1134 choices
=NATURE_REMUNERATION_CHOICES
,
1135 verbose_name
= u
"Nature de la rémunération")
1136 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1140 verbose_name
= u
"Type de rémunération"
1141 verbose_name_plural
= u
"Types de rémunération"
1143 def __unicode__(self
):
1145 archive
= u
"(archivé)"
1148 return u
'%s %s' % (self
.nom
, archive
)
1150 class TypeRevalorisation(AUFMetadata
):
1151 """Justification du changement de la Remuneration.
1152 (Actuellement utilisé dans aucun traitement informatique.)
1154 nom
= models
.CharField(max_length
=255)
1158 verbose_name
= u
"Type de revalorisation"
1159 verbose_name_plural
= u
"Types de revalorisation"
1161 def __unicode__(self
):
1162 return u
'%s' % (self
.nom
)
1164 class Service(AUFMetadata
):
1165 """Unité administrative où les Postes sont rattachés.
1167 objects
= ServiceManager()
1169 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1170 nom
= models
.CharField(max_length
=255)
1174 verbose_name
= u
"Service"
1175 verbose_name_plural
= u
"Services"
1177 def __unicode__(self
):
1179 archive
= u
"(archivé)"
1182 return u
'%s %s' % (self
.nom
, archive
)
1185 TYPE_ORGANISME_CHOICES
= (
1186 ('MAD', 'Mise à disposition'),
1187 ('DET', 'Détachement'),
1190 class OrganismeBstg(AUFMetadata
):
1191 """Organisation d'où provient un Employe mis à disposition (MAD) de
1192 ou détaché (DET) à l'AUF à titre gratuit.
1194 (BSTG = bien et service à titre gratuit.)
1196 nom
= models
.CharField(max_length
=255)
1197 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1198 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1200 related_name
='organismes_bstg',
1201 null
=True, blank
=True)
1204 ordering
= ['type', 'nom']
1205 verbose_name
= u
"Organisme BSTG"
1206 verbose_name_plural
= u
"Organismes BSTG"
1208 def __unicode__(self
):
1209 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1211 prefix_implantation
= "pays__region"
1212 def get_regions(self
):
1213 return [self
.pays
.region
]
1216 class Statut(AUFMetadata
):
1217 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1220 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.")
1221 nom
= models
.CharField(max_length
=255)
1225 verbose_name
= u
"Statut d'employé"
1226 verbose_name_plural
= u
"Statuts d'employé"
1228 def __unicode__(self
):
1229 return u
'%s : %s' % (self
.code
, self
.nom
)
1232 TYPE_CLASSEMENT_CHOICES
= (
1233 ('S', 'S -Soutien'),
1234 ('T', 'T - Technicien'),
1235 ('P', 'P - Professionel'),
1237 ('D', 'D - Direction'),
1238 ('SO', 'SO - Sans objet [expatriés]'),
1239 ('HG', 'HG - Hors grille [direction]'),
1242 class ClassementManager(models
.Manager
):
1244 Ordonner les spcéfiquement les classements.
1246 def get_query_set(self
):
1247 qs
= super(self
.__class__
, self
).get_query_set()
1248 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1249 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1253 class Classement_(AUFMetadata
):
1254 """Éléments de classement de la
1255 "Grille générique de classement hiérarchique".
1257 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1258 classement dans la grille. Le classement donne le coefficient utilisé dans:
1260 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1262 objects
= ClassementManager()
1265 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1266 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1267 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1268 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1271 # annee # au lieu de date_debut et date_fin
1272 commentaire
= models
.TextField(null
=True, blank
=True)
1276 ordering
= ['type','echelon','degre','coefficient']
1277 verbose_name
= u
"Classement"
1278 verbose_name_plural
= u
"Classements"
1280 def __unicode__(self
):
1281 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1283 class Classement(Classement_
):
1284 __doc__
= Classement_
.__doc__
1287 class TauxChange_(AUFMetadata
):
1288 """Taux de change de la devise vers l'euro (EUR)
1289 pour chaque année budgétaire.
1292 devise
= models
.ForeignKey('Devise', db_column
='devise')
1293 annee
= models
.IntegerField(verbose_name
= u
"Année")
1294 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1298 ordering
= ['-annee', 'devise__code']
1299 verbose_name
= u
"Taux de change"
1300 verbose_name_plural
= u
"Taux de change"
1302 def __unicode__(self
):
1303 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1306 class TauxChange(TauxChange_
):
1307 __doc__
= TauxChange_
.__doc__
1309 class ValeurPointManager(NoDeleteManager
):
1311 def get_query_set(self
):
1312 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1315 class ValeurPoint_(AUFMetadata
):
1316 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1317 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1318 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1320 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1323 actuelles
= ValeurPointManager()
1325 valeur
= models
.FloatField(null
=True)
1326 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1327 implantation
= models
.ForeignKey(ref
.Implantation
,
1328 db_column
='implantation',
1329 related_name
='%(app_label)s_valeur_point')
1331 annee
= models
.IntegerField()
1334 ordering
= ['-annee', 'implantation__nom']
1336 verbose_name
= u
"Valeur du point"
1337 verbose_name_plural
= u
"Valeurs du point"
1339 def __unicode__(self
):
1340 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1343 class ValeurPoint(ValeurPoint_
):
1344 __doc__
= ValeurPoint_
.__doc__
1348 class Devise(AUFMetadata
):
1349 """Devise monétaire.
1352 objects
= DeviseManager()
1354 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1355 code
= models
.CharField(max_length
=10, unique
=True)
1356 nom
= models
.CharField(max_length
=255)
1360 verbose_name
= u
"Devise"
1361 verbose_name_plural
= u
"Devises"
1363 def __unicode__(self
):
1364 return u
'%s - %s' % (self
.code
, self
.nom
)
1366 class TypeContrat(AUFMetadata
):
1369 nom
= models
.CharField(max_length
=255)
1370 nom_long
= models
.CharField(max_length
=255)
1374 verbose_name
= u
"Type de contrat"
1375 verbose_name_plural
= u
"Types de contrat"
1377 def __unicode__(self
):
1378 return u
'%s' % (self
.nom
)
1383 class ResponsableImplantation(AUFMetadata
):
1384 """Le responsable d'une implantation.
1385 Anciennement géré sur le Dossier du responsable.
1387 employe
= models
.ForeignKey('Employe', db_column
='employe',
1389 null
=True, blank
=True)
1390 implantation
= models
.ForeignKey(ref
.Implantation
,
1391 db_column
='implantation', related_name
='+',
1394 def __unicode__(self
):
1395 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1398 ordering
= ['implantation__nom']
1399 verbose_name
= "Responsable d'implantation"
1400 verbose_name_plural
= "Responsables d'implantation"