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 related_name
="pieces")
492 nom
= models
.CharField(verbose_name
="Nom", max_length
=255)
493 fichier
= models
.FileField(verbose_name
="Fichier",
494 upload_to
=employe_piece_dispatch
,
495 storage
=storage_prive
)
499 verbose_name
= u
"Employé pièce"
500 verbose_name_plural
= u
"Employé pièces"
502 def __unicode__(self
):
503 return u
'%s' % (self
.nom
)
505 class EmployeCommentaire(Commentaire
):
506 employe
= models
.ForeignKey('Employe', db_column
='employe',
510 verbose_name
= u
"Employé commentaire"
511 verbose_name_plural
= u
"Employé commentaires"
514 LIEN_PARENTE_CHOICES
= (
515 ('Conjoint', 'Conjoint'),
516 ('Conjointe', 'Conjointe'),
521 class AyantDroit(AUFMetadata
):
522 """Personne en relation avec un Employe.
525 nom
= models
.CharField(max_length
=255)
526 prenom
= models
.CharField(max_length
=255,
527 verbose_name
= u
"Prénom",)
528 nom_affichage
= models
.CharField(max_length
=255,
529 verbose_name
= u
"Nom d'affichage",
530 null
=True, blank
=True)
531 nationalite
= models
.ForeignKey(ref
.Pays
, to_field
='code',
532 db_column
='nationalite',
533 related_name
='ayantdroits_nationalite',
534 verbose_name
= u
"Nationalité",
535 null
=True, blank
=True)
536 date_naissance
= models
.DateField(verbose_name
= u
"Date de naissance",
537 help_text
=HELP_TEXT_DATE
,
538 validators
=[validate_date_passee
],
539 null
=True, blank
=True)
540 genre
= models
.CharField(max_length
=1, choices
=GENRE_CHOICES
)
543 employe
= models
.ForeignKey('Employe', db_column
='employe',
544 related_name
='ayantdroits',
545 verbose_name
= u
"Employé")
546 lien_parente
= models
.CharField(max_length
=10,
547 choices
=LIEN_PARENTE_CHOICES
,
548 verbose_name
= u
"Lien de parenté",
549 null
=True, blank
=True)
553 verbose_name
= u
"Ayant droit"
554 verbose_name_plural
= u
"Ayants droit"
556 def __unicode__(self
):
557 return u
'%s %s' % (self
.nom
.upper(), self
.prenom
, )
559 prefix_implantation
= "employe__dossiers__poste__implantation__region"
560 def get_regions(self
):
562 for d
in self
.employe
.dossiers
.all():
563 regions
.append(d
.poste
.implantation
.region
)
567 class AyantDroitCommentaire(Commentaire
):
568 ayant_droit
= models
.ForeignKey('AyantDroit', db_column
='ayant_droit',
574 STATUT_RESIDENCE_CHOICES
= (
576 ('expat', 'Expatrié'),
579 COMPTE_COMPTA_CHOICES
= (
585 class Dossier_(AUFMetadata
, DevisableMixin
):
586 """Le Dossier regroupe les informations relatives à l'occupation
587 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
590 Plusieurs Contrats peuvent être associés au Dossier.
591 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
592 lequel aucun Dossier n'existe est un poste vacant.
595 objects
= DossierManager()
598 statut
= models
.ForeignKey('Statut', related_name
='+', null
=True)
599 organisme_bstg
= models
.ForeignKey('OrganismeBstg',
600 db_column
='organisme_bstg',
602 verbose_name
= u
"Organisme",
603 help_text
="Si détaché (DET) ou \
604 mis à disposition (MAD), \
605 préciser l'organisme.",
606 null
=True, blank
=True)
609 remplacement
= models
.BooleanField(default
=False)
610 remplacement_de
= models
.ForeignKey('self', related_name
='+',
611 help_text
=u
"Taper le nom de l'employé",
612 null
=True, blank
=True)
613 statut_residence
= models
.CharField(max_length
=10, default
='local',
614 verbose_name
= u
"Statut", null
=True,
615 choices
=STATUT_RESIDENCE_CHOICES
)
618 classement
= models
.ForeignKey('Classement', db_column
='classement',
620 null
=True, blank
=True)
621 regime_travail
= models
.DecimalField(max_digits
=12, null
=True,
623 default
=REGIME_TRAVAIL_DEFAULT
,
624 verbose_name
= u
"Régime de travail",
625 help_text
="% du temps complet")
626 regime_travail_nb_heure_semaine
= models
.DecimalField(max_digits
=12,
627 decimal_places
=2, null
=True,
628 default
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT
,
629 verbose_name
=u
"Nb. heures par semaine",
630 help_text
=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
)
632 # Occupation du Poste par cet Employe (anciennement "mandat")
633 date_debut
= models
.DateField(verbose_name
= u
"Date de début d'occupation \
635 date_fin
= models
.DateField(verbose_name
= u
"Date de fin d'occupation \
637 null
=True, blank
=True)
644 ordering
= ['employe__nom', ]
645 verbose_name
= u
"Dossier"
646 verbose_name_plural
= "Dossiers"
648 def salaire_theorique(self
):
649 annee
= date
.today().year
650 coeff
= self
.classement
.coefficient
651 implantation
= self
.poste
.implantation
652 point
= ValeurPoint
.objects
.get(implantation
=implantation
, annee
=annee
)
654 montant
= coeff
* point
.valeur
655 devise
= point
.devise
656 return {'montant':montant
, 'devise':devise
}
658 def __unicode__(self
):
659 poste
= self
.poste
.nom
660 if self
.employe
.genre
== 'F':
661 poste
= self
.poste
.nom_feminin
662 return u
'%s - %s' % (self
.employe
, poste
)
664 prefix_implantation
= "poste__implantation__region"
665 def get_regions(self
):
666 return [self
.poste
.implantation
.region
]
669 def remunerations(self
):
670 key
= "%s_remunerations" % self
._meta
.app_label
671 remunerations
= getattr(self
, key
)
672 return remunerations
.all().order_by('-date_debut')
674 def remunerations_en_cours(self
):
675 q
= Q(date_fin__exact
=None) |
Q(date_fin__gt
=datetime
.date
.today())
676 return self
.remunerations().all().filter(q
).order_by('date_debut')
678 def get_salaire(self
):
680 return [r
for r
in self
.remunerations().order_by('-date_debut') if r
.type_id
== 1][0]
684 def get_salaire_euros(self
):
685 tx
= self
.taux_devise()
686 return (float)(tx
) * (float)(self
.salaire
)
688 def get_remunerations_brutes(self
):
692 4 Indemnité d'expatriation
693 5 Indemnité pour frais
694 6 Indemnité de logement
695 7 Indemnité de fonction
696 8 Indemnité de responsabilité
697 9 Indemnité de transport
698 10 Indemnité compensatrice
699 11 Indemnité de subsistance
700 12 Indemnité différentielle
701 13 Prime d'installation
704 16 Indemnité de départ
705 18 Prime de 13ième mois
708 ids
= [1,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19]
709 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
711 def get_charges_salariales(self
):
713 20 Charges salariales ?
716 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
718 def get_charges_patronales(self
):
720 17 Charges patronales
723 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in ids
]
725 def get_remunerations_tierces(self
):
729 return [r
for r
in self
.remunerations_en_cours().all() if r
.type_id
in (2, )]
733 def get_total_local_charges_salariales(self
):
734 devise
= self
.poste
.get_devise()
736 for r
in self
.get_charges_salariales():
737 if r
.devise
!= devise
:
739 total
+= float(r
.montant
)
742 def get_total_local_charges_patronales(self
):
743 devise
= self
.poste
.get_devise()
745 for r
in self
.get_charges_patronales():
746 if r
.devise
!= devise
:
748 total
+= float(r
.montant
)
751 def get_local_salaire_brut(self
):
753 somme des rémuérations brutes
755 devise
= self
.poste
.get_devise()
757 for r
in self
.get_remunerations_brutes():
758 if r
.devise
!= devise
:
760 total
+= float(r
.montant
)
763 def get_local_salaire_net(self
):
765 salaire brut - charges salariales
767 devise
= self
.poste
.get_devise()
769 for r
in self
.get_charges_salariales():
770 if r
.devise
!= devise
:
772 total_charges
+= float(r
.montant
)
773 return self
.get_local_salaire_brut() - total_charges
775 def get_local_couts_auf(self
):
777 salaire net + charges patronales
779 devise
= self
.poste
.get_devise()
781 for r
in self
.get_charges_patronales():
782 if r
.devise
!= devise
:
784 total_charges
+= float(r
.montant
)
785 return self
.get_local_salaire_net() + total_charges
787 def get_total_local_remunerations_tierces(self
):
788 devise
= self
.poste
.get_devise()
790 for r
in self
.get_remunerations_tierces():
791 if r
.devise
!= devise
:
793 total
+= float(r
.montant
)
798 def get_total_charges_salariales(self
):
800 for r
in self
.get_charges_salariales():
801 total
+= r
.montant_euros()
804 def get_total_charges_patronales(self
):
806 for r
in self
.get_charges_patronales():
807 total
+= r
.montant_euros()
810 def get_salaire_brut(self
):
812 somme des rémuérations brutes
815 for r
in self
.get_remunerations_brutes():
816 total
+= r
.montant_euros()
819 def get_salaire_net(self
):
821 salaire brut - charges salariales
824 for r
in self
.get_charges_salariales():
825 total_charges
+= r
.montant_euros()
826 return self
.get_salaire_brut() - total_charges
828 def get_couts_auf(self
):
830 salaire net + charges patronales
833 for r
in self
.get_charges_patronales():
834 total_charges
+= r
.montant_euros()
835 return self
.get_salaire_net() + total_charges
837 def get_total_remunerations_tierces(self
):
839 for r
in self
.get_remunerations_tierces():
840 total
+= r
.montant_euros()
844 class Dossier(Dossier_
):
845 __doc__
= Dossier_
.__doc__
846 poste
= models
.ForeignKey('%s.Poste' % app_context(),
848 related_name
='%(app_label)s_dossiers',
849 help_text
=u
"Taper le nom du poste ou du type de poste",
851 employe
= models
.ForeignKey('Employe', db_column
='employe',
852 help_text
=u
"Taper le nom de l'employé",
853 related_name
='%(app_label)s_dossiers',
854 verbose_name
=u
"Employé")
855 principal
= models
.BooleanField(verbose_name
=u
"Principal?", default
=True,
856 help_text
=u
"Ce Dossier est pour le principal Poste occupé par l'Employé")
859 class DossierPiece_(models
.Model
):
860 """Documents relatifs au Dossier (à l'occupation de ce poste par employé).
861 Ex.: Lettre de motivation.
863 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_dossierpieces')
864 nom
= models
.CharField(verbose_name
= u
"Nom", max_length
=255)
865 fichier
= models
.FileField(verbose_name
= u
"Fichier",
866 upload_to
=dossier_piece_dispatch
,
867 storage
=storage_prive
)
873 def __unicode__(self
):
874 return u
'%s' % (self
.nom
)
876 class DossierPiece(DossierPiece_
):
879 class DossierCommentaire_(Commentaire
):
880 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='+')
884 class DossierCommentaire(DossierCommentaire_
):
887 class DossierComparaison_(models
.Model
, DevisableMixin
):
889 Photo d'une comparaison salariale au moment de l'embauche.
891 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), related_name
='%(app_label)s_comparaisons')
892 objects
= DossierComparaisonManager()
894 implantation
= models
.ForeignKey(ref
.Implantation
, related_name
="+", null
=True, blank
=True)
895 poste
= models
.CharField(max_length
=255, null
=True, blank
=True)
896 personne
= models
.CharField(max_length
=255, null
=True, blank
=True)
897 montant
= models
.IntegerField(null
=True)
898 devise
= models
.ForeignKey('Devise', related_name
='+', null
=True, blank
=True)
903 def __unicode__(self
):
904 return "%s (%s)" % (self
.poste
, self
.personne
)
907 class DossierComparaison(DossierComparaison_
):
912 class RemunerationMixin(AUFMetadata
):
913 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_remunerations')
915 type = models
.ForeignKey('TypeRemuneration', db_column
='type',
917 verbose_name
= u
"Type de rémunération")
918 type_revalorisation
= models
.ForeignKey('TypeRevalorisation',
919 db_column
='type_revalorisation',
921 verbose_name
= u
"Type de revalorisation",
922 null
=True, blank
=True)
923 montant
= models
.DecimalField(null
=True, blank
=True,
924 default
=0, max_digits
=12, decimal_places
=2)
925 # Annuel (12 mois, 52 semaines, 364 jours?)
926 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
927 # commentaire = precision
928 commentaire
= models
.CharField(max_length
=255, null
=True, blank
=True)
929 # date_debut = anciennement date_effectif
930 date_debut
= models
.DateField(verbose_name
= u
"Date de début",
931 null
=True, blank
=True)
932 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
933 null
=True, blank
=True)
937 ordering
= ['type__nom', '-date_fin']
939 def __unicode__(self
):
940 return u
'%s %s (%s)' % (self
.montant
, self
.devise
.code
, self
.type.nom
)
942 class Remuneration_(RemunerationMixin
, DevisableMixin
):
943 """Structure de rémunération (données budgétaires) en situation normale
944 pour un Dossier. Si un Evenement existe, utiliser la structure de
945 rémunération EvenementRemuneration de cet événement.
948 def montant_mois(self
):
949 return round(self
.montant
/ 12, 2)
951 def montant_avec_regime(self
):
952 return round(self
.montant
* (self
.dossier
.regime_travail
/100), 2)
954 def montant_euro_mois(self
):
955 return round(self
.montant_euros() / 12, 2)
957 def __unicode__(self
):
959 devise
= self
.devise
.code
962 return "%s %s" % (self
.montant
, devise
)
966 verbose_name
= u
"Rémunération"
967 verbose_name_plural
= u
"Rémunérations"
970 class Remuneration(Remuneration_
):
976 class ContratManager(NoDeleteManager
):
977 def get_query_set(self
):
978 return super(ContratManager
, self
).get_query_set().select_related('dossier', 'dossier__poste')
981 class Contrat_(AUFMetadata
):
982 """Document juridique qui encadre la relation de travail d'un Employe
983 pour un Poste particulier. Pour un Dossier (qui documente cette
984 relation de travail) plusieurs contrats peuvent être associés.
986 objects
= ContratManager()
987 dossier
= models
.ForeignKey('%s.Dossier' % app_context(), db_column
='dossier', related_name
='%(app_label)s_contrats')
988 type_contrat
= models
.ForeignKey('TypeContrat', db_column
='type_contrat',
990 verbose_name
= u
"type de contrat")
991 date_debut
= models
.DateField(verbose_name
= u
"Date de début")
992 date_fin
= models
.DateField(verbose_name
= u
"Date de fin",
993 null
=True, blank
=True)
994 fichier
= models
.FileField(verbose_name
= u
"Fichier",
995 upload_to
=contrat_dispatch
,
996 storage
=storage_prive
,
997 null
=True, blank
=True)
1001 ordering
= ['dossier__employe__nom']
1002 verbose_name
= u
"Contrat"
1003 verbose_name_plural
= u
"Contrats"
1005 def __unicode__(self
):
1006 return u
'%s - %s' % (self
.dossier
, self
.id)
1008 class Contrat(Contrat_
):
1014 #class Evenement_(AUFMetadata):
1015 # """Un Evenement sert à déclarer une situation temporaire (exceptionnelle)
1016 # d'un Dossier qui vient altérer des informations normales liées à un Dossier
1017 # (ex.: la Remuneration).
1019 # Ex.: congé de maternité, maladie...
1021 # Lors de ces situations exceptionnelles, l'Employe a un régime de travail
1022 # différent et une rémunération en conséquence. On souhaite toutefois
1023 # conserver le Dossier intact afin d'éviter une re-saisie des données lors
1024 # du retour à la normale.
1026 # dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier',
1028 # nom = models.CharField(max_length=255)
1029 # date_debut = models.DateField(verbose_name = u"Date de début")
1030 # date_fin = models.DateField(verbose_name = u"Date de fin",
1031 # null=True, blank=True)
1035 # ordering = ['nom']
1036 # verbose_name = u"Évènement"
1037 # verbose_name_plural = u"Évènements"
1039 # def __unicode__(self):
1040 # return u'%s' % (self.nom)
1043 #class Evenement(Evenement_):
1044 # __doc__ = Evenement_.__doc__
1047 #class EvenementRemuneration_(RemunerationMixin):
1048 # """Structure de rémunération liée à un Evenement qui remplace
1049 # temporairement la Remuneration normale d'un Dossier, pour toute la durée
1052 # evenement = models.ForeignKey("Evenement", db_column='evenement',
1054 # verbose_name = u"Évènement")
1055 # # TODO : le champ dossier hérité de Remuneration doit être dérivé
1056 # # de l'Evenement associé
1060 # ordering = ['evenement', 'type__nom', '-date_fin']
1061 # verbose_name = u"Évènement - rémunération"
1062 # verbose_name_plural = u"Évènements - rémunérations"
1065 #class EvenementRemuneration(EvenementRemuneration_):
1066 # __doc__ = EvenementRemuneration_.__doc__
1072 #class EvenementRemuneration(EvenementRemuneration_):
1073 # __doc__ = EvenementRemuneration_.__doc__
1074 # TODO? class ContratPiece(models.Model):
1079 class FamilleEmploi(AUFMetadata
):
1080 """Catégorie utilisée dans la gestion des Postes.
1081 Catégorie supérieure à TypePoste.
1083 nom
= models
.CharField(max_length
=255)
1087 verbose_name
= u
"Famille d'emploi"
1088 verbose_name_plural
= u
"Familles d'emploi"
1090 def __unicode__(self
):
1091 return u
'%s' % (self
.nom
)
1093 class TypePoste(AUFMetadata
):
1094 """Catégorie de Poste.
1096 nom
= models
.CharField(max_length
=255)
1097 nom_feminin
= models
.CharField(max_length
=255,
1098 verbose_name
= u
"Nom féminin")
1100 is_responsable
= models
.BooleanField(default
=False,
1101 verbose_name
= u
"Poste de responsabilité")
1102 famille_emploi
= models
.ForeignKey('FamilleEmploi',
1103 db_column
='famille_emploi',
1105 verbose_name
= u
"famille d'emploi")
1109 verbose_name
= u
"Type de poste"
1110 verbose_name_plural
= u
"Types de poste"
1112 def __unicode__(self
):
1113 return u
'%s' % (self
.nom
)
1116 TYPE_PAIEMENT_CHOICES
= (
1117 (u
'Régulier', u
'Régulier'),
1118 (u
'Ponctuel', u
'Ponctuel'),
1121 NATURE_REMUNERATION_CHOICES
= (
1122 (u
'Accessoire', u
'Accessoire'),
1123 (u
'Charges', u
'Charges'),
1124 (u
'Indemnité', u
'Indemnité'),
1125 (u
'RAS', u
'Rémunération autre source'),
1126 (u
'Traitement', u
'Traitement'),
1129 class TypeRemuneration(AUFMetadata
):
1130 """Catégorie de Remuneration.
1132 objects
= TypeRemunerationManager()
1134 nom
= models
.CharField(max_length
=255)
1135 type_paiement
= models
.CharField(max_length
=30,
1136 choices
=TYPE_PAIEMENT_CHOICES
,
1137 verbose_name
= u
"Type de paiement")
1138 nature_remuneration
= models
.CharField(max_length
=30,
1139 choices
=NATURE_REMUNERATION_CHOICES
,
1140 verbose_name
= u
"Nature de la rémunération")
1141 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1145 verbose_name
= u
"Type de rémunération"
1146 verbose_name_plural
= u
"Types de rémunération"
1148 def __unicode__(self
):
1150 archive
= u
"(archivé)"
1153 return u
'%s %s' % (self
.nom
, archive
)
1155 class TypeRevalorisation(AUFMetadata
):
1156 """Justification du changement de la Remuneration.
1157 (Actuellement utilisé dans aucun traitement informatique.)
1159 nom
= models
.CharField(max_length
=255)
1163 verbose_name
= u
"Type de revalorisation"
1164 verbose_name_plural
= u
"Types de revalorisation"
1166 def __unicode__(self
):
1167 return u
'%s' % (self
.nom
)
1169 class Service(AUFMetadata
):
1170 """Unité administrative où les Postes sont rattachés.
1172 objects
= ServiceManager()
1174 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1175 nom
= models
.CharField(max_length
=255)
1179 verbose_name
= u
"Service"
1180 verbose_name_plural
= u
"Services"
1182 def __unicode__(self
):
1184 archive
= u
"(archivé)"
1187 return u
'%s %s' % (self
.nom
, archive
)
1190 TYPE_ORGANISME_CHOICES
= (
1191 ('MAD', 'Mise à disposition'),
1192 ('DET', 'Détachement'),
1195 class OrganismeBstg(AUFMetadata
):
1196 """Organisation d'où provient un Employe mis à disposition (MAD) de
1197 ou détaché (DET) à l'AUF à titre gratuit.
1199 (BSTG = bien et service à titre gratuit.)
1201 nom
= models
.CharField(max_length
=255)
1202 type = models
.CharField(max_length
=10, choices
=TYPE_ORGANISME_CHOICES
)
1203 pays
= models
.ForeignKey(ref
.Pays
, to_field
='code',
1205 related_name
='organismes_bstg',
1206 null
=True, blank
=True)
1209 ordering
= ['type', 'nom']
1210 verbose_name
= u
"Organisme BSTG"
1211 verbose_name_plural
= u
"Organismes BSTG"
1213 def __unicode__(self
):
1214 return u
'%s (%s)' % (self
.nom
, self
.get_type_display())
1216 prefix_implantation
= "pays__region"
1217 def get_regions(self
):
1218 return [self
.pays
.region
]
1221 class Statut(AUFMetadata
):
1222 """Statut de l'Employe dans le cadre d'un Dossier particulier.
1225 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.")
1226 nom
= models
.CharField(max_length
=255)
1230 verbose_name
= u
"Statut d'employé"
1231 verbose_name_plural
= u
"Statuts d'employé"
1233 def __unicode__(self
):
1234 return u
'%s : %s' % (self
.code
, self
.nom
)
1237 TYPE_CLASSEMENT_CHOICES
= (
1238 ('S', 'S -Soutien'),
1239 ('T', 'T - Technicien'),
1240 ('P', 'P - Professionel'),
1242 ('D', 'D - Direction'),
1243 ('SO', 'SO - Sans objet [expatriés]'),
1244 ('HG', 'HG - Hors grille [direction]'),
1247 class ClassementManager(models
.Manager
):
1249 Ordonner les spcéfiquement les classements.
1251 def get_query_set(self
):
1252 qs
= super(self
.__class__
, self
).get_query_set()
1253 qs
= qs
.extra(select
={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'})
1254 qs
= qs
.extra(order_by
=('ponderation', 'echelon', 'degre', ))
1258 class Classement_(AUFMetadata
):
1259 """Éléments de classement de la
1260 "Grille générique de classement hiérarchique".
1262 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
1263 classement dans la grille. Le classement donne le coefficient utilisé dans:
1265 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1267 objects
= ClassementManager()
1270 type = models
.CharField(max_length
=10, choices
=TYPE_CLASSEMENT_CHOICES
)
1271 echelon
= models
.IntegerField(verbose_name
=u
"Échelon", blank
=True, default
=0)
1272 degre
= models
.IntegerField(verbose_name
=u
"Degré", blank
=True, default
=0)
1273 coefficient
= models
.FloatField(default
=0, verbose_name
=u
"Coefficient",
1276 # annee # au lieu de date_debut et date_fin
1277 commentaire
= models
.TextField(null
=True, blank
=True)
1281 ordering
= ['type','echelon','degre','coefficient']
1282 verbose_name
= u
"Classement"
1283 verbose_name_plural
= u
"Classements"
1285 def __unicode__(self
):
1286 return u
'%s.%s.%s' % (self
.type, self
.echelon
, self
.degre
, )
1288 class Classement(Classement_
):
1289 __doc__
= Classement_
.__doc__
1292 class TauxChange_(AUFMetadata
):
1293 """Taux de change de la devise vers l'euro (EUR)
1294 pour chaque année budgétaire.
1297 devise
= models
.ForeignKey('Devise', db_column
='devise')
1298 annee
= models
.IntegerField(verbose_name
= u
"Année")
1299 taux
= models
.FloatField(verbose_name
= u
"Taux vers l'euro")
1303 ordering
= ['-annee', 'devise__code']
1304 verbose_name
= u
"Taux de change"
1305 verbose_name_plural
= u
"Taux de change"
1307 def __unicode__(self
):
1308 return u
'%s : %s € (%s)' % (self
.devise
, self
.taux
, self
.annee
)
1311 class TauxChange(TauxChange_
):
1312 __doc__
= TauxChange_
.__doc__
1314 class ValeurPointManager(NoDeleteManager
):
1316 def get_query_set(self
):
1317 return super(ValeurPointManager
, self
).get_query_set().select_related('devise', 'implantation')
1320 class ValeurPoint_(AUFMetadata
):
1321 """Utile pour connaître, pour un Dossier, le salaire de base théorique lié
1322 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
1323 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
1325 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1328 actuelles
= ValeurPointManager()
1330 valeur
= models
.FloatField(null
=True)
1331 devise
= models
.ForeignKey('Devise', db_column
='devise', related_name
='+',)
1332 implantation
= models
.ForeignKey(ref
.Implantation
,
1333 db_column
='implantation',
1334 related_name
='%(app_label)s_valeur_point')
1336 annee
= models
.IntegerField()
1339 ordering
= ['-annee', 'implantation__nom']
1341 verbose_name
= u
"Valeur du point"
1342 verbose_name_plural
= u
"Valeurs du point"
1344 def __unicode__(self
):
1345 return u
'%s %s %s [%s] %s' % (self
.devise
.code
, self
.annee
, self
.valeur
, self
.implantation
.nom_court
, self
.devise
.nom
)
1348 class ValeurPoint(ValeurPoint_
):
1349 __doc__
= ValeurPoint_
.__doc__
1353 class Devise(AUFMetadata
):
1354 """Devise monétaire.
1357 objects
= DeviseManager()
1359 archive
= models
.BooleanField(verbose_name
=u
"Archivé", default
=False)
1360 code
= models
.CharField(max_length
=10, unique
=True)
1361 nom
= models
.CharField(max_length
=255)
1365 verbose_name
= u
"Devise"
1366 verbose_name_plural
= u
"Devises"
1368 def __unicode__(self
):
1369 return u
'%s - %s' % (self
.code
, self
.nom
)
1371 class TypeContrat(AUFMetadata
):
1374 nom
= models
.CharField(max_length
=255)
1375 nom_long
= models
.CharField(max_length
=255)
1379 verbose_name
= u
"Type de contrat"
1380 verbose_name_plural
= u
"Types de contrat"
1382 def __unicode__(self
):
1383 return u
'%s' % (self
.nom
)
1388 class ResponsableImplantationProxy(ref
.Implantation
):
1391 verbose_name
= u
"Responsable d'implantation"
1392 verbose_name_plural
= u
"Responsables d'implantation"
1395 class ResponsableImplantation(models
.Model
):
1396 """Le responsable d'une implantation.
1397 Anciennement géré sur le Dossier du responsable.
1399 employe
= models
.ForeignKey('Employe', db_column
='employe',
1401 null
=True, blank
=True)
1402 implantation
= models
.OneToOneField("ResponsableImplantationProxy",
1403 db_column
='implantation', related_name
='responsable',
1406 def __unicode__(self
):
1407 return u
'%s : %s' % (self
.implantation
, self
.employe
)
1410 ordering
= ['implantation__nom']
1411 verbose_name
= "Responsable d'implantation"
1412 verbose_name_plural
= "Responsables d'implantation"