Merge branch 'release/1.7' into dev
[auf_rh_dae.git] / project / rh / models.py
CommitLineData
e9bbd6ba 1# -=- encoding: utf-8 -=-
2
a4125771 3import datetime
c267f20c 4from datetime import date
ca1a7b76 5from decimal import Decimal
c267f20c 6
d104b0ae 7import reversion
fa1f7426
EMS
8from auf.django.emploi.models import \
9 GENRE_CHOICES, SITUATION_CHOICES # devrait plutot être dans references
fa1f7426 10from auf.django.references import models as ref
e8b6a20c 11from django.contrib.auth.models import User
d104b0ae
EMS
12from django.core.files.storage import FileSystemStorage
13from django.db import models
14from django.db.models import Q
343cfd9c 15from django.db.models.signals import post_save, pre_save
d104b0ae 16from django.conf import settings
c267f20c 17
75f0e87b 18from project.rh.change_list import \
fa1f7426
EMS
19 RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, STATUT_INACTIF, \
20 STATUT_FUTUR
e8b6a20c 21from project import groups
15659516
BS
22from project.rh.managers import (
23 PosteManager,
24 DossierManager,
25 EmployeManager,
26 DossierComparaisonManager,
27 PosteComparaisonManager,
28 ContratManager,
29 RemunerationManager,
30 ArchivableManager,
31 )
32
33
e0a465f2
BS
34TWOPLACES = Decimal('0.01')
35
75f0e87b 36from project.rh.validators import validate_date_passee
a4125771 37
52f4c1e7
OL
38# import pour relocaliser le modèle selon la convention (models.py pour
39# introspection)
edbc9e37 40from project.rh.historique import ModificationTraite
a4125771 41
2d4d2fcf 42# Constantes
4047b783 43HELP_TEXT_DATE = "format: jj-mm-aaaa"
ca1a7b76
EMS
44REGIME_TRAVAIL_DEFAULT = Decimal('100.00')
45REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = Decimal('35.00')
fa1f7426
EMS
46REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT = \
47 "Saisir le nombre d'heure de travail à temps complet (100%), " \
48 "sans tenir compte du régime de travail"
2d4d2fcf 49
83b7692b 50# Upload de fichiers
ca1a7b76 51storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT,
83b7692b 52 base_url=settings.PRIVE_MEDIA_URL)
53
fa1f7426 54
a6ed66f9
OL
55class RemunIntegrityException(Exception):
56 pass
57
83b7692b 58def poste_piece_dispatch(instance, filename):
fa1f7426
EMS
59 path = "%s/poste/%s/%s" % (
60 instance._meta.app_label, instance.poste_id, filename
61 )
83b7692b 62 return path
63
fa1f7426 64
83b7692b 65def dossier_piece_dispatch(instance, filename):
fa1f7426
EMS
66 path = "%s/dossier/%s/%s" % (
67 instance._meta.app_label, instance.dossier_id, filename
68 )
83b7692b 69 return path
70
fa1f7426 71
5ea6b5bb 72def employe_piece_dispatch(instance, filename):
fa1f7426
EMS
73 path = "%s/employe/%s/%s" % (
74 instance._meta.app_label, instance.employe_id, filename
75 )
5ea6b5bb 76 return path
77
fa1f7426 78
4ba73558 79def contrat_dispatch(instance, filename):
fa1f7426
EMS
80 path = "%s/contrat/%s/%s" % (
81 instance._meta.app_label, instance.dossier_id, filename
82 )
4ba73558
OL
83 return path
84
5f5a4f06 85
52f4c1e7
OL
86class DateActiviteMixin(models.Model):
87 """
88 Mixin pour mettre à jour l'activité d'un modèle
89 """
90 class Meta:
91 abstract = True
92 date_creation = models.DateTimeField(auto_now_add=True,
93 null=True, blank=True,
94 verbose_name=u"Date de création",)
95 date_modification = models.DateTimeField(auto_now=True,
96 null=True, blank=True,
97 verbose_name=u"Date de modification",)
98
99
7013d234
EMS
100class Archivable(models.Model):
101 archive = models.BooleanField(u'archivé', default=False)
102
103 objects = ArchivableManager()
156d6b4d 104 avec_archives = models.Manager()
7013d234
EMS
105
106 class Meta:
107 abstract = True
108
109
e84c8ef1
OL
110class DevisableMixin(object):
111
112 def get_annee_pour_taux_devise(self):
5a165e95 113 return datetime.datetime.now().year
e84c8ef1 114
03ff41e3
OL
115 def taux_devise(self, devise=None):
116 if devise is None:
117 devise = self.devise
118
119 if devise is None:
e84c8ef1 120 return None
03ff41e3 121 if devise.code == "EUR":
e84c8ef1
OL
122 return 1
123
124 annee = self.get_annee_pour_taux_devise()
6e6bc910
EMS
125 taux = TauxChange.objects.filter(devise=devise, annee__lte=annee) \
126 .order_by('-annee')
127 return taux[0].taux
e84c8ef1 128
e0a465f2 129 def montant_euros_float(self):
e84c8ef1
OL
130 try:
131 taux = self.taux_devise()
132 except Exception, e:
133 return e
134 if not taux:
135 return None
e0a465f2
BS
136 return float(self.montant) * float(taux)
137
138 def montant_euros(self):
139 return int(round(self.montant_euros_float(), 2))
e84c8ef1
OL
140
141
45066657 142class Commentaire(models.Model):
2d4d2fcf 143 texte = models.TextField()
fa1f7426
EMS
144 owner = models.ForeignKey(
145 'auth.User', db_column='owner', related_name='+',
146 verbose_name=u"Commentaire de"
147 )
45066657
EMS
148 date_creation = models.DateTimeField(
149 u'date', auto_now_add=True, blank=True, null=True
150 )
ca1a7b76 151
2d4d2fcf 152 class Meta:
153 abstract = True
6e4600ef 154 ordering = ['-date_creation']
ca1a7b76 155
6e4600ef 156 def __unicode__(self):
157 return u'%s' % (self.texte)
07b40eda 158
83b7692b 159
160### POSTE
161
162POSTE_APPEL_CHOICES = (
163 ('interne', 'Interne'),
164 ('externe', 'Externe'),
165)
166
fa1f7426 167
52f4c1e7 168class Poste_( DateActiviteMixin, models.Model,):
fa1f7426
EMS
169 """
170 Un Poste est un emploi (job) à combler dans une implantation.
6e4600ef 171 Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé.
172 Si on veut recruter 2 jardiniers, 2 Postes distincts existent.
173 """
1f2979b8
OL
174
175 objects = PosteManager()
176
83b7692b 177 # Identification
fa1f7426
EMS
178 nom = models.CharField(u"Titre du poste", max_length=255)
179 nom_feminin = models.CharField(
180 u"Titre du poste (au féminin)", max_length=255, null=True
181 )
182 implantation = models.ForeignKey(
183 ref.Implantation,
184 help_text=u"Taper le nom de l'implantation ou sa région",
185 db_column='implantation', related_name='+'
186 )
187 type_poste = models.ForeignKey(
188 'TypePoste', db_column='type_poste',
189 help_text=u"Taper le nom du type de poste", related_name='+',
190 null=True, verbose_name=u"type de poste"
191 )
192 service = models.ForeignKey(
f7badf51 193 'Service', db_column='service', related_name='%(app_label)s_postes',
fa1f7426
EMS
194 verbose_name=u"direction/service/pôle support", null=True
195 )
196 responsable = models.ForeignKey(
197 'Poste', db_column='responsable',
198 related_name='+', null=True,
199 help_text=u"Taper le nom du poste ou du type de poste",
200 verbose_name=u"Poste du responsable"
201 )
202
83b7692b 203 # Contrat
fa1f7426
EMS
204 regime_travail = models.DecimalField(
205 u"temps de travail", max_digits=12, decimal_places=2,
206 default=REGIME_TRAVAIL_DEFAULT, null=True,
207 help_text="% du temps complet"
208 )
209 regime_travail_nb_heure_semaine = models.DecimalField(
210 u"nb. heures par semaine", max_digits=12, decimal_places=2,
211 null=True, default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
212 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
213 )
83b7692b 214
215 # Recrutement
fa1f7426
EMS
216 local = models.NullBooleanField(
217 u"local", default=True, null=True, blank=True
218 )
219 expatrie = models.NullBooleanField(
220 u"expatrié", default=False, null=True, blank=True
221 )
8277a35b 222 mise_a_disposition = models.NullBooleanField(
fa1f7426
EMS
223 u"mise à disposition", null=True, default=False
224 )
225 appel = models.CharField(
226 u"Appel à candidature", max_length=10, null=True,
227 choices=POSTE_APPEL_CHOICES, default='interne'
228 )
83b7692b 229
230 # Rémunération
fa1f7426
EMS
231 classement_min = models.ForeignKey(
232 'Classement', db_column='classement_min', related_name='+',
233 null=True, blank=True
234 )
235 classement_max = models.ForeignKey(
236 'Classement', db_column='classement_max', related_name='+',
237 null=True, blank=True
238 )
239 valeur_point_min = models.ForeignKey(
240 'ValeurPoint',
241 help_text=u"Taper le code ou le nom de l'implantation",
242 db_column='valeur_point_min', related_name='+', null=True,
243 blank=True
244 )
245 valeur_point_max = models.ForeignKey(
246 'ValeurPoint',
247 help_text=u"Taper le code ou le nom de l'implantation",
248 db_column='valeur_point_max', related_name='+', null=True,
249 blank=True
250 )
251 devise_min = models.ForeignKey(
252 'Devise', db_column='devise_min', null=True, related_name='+'
253 )
254 devise_max = models.ForeignKey(
255 'Devise', db_column='devise_max', null=True, related_name='+'
256 )
257 salaire_min = models.DecimalField(
7ad3b930 258 max_digits=12, decimal_places=2, default=0,
fa1f7426
EMS
259 )
260 salaire_max = models.DecimalField(
7ad3b930 261 max_digits=12, decimal_places=2, default=0,
fa1f7426
EMS
262 )
263 indemn_min = models.DecimalField(
7ad3b930 264 max_digits=12, decimal_places=2, default=0,
fa1f7426
EMS
265 )
266 indemn_max = models.DecimalField(
7ad3b930 267 max_digits=12, decimal_places=2, default=0,
fa1f7426
EMS
268 )
269 autre_min = models.DecimalField(
7ad3b930 270 max_digits=12, decimal_places=2, default=0,
fa1f7426
EMS
271 )
272 autre_max = models.DecimalField(
7ad3b930 273 max_digits=12, decimal_places=2, default=0,
fa1f7426 274 )
83b7692b 275
276 # Comparatifs de rémunération
fa1f7426
EMS
277 devise_comparaison = models.ForeignKey(
278 'Devise', null=True, blank=True, db_column='devise_comparaison',
279 related_name='+'
280 )
281 comp_locale_min = models.DecimalField(
282 max_digits=12, decimal_places=2, null=True, blank=True
283 )
284 comp_locale_max = models.DecimalField(
285 max_digits=12, decimal_places=2, null=True, blank=True
286 )
287 comp_universite_min = models.DecimalField(
288 max_digits=12, decimal_places=2, null=True, blank=True
289 )
290 comp_universite_max = models.DecimalField(
291 max_digits=12, decimal_places=2, null=True, blank=True
292 )
293 comp_fonctionpub_min = models.DecimalField(
294 max_digits=12, decimal_places=2, null=True, blank=True
295 )
296 comp_fonctionpub_max = models.DecimalField(
297 max_digits=12, decimal_places=2, null=True, blank=True
298 )
299 comp_ong_min = models.DecimalField(
300 max_digits=12, decimal_places=2, null=True, blank=True
301 )
302 comp_ong_max = models.DecimalField(
303 max_digits=12, decimal_places=2, null=True, blank=True
304 )
305 comp_autre_min = models.DecimalField(
306 max_digits=12, decimal_places=2, null=True, blank=True
307 )
308 comp_autre_max = models.DecimalField(
309 max_digits=12, decimal_places=2, null=True, blank=True
310 )
83b7692b 311
312 # Justification
6e4600ef 313 justification = models.TextField(null=True, blank=True)
83b7692b 314
2d4d2fcf 315 # Autres Metadata
fa1f7426 316 date_debut = models.DateField(
8bb6f549
EMS
317 u"date de début", help_text=HELP_TEXT_DATE, null=True, blank=True,
318 db_index=True
fa1f7426
EMS
319 )
320 date_fin = models.DateField(
8bb6f549
EMS
321 u"date de fin", help_text=HELP_TEXT_DATE, null=True, blank=True,
322 db_index=True
fa1f7426 323 )
6e4600ef 324
325 class Meta:
37868f0b 326 abstract = True
6e4600ef 327 ordering = ['implantation__nom', 'nom']
c1195471
OL
328 verbose_name = u"Poste"
329 verbose_name_plural = u"Postes"
30e3cf7c 330 ordering = ["nom"]
6e4600ef 331
83b7692b 332 def __unicode__(self):
fa1f7426
EMS
333 representation = u'%s - %s [%s]' % (
334 self.implantation, self.nom, self.id
335 )
8c1ae2b3 336 return representation
ca1a7b76 337
b0cf30b8 338 prefix_implantation = "implantation__zone_administrative"
fa1f7426 339
b0cf30b8
EMS
340 def get_zones_administratives(self):
341 return [self.implantation.zone_administrative]
4c53dda4 342
552d0db7 343 def get_devise(self):
fa1f7426
EMS
344 vp = ValeurPoint.objects.filter(
345 implantation=self.implantation, devise__archive=False
346 ).order_by('annee')
523c8c0f
EMS
347 if len(vp) > 0:
348 return vp[0].devise
349 else:
350 return Devise.objects.get(code='EUR')
4c53dda4 351
fa1f7426 352
4c53dda4
OL
353class Poste(Poste_):
354 __doc__ = Poste_.__doc__
355
356 # meta dématérialisation : pour permettre le filtrage
fa1f7426 357 vacant = models.NullBooleanField(u"vacant", null=True, blank=True)
4c53dda4 358
8c1ae2b3 359 def is_vacant(self):
23102192
DB
360 vacant = True
361 if self.occupe_par():
362 vacant = False
363 return vacant
364
365 def occupe_par(self):
fa1f7426
EMS
366 """
367 Retourne la liste d'employé occupant ce poste.
23102192
DB
368 Généralement, retourne une liste d'un élément.
369 Si poste inoccupé, retourne liste vide.
4c53dda4 370 UTILISE pour mettre a jour le flag vacant
23102192 371 """
fa1f7426 372 return [
45066657
EMS
373 d.employe
374 for d in self.rh_dossiers.exclude(date_fin__lt=date.today())
fa1f7426 375 ]
83b7692b 376
d104b0ae
EMS
377reversion.register(Poste, format='xml', follow=[
378 'rh_financements', 'rh_pieces', 'rh_comparaisons_internes',
45066657 379 'commentaires'
d104b0ae
EMS
380])
381
37868f0b 382
83b7692b 383POSTE_FINANCEMENT_CHOICES = (
384 ('A', 'A - Frais de personnel'),
385 ('B', 'B - Projet(s)-Titre(s)'),
386 ('C', 'C - Autre')
387)
388
6e7c919b
NC
389
390class PosteFinancement_(models.Model):
fa1f7426
EMS
391 """
392 Pour un Poste, structure d'informations décrivant comment on prévoit
6e4600ef 393 financer ce Poste.
394 """
83b7692b 395 type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES)
fa1f7426
EMS
396 pourcentage = models.DecimalField(
397 max_digits=12, decimal_places=2,
398 help_text="ex.: 33.33 % (décimale avec point)"
399 )
83b7692b 400 commentaire = models.TextField(
fa1f7426
EMS
401 help_text="Spécifiez la source de financement."
402 )
83b7692b 403
404 class Meta:
6e7c919b 405 abstract = True
83b7692b 406 ordering = ['type']
ca1a7b76 407
6e4600ef 408 def __unicode__(self):
a184c555 409 return u'%s : %s %%' % (self.type, self.pourcentage)
83b7692b 410
abf91905
JPC
411 def choix(self):
412 return u"%s" % dict(POSTE_FINANCEMENT_CHOICES)[self.type]
413
6e7c919b
NC
414
415class PosteFinancement(PosteFinancement_):
4ba84959
EMS
416 poste = models.ForeignKey(
417 Poste, db_column='poste', related_name='rh_financements'
418 )
6e7c919b 419
d104b0ae
EMS
420reversion.register(PosteFinancement, format='xml')
421
6e7c919b 422
317ce433 423class PostePiece_(models.Model):
fa1f7426
EMS
424 """
425 Documents relatifs au Poste.
7abc6d45 426 Ex.: Description de poste
427 """
fa1f7426
EMS
428 nom = models.CharField(u"Nom", max_length=255)
429 fichier = models.FileField(
430 u"Fichier", upload_to=poste_piece_dispatch, storage=storage_prive
431 )
83b7692b 432
6e4600ef 433 class Meta:
317ce433 434 abstract = True
6e4600ef 435 ordering = ['nom']
ca1a7b76 436
6e4600ef 437 def __unicode__(self):
438 return u'%s' % (self.nom)
439
fa1f7426 440
317ce433 441class PostePiece(PostePiece_):
4ba84959
EMS
442 poste = models.ForeignKey(
443 Poste, db_column='poste', related_name='rh_pieces'
444 )
317ce433 445
d104b0ae
EMS
446reversion.register(PostePiece, format='xml')
447
fa1f7426 448
45066657 449class PosteComparaison_(models.Model, DevisableMixin):
068d1462 450 """
fa1f7426
EMS
451 De la même manière qu'un dossier, un poste peut-être comparé à un autre
452 poste.
068d1462 453 """
16b1454e
OL
454 objects = PosteComparaisonManager()
455
fa1f7426
EMS
456 implantation = models.ForeignKey(
457 ref.Implantation, null=True, blank=True, related_name="+"
458 )
459 nom = models.CharField(u"Poste", max_length=255, null=True, blank=True)
068d1462 460 montant = models.IntegerField(null=True)
fa1f7426
EMS
461 devise = models.ForeignKey(
462 "Devise", related_name='+', null=True, blank=True
463 )
1d0f4eef 464
317ce433
OL
465 class Meta:
466 abstract = True
467
783e077a
JPC
468 def __unicode__(self):
469 return self.nom
470
fa1f7426 471
317ce433 472class PosteComparaison(PosteComparaison_):
4ba84959
EMS
473 poste = models.ForeignKey(
474 Poste, related_name='rh_comparaisons_internes'
475 )
476
d104b0ae
EMS
477reversion.register(PosteComparaison, format='xml')
478
fa1f7426 479
4ba84959 480class PosteCommentaire(Commentaire):
fa1f7426 481 poste = models.ForeignKey(
4ba84959 482 Poste, db_column='poste', related_name='commentaires'
fa1f7426 483 )
83b7692b 484
d104b0ae 485reversion.register(PosteCommentaire, format='xml')
2d4d2fcf 486
83b7692b 487### EMPLOYÉ/PERSONNE
e9bbd6ba 488
45066657 489class Employe(models.Model):
fa1f7426
EMS
490 """
491 Personne occupant ou ayant occupé un Poste. Un Employe aura autant de
6e4600ef 492 Dossiers qu'il occupe ou a occupé de Postes.
ca1a7b76
EMS
493
494 Cette classe aurait pu avantageusement s'appeler Personne car la notion
6e4600ef 495 d'employé n'a pas de sens si aucun Dossier n'existe pour une personne.
496 """
6fb68b2f
DB
497
498 objects = EmployeManager()
d104b0ae 499
9afaa55e 500 # Identification
e9bbd6ba 501 nom = models.CharField(max_length=255)
fa1f7426
EMS
502 prenom = models.CharField(u"prénom", max_length=255)
503 nom_affichage = models.CharField(
504 u"nom d'affichage", max_length=255, null=True, blank=True
505 )
506 nationalite = models.ForeignKey(
507 ref.Pays, to_field='code', db_column='nationalite',
508 related_name='employes_nationalite', verbose_name=u"nationalité",
509 blank=True, null=True
510 )
511 date_naissance = models.DateField(
512 u"date de naissance", help_text=HELP_TEXT_DATE,
513 validators=[validate_date_passee], null=True, blank=True
514 )
2d4d2fcf 515 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
ca1a7b76 516
9afaa55e 517 # Infos personnelles
fa1f7426
EMS
518 situation_famille = models.CharField(
519 u"situation familiale", max_length=1, choices=SITUATION_CHOICES,
520 null=True, blank=True
521 )
522 date_entree = models.DateField(
523 u"date d'entrée à l'AUF", help_text=HELP_TEXT_DATE, null=True,
524 blank=True
525 )
ca1a7b76 526
9afaa55e 527 # Coordonnées
fa1f7426
EMS
528 tel_domicile = models.CharField(
529 u"tél. domicile", max_length=255, null=True, blank=True
530 )
531 tel_cellulaire = models.CharField(
532 u"tél. cellulaire", max_length=255, null=True, blank=True
533 )
e9bbd6ba 534 adresse = models.CharField(max_length=255, null=True, blank=True)
e9bbd6ba 535 ville = models.CharField(max_length=255, null=True, blank=True)
536 province = models.CharField(max_length=255, null=True, blank=True)
537 code_postal = models.CharField(max_length=255, null=True, blank=True)
fa1f7426
EMS
538 pays = models.ForeignKey(
539 ref.Pays, to_field='code', db_column='pays',
540 related_name='employes', null=True, blank=True
541 )
89a8df07
EMS
542 courriel_perso = models.EmailField(
543 u'adresse courriel personnelle', blank=True
544 )
9afaa55e 545
a45e414b 546 # meta dématérialisation : pour permettre le filtrage
fa1f7426 547 nb_postes = models.IntegerField(u"nombre de postes", null=True, blank=True)
a45e414b 548
6e4600ef 549 class Meta:
fa1f7426 550 ordering = ['nom', 'prenom']
c1195471
OL
551 verbose_name = u"Employé"
552 verbose_name_plural = u"Employés"
ca1a7b76 553
9afaa55e 554 def __unicode__(self):
a2c3ad52 555 return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id)
ca1a7b76 556
d04d084c 557 def civilite(self):
558 civilite = u''
559 if self.genre.upper() == u'M':
560 civilite = u'M.'
561 elif self.genre.upper() == u'F':
562 civilite = u'Mme'
563 return civilite
ca1a7b76 564
5ea6b5bb 565 def url_photo(self):
fa1f7426
EMS
566 """
567 Retourne l'URL du service retournant la photo de l'Employe.
5ea6b5bb 568 Équivalent reverse url 'rh_photo' avec id en param.
569 """
570 from django.core.urlresolvers import reverse
fa1f7426 571 return reverse('rh_photo', kwargs={'id': self.id})
ca1a7b76 572
c267f20c 573 def dossiers_passes(self):
6bee05ff 574 params = {KEY_STATUT: STATUT_INACTIF, }
da202402 575 search = RechercheTemporelle(params, Dossier)
6bee05ff 576 search.purge_params(params)
dcd1b959
OL
577 q = search.get_q_temporel(self.rh_dossiers)
578 return self.rh_dossiers.filter(q)
ca1a7b76 579
c267f20c 580 def dossiers_futurs(self):
6bee05ff 581 params = {KEY_STATUT: STATUT_FUTUR, }
da202402 582 search = RechercheTemporelle(params, Dossier)
6bee05ff 583 search.purge_params(params)
dcd1b959
OL
584 q = search.get_q_temporel(self.rh_dossiers)
585 return self.rh_dossiers.filter(q)
ca1a7b76 586
c267f20c 587 def dossiers_encours(self):
6bee05ff 588 params = {KEY_STATUT: STATUT_ACTIF, }
da202402 589 search = RechercheTemporelle(params, Dossier)
6bee05ff 590 search.purge_params(params)
dcd1b959
OL
591 q = search.get_q_temporel(self.rh_dossiers)
592 return self.rh_dossiers.filter(q)
ca1a7b76 593
9b818715
BS
594 def dossier_principal_pour_annee(self):
595 return self.dossier_principal(pour_annee=True)
596
597 def dossier_principal(self, pour_annee=False):
db265492
EMS
598 """
599 Retourne le dossier principal (ou le plus ancien si il y en a
600 plusieurs)
9b818715
BS
601
602 Si pour_annee == True, retourne le ou les dossiers principaux
603 pour l'annee en cours, sinon, le ou les dossiers principaux
604 pour la journee en cours.
605
606 TODO: (Refactoring possible): Utiliser meme logique dans
607 dae/templatetags/dae.py
5db1c5a3 608 """
9b818715
BS
609
610 today = date.today()
611 if pour_annee:
612 year = today.year
613 year_start = date(year, 1, 1)
614 year_end = date(year, 12, 31)
43c2929b 615
9b818715
BS
616 try:
617 dossier = self.rh_dossiers.filter(
618 (Q(date_debut__lte=year_end, date_fin__isnull=True) |
619 Q(date_debut__isnull=True, date_fin__gte=year_start) |
620 Q(date_debut__lte=year_end, date_fin__gte=year_start) |
621 Q(date_debut__isnull=True, date_fin__isnull=True)) &
622 Q(principal=True)).order_by('date_debut')[0]
623 except IndexError, Dossier.DoesNotExist:
624 dossier = None
625 return dossier
626 else:
627 try:
628 dossier = self.rh_dossiers.filter(
629 (Q(date_debut__lte=today, date_fin__isnull=True) |
630 Q(date_debut__isnull=True, date_fin__gte=today) |
631 Q(date_debut__lte=today, date_fin__gte=today) |
632 Q(date_debut__isnull=True, date_fin__isnull=True)) &
633 Q(principal=True)).order_by('date_debut')[0]
634 except IndexError, Dossier.DoesNotExist:
635 dossier = None
636 return dossier
637
5db1c5a3 638
35c0c2fe 639 def postes_encours(self):
640 postes_encours = set()
641 for d in self.dossiers_encours():
642 postes_encours.add(d.poste)
643 return postes_encours
ca1a7b76 644
35c0c2fe 645 def poste_principal(self):
65f9fac8 646 """
647 Retourne le Poste du premier Dossier créé parmi les Dossiers en cours.
ca1a7b76 648 Idée derrière :
65f9fac8 649 si on ajout d'autre Dossiers, c'est pour des Postes secondaires.
650 """
5db1c5a3 651 # DEPRECATED : on a maintenant Dossier.principal
65f9fac8 652 poste = Poste.objects.none()
653 try:
654 poste = self.dossiers_encours().order_by('date_debut')[0].poste
655 except:
656 pass
657 return poste
9afaa55e 658
b0cf30b8
EMS
659 prefix_implantation = \
660 "rh_dossiers__poste__implantation__zone_administrative"
fa1f7426 661
b0cf30b8
EMS
662 def get_zones_administratives(self):
663 return [
664 d.poste.implantation.zone_administrative
665 for d in self.dossiers.all()
666 ]
aff1a4c6 667
d104b0ae 668reversion.register(Employe, format='xml', follow=[
45066657 669 'pieces', 'commentaires', 'ayantdroits'
d104b0ae
EMS
670])
671
aff1a4c6 672
7abc6d45 673class EmployePiece(models.Model):
fa1f7426
EMS
674 """
675 Documents relatifs à un employé.
7abc6d45 676 Ex.: CV...
677 """
fa1f7426
EMS
678 employe = models.ForeignKey(
679 'Employe', db_column='employe', related_name="pieces",
680 verbose_name=u"employé"
681 )
682 nom = models.CharField(max_length=255)
683 fichier = models.FileField(
684 u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive
685 )
7abc6d45 686
6e4600ef 687 class Meta:
688 ordering = ['nom']
f9e54d59
PP
689 verbose_name = u"Employé pièce"
690 verbose_name_plural = u"Employé pièces"
691
6e4600ef 692 def __unicode__(self):
693 return u'%s' % (self.nom)
694
d104b0ae
EMS
695reversion.register(EmployePiece, format='xml')
696
fa1f7426 697
07b40eda 698class EmployeCommentaire(Commentaire):
fa1f7426 699 employe = models.ForeignKey(
d104b0ae 700 'Employe', db_column='employe', related_name='commentaires'
fa1f7426 701 )
9afaa55e 702
b343eb3d
PP
703 class Meta:
704 verbose_name = u"Employé commentaire"
705 verbose_name_plural = u"Employé commentaires"
706
d104b0ae
EMS
707reversion.register(EmployeCommentaire, format='xml')
708
2d4d2fcf 709
e9bbd6ba 710LIEN_PARENTE_CHOICES = (
711 ('Conjoint', 'Conjoint'),
712 ('Conjointe', 'Conjointe'),
713 ('Fille', 'Fille'),
714 ('Fils', 'Fils'),
715)
716
fa1f7426 717
45066657 718class AyantDroit(models.Model):
fa1f7426
EMS
719 """
720 Personne en relation avec un Employe.
6e4600ef 721 """
9afaa55e 722 # Identification
e9bbd6ba 723 nom = models.CharField(max_length=255)
fa1f7426
EMS
724 prenom = models.CharField(u"prénom", max_length=255)
725 nom_affichage = models.CharField(
726 u"nom d'affichage", max_length=255, null=True, blank=True
727 )
728 nationalite = models.ForeignKey(
729 ref.Pays, to_field='code', db_column='nationalite',
730 related_name='ayantdroits_nationalite',
731 verbose_name=u"nationalité", null=True, blank=True
732 )
733 date_naissance = models.DateField(
734 u"Date de naissance", help_text=HELP_TEXT_DATE,
735 validators=[validate_date_passee], null=True, blank=True
736 )
2d4d2fcf 737 genre = models.CharField(max_length=1, choices=GENRE_CHOICES)
ca1a7b76 738
9afaa55e 739 # Relation
fa1f7426
EMS
740 employe = models.ForeignKey(
741 'Employe', db_column='employe', related_name='ayantdroits',
742 verbose_name=u"Employé"
743 )
744 lien_parente = models.CharField(
745 u"lien de parenté", max_length=10, choices=LIEN_PARENTE_CHOICES,
746 null=True, blank=True
747 )
6e4600ef 748
749 class Meta:
a2c3ad52 750 ordering = ['nom', ]
c1195471
OL
751 verbose_name = u"Ayant droit"
752 verbose_name_plural = u"Ayants droit"
ca1a7b76 753
6e4600ef 754 def __unicode__(self):
2de29065 755 return u'%s %s' % (self.nom.upper(), self.prenom, )
83b7692b 756
b0cf30b8
EMS
757 prefix_implantation = \
758 "employe__dossiers__poste__implantation__zone_administrative"
fa1f7426 759
b0cf30b8
EMS
760 def get_zones_administratives(self):
761 return [
762 d.poste.implantation.zone_administrative
763 for d in self.employe.dossiers.all()
764 ]
aff1a4c6 765
d104b0ae
EMS
766reversion.register(AyantDroit, format='xml', follow=['commentaires'])
767
aff1a4c6 768
07b40eda 769class AyantDroitCommentaire(Commentaire):
fa1f7426 770 ayant_droit = models.ForeignKey(
d104b0ae 771 'AyantDroit', db_column='ayant_droit', related_name='commentaires'
fa1f7426 772 )
83b7692b 773
d104b0ae
EMS
774reversion.register(AyantDroitCommentaire, format='xml')
775
2d4d2fcf 776
83b7692b 777### DOSSIER
778
779STATUT_RESIDENCE_CHOICES = (
780 ('local', 'Local'),
781 ('expat', 'Expatrié'),
782)
783
784COMPTE_COMPTA_CHOICES = (
785 ('coda', 'CODA'),
786 ('scs', 'SCS'),
787 ('aucun', 'Aucun'),
788)
789
fa1f7426 790
52f4c1e7 791class Dossier_(DateActiviteMixin, models.Model, DevisableMixin,):
fa1f7426
EMS
792 """
793 Le Dossier regroupe les informations relatives à l'occupation
6e4600ef 794 d'un Poste par un Employe. Un seul Dossier existe par Poste occupé
795 par un Employe.
ca1a7b76 796
6e4600ef 797 Plusieurs Contrats peuvent être associés au Dossier.
798 Une structure de Remuneration est rattachée au Dossier. Un Poste pour
799 lequel aucun Dossier n'existe est un poste vacant.
800 """
3f5cbabe
OL
801
802 objects = DossierManager()
803
63e17dff 804 # TODO: OneToOne ??
eb6bf568 805 statut = models.ForeignKey('Statut', related_name='+', null=True)
fa1f7426
EMS
806 organisme_bstg = models.ForeignKey(
807 'OrganismeBstg', db_column='organisme_bstg', related_name='+',
808 verbose_name=u"organisme",
809 help_text=(
810 u"Si détaché (DET) ou mis à disposition (MAD), "
811 u"préciser l'organisme."
812 ), null=True, blank=True
813 )
ca1a7b76 814
83b7692b 815 # Recrutement
2d4d2fcf 816 remplacement = models.BooleanField(default=False)
fa1f7426
EMS
817 remplacement_de = models.ForeignKey(
818 'self', related_name='+', help_text=u"Taper le nom de l'employé",
819 null=True, blank=True
820 )
821 statut_residence = models.CharField(
822 u"statut", max_length=10, default='local', null=True,
823 choices=STATUT_RESIDENCE_CHOICES
824 )
ca1a7b76 825
83b7692b 826 # Rémunération
fa1f7426
EMS
827 classement = models.ForeignKey(
828 'Classement', db_column='classement', related_name='+', null=True,
829 blank=True
830 )
831 regime_travail = models.DecimalField(
832 u"régime de travail", max_digits=12, null=True, decimal_places=2,
833 default=REGIME_TRAVAIL_DEFAULT, help_text="% du temps complet"
834 )
835 regime_travail_nb_heure_semaine = models.DecimalField(
836 u"nb. heures par semaine", max_digits=12,
837 decimal_places=2, null=True,
838 default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT,
839 help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT
840 )
7abc6d45 841
842 # Occupation du Poste par cet Employe (anciennement "mandat")
8bb6f549
EMS
843 date_debut = models.DateField(
844 u"date de début d'occupation de poste", db_index=True
845 )
fa1f7426 846 date_fin = models.DateField(
8bb6f549
EMS
847 u"Date de fin d'occupation de poste", null=True, blank=True,
848 db_index=True
fa1f7426 849 )
ca1a7b76 850
84934747
BS
851 # Meta-data:
852 est_cadre = models.BooleanField(
9623a926 853 u"Est un cadre?",
84934747
BS
854 default=False,
855 )
856
2d4d2fcf 857 # Comptes
858 # TODO?
ca1a7b76 859
6e4600ef 860 class Meta:
37868f0b 861 abstract = True
49449367 862 ordering = ['employe__nom', ]
3f5f3898 863 verbose_name = u"Dossier"
8c1ae2b3 864 verbose_name_plural = "Dossiers"
ca1a7b76 865
65f9fac8 866 def salaire_theorique(self):
867 annee = date.today().year
868 coeff = self.classement.coefficient
869 implantation = self.poste.implantation
870 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
ca1a7b76 871
65f9fac8 872 montant = coeff * point.valeur
873 devise = point.devise
fa1f7426 874 return {'montant': montant, 'devise': devise}
ca1a7b76 875
83b7692b 876 def __unicode__(self):
8c1ae2b3 877 poste = self.poste.nom
878 if self.employe.genre == 'F':
ca1a7b76 879 poste = self.poste.nom_feminin
8c1ae2b3 880 return u'%s - %s' % (self.employe, poste)
83b7692b 881
b0cf30b8 882 prefix_implantation = "poste__implantation__zone_administrative"
fa1f7426 883
b0cf30b8
EMS
884 def get_zones_administratives(self):
885 return [self.poste.implantation.zone_administrative]
aff1a4c6 886
3ebc0952 887 def remunerations(self):
838bc59d
OL
888 key = "%s_remunerations" % self._meta.app_label
889 remunerations = getattr(self, key)
890 return remunerations.all().order_by('-date_debut')
3ebc0952 891
02e69aa2 892 def remunerations_en_cours(self):
838bc59d
OL
893 q = Q(date_fin__exact=None) | Q(date_fin__gt=datetime.date.today())
894 return self.remunerations().all().filter(q).order_by('date_debut')
02e69aa2 895
09aa8374
OL
896 def get_salaire(self):
897 try:
fa1f7426
EMS
898 return [r for r in self.remunerations().order_by('-date_debut')
899 if r.type_id == 1][0]
09aa8374
OL
900 except:
901 return None
3ebc0952 902
838bc59d
OL
903 def get_salaire_euros(self):
904 tx = self.taux_devise()
905 return (float)(tx) * (float)(self.salaire)
906
907 def get_remunerations_brutes(self):
908 """
909 1 Salaire de base
910 3 Indemnité de base
911 4 Indemnité d'expatriation
912 5 Indemnité pour frais
913 6 Indemnité de logement
914 7 Indemnité de fonction
915 8 Indemnité de responsabilité
916 9 Indemnité de transport
917 10 Indemnité compensatrice
918 11 Indemnité de subsistance
919 12 Indemnité différentielle
920 13 Prime d'installation
921 14 Billet d'avion
922 15 Déménagement
923 16 Indemnité de départ
924 18 Prime de 13ième mois
925 19 Prime d'intérim
926 """
fa1f7426
EMS
927 ids = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
928 return [r for r in self.remunerations_en_cours().all()
929 if r.type_id in ids]
838bc59d
OL
930
931 def get_charges_salariales(self):
932 """
933 20 Charges salariales ?
934 """
fa1f7426
EMS
935 ids = [20]
936 return [r for r in self.remunerations_en_cours().all()
937 if r.type_id in ids]
838bc59d 938
838bc59d
OL
939 def get_charges_patronales(self):
940 """
941 17 Charges patronales
942 """
fa1f7426
EMS
943 ids = [17]
944 return [r for r in self.remunerations_en_cours().all()
945 if r.type_id in ids]
838bc59d 946
552d0db7
OL
947 def get_remunerations_tierces(self):
948 """
949 2 Salaire MAD
950 """
fa1f7426
EMS
951 return [r for r in self.remunerations_en_cours().all()
952 if r.type_id in (2,)]
552d0db7
OL
953
954 # DEVISE LOCALE
955
956 def get_total_local_charges_salariales(self):
bc17b82c 957 devise = self.poste.get_devise()
552d0db7
OL
958 total = 0.0
959 for r in self.get_charges_salariales():
bc17b82c
OL
960 if r.devise != devise:
961 return None
962 total += float(r.montant)
552d0db7
OL
963 return total
964
965 def get_total_local_charges_patronales(self):
bc17b82c 966 devise = self.poste.get_devise()
552d0db7
OL
967 total = 0.0
968 for r in self.get_charges_patronales():
bc17b82c
OL
969 if r.devise != devise:
970 return None
552d0db7
OL
971 total += float(r.montant)
972 return total
973
974 def get_local_salaire_brut(self):
975 """
976 somme des rémuérations brutes
977 """
978 devise = self.poste.get_devise()
979 total = 0.0
980 for r in self.get_remunerations_brutes():
981 if r.devise != devise:
982 return None
983 total += float(r.montant)
984 return total
985
986 def get_local_salaire_net(self):
987 """
988 salaire brut - charges salariales
989 """
990 devise = self.poste.get_devise()
991 total_charges = 0.0
992 for r in self.get_charges_salariales():
993 if r.devise != devise:
994 return None
995 total_charges += float(r.montant)
996 return self.get_local_salaire_brut() - total_charges
997
998 def get_local_couts_auf(self):
999 """
1000 salaire net + charges patronales
1001 """
1002 devise = self.poste.get_devise()
1003 total_charges = 0.0
1004 for r in self.get_charges_patronales():
1005 if r.devise != devise:
1006 return None
1007 total_charges += float(r.montant)
1008 return self.get_local_salaire_net() + total_charges
1009
1010 def get_total_local_remunerations_tierces(self):
1011 devise = self.poste.get_devise()
1012 total = 0.0
1013 for r in self.get_remunerations_tierces():
1014 if r.devise != devise:
1015 return None
1016 total += float(r.montant)
1017 return total
1018
1019 # DEVISE EURO
1020
1021 def get_total_charges_salariales(self):
1022 total = 0.0
1023 for r in self.get_charges_salariales():
1024 total += r.montant_euros()
1025 return total
1026
838bc59d
OL
1027 def get_total_charges_patronales(self):
1028 total = 0.0
1029 for r in self.get_charges_patronales():
1030 total += r.montant_euros()
1031 return total
1032
1033 def get_salaire_brut(self):
1034 """
1035 somme des rémuérations brutes
1036 """
1037 total = 0.0
1038 for r in self.get_remunerations_brutes():
1039 total += r.montant_euros()
1040 return total
1041
1042 def get_salaire_net(self):
1043 """
1044 salaire brut - charges salariales
1045 """
1046 total_charges = 0.0
1047 for r in self.get_charges_salariales():
1048 total_charges += r.montant_euros()
1049 return self.get_salaire_brut() - total_charges
1050
1051 def get_couts_auf(self):
1052 """
1053 salaire net + charges patronales
1054 """
1055 total_charges = 0.0
1056 for r in self.get_charges_patronales():
1057 total_charges += r.montant_euros()
1058 return self.get_salaire_net() + total_charges
1059
838bc59d
OL
1060 def get_total_remunerations_tierces(self):
1061 total = 0.0
1062 for r in self.get_remunerations_tierces():
1063 total += r.montant_euros()
1064 return total
1065
5db1c5a3
DB
1066 def premier_contrat(self):
1067 """contrat avec plus petite date de début"""
1068 try:
db265492
EMS
1069 contrat = self.rh_contrats.exclude(date_debut=None) \
1070 .order_by('date_debut')[0]
5db1c5a3
DB
1071 except IndexError, Contrat.DoesNotExist:
1072 contrat = None
1073 return contrat
db265492 1074
5db1c5a3
DB
1075 def dernier_contrat(self):
1076 """contrat avec plus grande date de fin"""
1077 try:
db265492
EMS
1078 contrat = self.rh_contrats.exclude(date_debut=None) \
1079 .order_by('-date_debut')[0]
5db1c5a3
DB
1080 except IndexError, Contrat.DoesNotExist:
1081 contrat = None
1082 return contrat
1083
bfb5e43e
EMS
1084 def actif(self):
1085 today = date.today()
1086 return (self.date_debut is None or self.date_debut <= today) \
1087 and (self.date_fin is None or self.date_fin >= today) \
1088 and not (self.date_fin is None and self.date_debut is None)
1089
22343fe7 1090
37868f0b
NC
1091class Dossier(Dossier_):
1092 __doc__ = Dossier_.__doc__
4ba84959
EMS
1093 poste = models.ForeignKey(
1094 Poste, db_column='poste', related_name='rh_dossiers',
0b0545bd 1095 help_text=u"Taper le nom du poste ou du type de poste",
4ba84959 1096 )
fa1f7426
EMS
1097 employe = models.ForeignKey(
1098 'Employe', db_column='employe',
1099 help_text=u"Taper le nom de l'employé",
4ba84959
EMS
1100 related_name='rh_dossiers', verbose_name=u"employé"
1101 )
fa1f7426 1102 principal = models.BooleanField(
c1f5d83c 1103 u"dossier principal", default=True,
fa1f7426
EMS
1104 help_text=(
1105 u"Ce dossier est pour le principal poste occupé par l'employé"
1106 )
1107 )
343cfd9c 1108
37868f0b 1109
d104b0ae
EMS
1110reversion.register(Dossier, format='xml', follow=[
1111 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
1112 'rh_contrats', 'commentaires'
1113])
1114
37868f0b 1115
9388fbac
BS
1116class RHDossierClassementRecord(models.Model):
1117 classement = models.ForeignKey(
1118 'Classement',
1119 related_name='classement_records',
1120 )
1121 dossier = models.ForeignKey(
1122 'Dossier',
1123 related_name='classement_records',
1124 )
1125 date_debut = models.DateField(
1126 u"date de début",
1127 help_text=HELP_TEXT_DATE,
1128 null=True,
1129 blank=True,
1130 db_index=True
1131 )
1132 date_fin = models.DateField(
1133 u"date de fin",
1134 help_text=HELP_TEXT_DATE,
1135 null=True,
1136 blank=True,
1137 db_index=True
1138 )
1139
1140 def __unicode__(self):
1141 return self.classement.__unicode__()
1142
1143 class Meta:
1144 verbose_name = u"Element d'historique de classement"
1145 verbose_name_plural = u"Historique de classement"
1146
1147 @classmethod
1148 def post_save_handler(cls,
1149 sender,
1150 instance,
1151 created,
1152 using,
1153 **kw):
1154
990c9e12
BS
1155 today = date.today()
1156 previous_record = None
9388fbac
BS
1157 previous_classement = None
1158 has_changed = False
1159
990c9e12
BS
1160 # Premièrement, pour les nouvelles instances:
1161 if created:
182fb8f9 1162 if not instance.classement:
990c9e12
BS
1163 return
1164 else:
1165 cls.objects.create(
1166 date_debut=instance.date_debut,
1167 classement=instance.classement,
9388fbac 1168 dossier=instance,
9388fbac 1169 )
990c9e12 1170 return
9388fbac 1171
990c9e12 1172 # Deuxièmement, pour les instances existantes:
9388fbac 1173
990c9e12
BS
1174 # Détermine si:
1175 # 1. Est-ce que le classement a changé?
1176 # 2. Est-ce qu'une historique de classement existe déjà
1177 try:
1178 previous_record = cls.objects.get(
9388fbac 1179 dossier=instance,
990c9e12
BS
1180 classement=instance.before_save.classement,
1181 date_fin=None,
9388fbac 1182 )
990c9e12
BS
1183 except cls.DoesNotExist:
1184 if instance.before_save.classement:
1185 # Il était censé avoir une historique de classement
1186 # donc on le créé.
1187 previous_record = cls.objects.create(
1188 date_debut=instance.before_save.date_debut,
1189 classement=instance.before_save.classement,
1190 dossier=instance,
1191 )
1192 previous_classement = instance.before_save.classement
9388fbac 1193
990c9e12
BS
1194 else:
1195 previous_classement = previous_record.classement
9388fbac 1196
990c9e12
BS
1197 has_changed = (
1198 instance.classement !=
1199 previous_classement
1200 )
1201
1202 # Cas aucun changement:
1203 if not has_changed:
1204 return
1205
1206 else:
1207 # Classement a changé
1208 if previous_record:
1209 previous_record.date_fin = today
1210 previous_record.save()
9388fbac 1211
990c9e12
BS
1212 if instance.classement:
1213 cls.objects.create(
1214 date_debut=today,
1215 classement=instance.classement,
1216 dossier=instance,
1217 )
9388fbac
BS
1218
1219
fc917340 1220class DossierPiece_(models.Model):
fa1f7426
EMS
1221 """
1222 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
7abc6d45 1223 Ex.: Lettre de motivation.
1224 """
fa1f7426
EMS
1225 nom = models.CharField(max_length=255)
1226 fichier = models.FileField(
1227 upload_to=dossier_piece_dispatch, storage=storage_prive
1228 )
83b7692b 1229
6e4600ef 1230 class Meta:
fc917340 1231 abstract = True
6e4600ef 1232 ordering = ['nom']
ca1a7b76 1233
6e4600ef 1234 def __unicode__(self):
1235 return u'%s' % (self.nom)
1236
fa1f7426 1237
fc917340 1238class DossierPiece(DossierPiece_):
fa1f7426 1239 dossier = models.ForeignKey(
4ba84959 1240 Dossier, db_column='dossier', related_name='rh_dossierpieces'
fa1f7426
EMS
1241 )
1242
d104b0ae 1243reversion.register(DossierPiece, format='xml')
fc917340 1244
4ba84959
EMS
1245class DossierCommentaire(Commentaire):
1246 dossier = models.ForeignKey(
1247 Dossier, db_column='dossier', related_name='commentaires'
1248 )
fc917340 1249
d104b0ae
EMS
1250reversion.register(DossierCommentaire, format='xml')
1251
fa1f7426 1252
e84c8ef1 1253class DossierComparaison_(models.Model, DevisableMixin):
1d0f4eef
OL
1254 """
1255 Photo d'une comparaison salariale au moment de l'embauche.
1256 """
16b1454e
OL
1257 objects = DossierComparaisonManager()
1258
fa1f7426
EMS
1259 implantation = models.ForeignKey(
1260 ref.Implantation, related_name="+", null=True, blank=True
1261 )
1d0f4eef
OL
1262 poste = models.CharField(max_length=255, null=True, blank=True)
1263 personne = models.CharField(max_length=255, null=True, blank=True)
1264 montant = models.IntegerField(null=True)
fa1f7426
EMS
1265 devise = models.ForeignKey(
1266 'Devise', related_name='+', null=True, blank=True
1267 )
1d0f4eef 1268
fc917340
OL
1269 class Meta:
1270 abstract = True
1271
3b14230d
OL
1272 def __unicode__(self):
1273 return "%s (%s)" % (self.poste, self.personne)
1274
1d0f4eef 1275
fc917340 1276class DossierComparaison(DossierComparaison_):
4ba84959
EMS
1277 dossier = models.ForeignKey(
1278 Dossier, related_name='rh_comparaisons'
1279 )
2d4d2fcf 1280
d104b0ae
EMS
1281reversion.register(DossierComparaison, format='xml')
1282
fa1f7426 1283
07b40eda 1284### RÉMUNÉRATION
ca1a7b76 1285
45066657 1286class RemunerationMixin(models.Model):
fa1f7426 1287
9afaa55e 1288 # Identification
fa1f7426
EMS
1289 type = models.ForeignKey(
1290 'TypeRemuneration', db_column='type', related_name='+',
1291 verbose_name=u"type de rémunération"
1292 )
1293 type_revalorisation = models.ForeignKey(
1294 'TypeRevalorisation', db_column='type_revalorisation',
1295 related_name='+', verbose_name=u"type de revalorisation",
1296 null=True, blank=True
1297 )
1298 montant = models.DecimalField(
d104b0ae 1299 null=True, blank=True, max_digits=12, decimal_places=2
fa1f7426
EMS
1300 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1301 devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
1302
2d4d2fcf 1303 # commentaire = precision
1304 commentaire = models.CharField(max_length=255, null=True, blank=True)
fa1f7426 1305
2d4d2fcf 1306 # date_debut = anciennement date_effectif
8bb6f549
EMS
1307 date_debut = models.DateField(
1308 u"date de début", null=True, blank=True, db_index=True
1309 )
1310 date_fin = models.DateField(
1311 u"date de fin", null=True, blank=True, db_index=True
1312 )
ca1a7b76
EMS
1313
1314 class Meta:
2d4d2fcf 1315 abstract = True
6e4600ef 1316 ordering = ['type__nom', '-date_fin']
ca1a7b76 1317
6e4600ef 1318 def __unicode__(self):
1319 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
ca1a7b76 1320
fa1f7426 1321
e84c8ef1 1322class Remuneration_(RemunerationMixin, DevisableMixin):
fa1f7426
EMS
1323 """
1324 Structure de rémunération (données budgétaires) en situation normale
ca1a7b76
EMS
1325 pour un Dossier. Si un Evenement existe, utiliser la structure de
1326 rémunération EvenementRemuneration de cet événement.
2d4d2fcf 1327 """
c3550a05 1328 objects = RemunerationManager()
83b7692b 1329
7d8f6789
BS
1330 @staticmethod
1331 def find_yearly_range(from_date, to_date, year):
1332 today = date.today()
1333 year = year or date.today().year
1334 year_start = date(year, 1, 1)
1335 year_end = date(year, 12, 31)
1336
1337 def constrain_to_year(*dates):
1338 """
1339 S'assure que les dates soient dans le range year_start a
1340 year_end
1341 """
1342 return [min(max(year_start, d), year_end)
1343 for d in dates]
1344
1345 start_date = max(
1346 from_date or year_start, year_start)
1347 end_date = min(
1348 to_date or year_end, year_end)
1349
1350 start_date, end_date = constrain_to_year(start_date, end_date)
1351
1352 jours_annee = (year_end - year_start).days
1353 jours_dates = (end_date - start_date).days
1354 factor = Decimal(str(jours_dates)) / Decimal(str(jours_annee))
1355
1356 return start_date, end_date, factor
1357
1358
e0a465f2
BS
1359 def montant_ajuste_euros(self, annee=None):
1360 """
1361 Le montant ajusté représente le montant annuel, ajusté sur la
1362 période de temps travaillée, multipliée par le ratio de temps
1363 travaillé (en rapport au temps plein).
1364 """
7d8f6789
BS
1365 date_debut, date_fin, factor = self.find_yearly_range(
1366 self.date_debut,
1367 self.date_fin,
1368 annee,
1369 )
1370
1371 montant_euros = Decimal(str(self.montant_euros_float()) or '0')
1372
e0a465f2 1373 if self.type.nature_remuneration != u'Accessoire':
182fb8f9
BS
1374 dossier = getattr(self, 'dossier', None)
1375 if not dossier:
1376 """
1377 Dans le cas d'un DossierComparaisonRemuneration, il
1378 n'y a plus de reference au dossier.
1379 """
1380 regime_travail = REGIME_TRAVAIL_DEFAULT
1381 else:
1382 regime_travail = self.dossier.regime_travail
7d8f6789 1383 return (montant_euros * factor *
182fb8f9 1384 regime_travail / 100)
e0a465f2 1385 else:
7d8f6789
BS
1386 return montant_euros
1387
83b7692b 1388 def montant_mois(self):
1389 return round(self.montant / 12, 2)
1390
626beb4d 1391 def montant_avec_regime(self):
fa1f7426 1392 return round(self.montant * (self.dossier.regime_travail / 100), 2)
626beb4d 1393
83b7692b 1394 def montant_euro_mois(self):
e84c8ef1 1395 return round(self.montant_euros() / 12, 2)
ca1a7b76 1396
9afaa55e 1397 def __unicode__(self):
1398 try:
1399 devise = self.devise.code
1400 except:
1401 devise = "???"
1402 return "%s %s" % (self.montant, devise)
83b7692b 1403
6e7c919b
NC
1404 class Meta:
1405 abstract = True
c1195471
OL
1406 verbose_name = u"Rémunération"
1407 verbose_name_plural = u"Rémunérations"
6e7c919b
NC
1408
1409
1410class Remuneration(Remuneration_):
4ba84959
EMS
1411 dossier = models.ForeignKey(
1412 Dossier, db_column='dossier', related_name='rh_remunerations'
1413 )
6e7c919b 1414
d104b0ae
EMS
1415reversion.register(Remuneration, format='xml')
1416
2d4d2fcf 1417
1418### CONTRATS
c41b7fcc 1419
45066657 1420class Contrat_(models.Model):
fa1f7426
EMS
1421 """
1422 Document juridique qui encadre la relation de travail d'un Employe
ca1a7b76 1423 pour un Poste particulier. Pour un Dossier (qui documente cette
2d4d2fcf 1424 relation de travail) plusieurs contrats peuvent être associés.
1425 """
c41b7fcc 1426 objects = ContratManager()
fa1f7426
EMS
1427 type_contrat = models.ForeignKey(
1428 'TypeContrat', db_column='type_contrat',
1429 verbose_name=u'type de contrat', related_name='+'
1430 )
8bb6f549
EMS
1431 date_debut = models.DateField(
1432 u"date de début", db_index=True
1433 )
1434 date_fin = models.DateField(
1435 u"date de fin", null=True, blank=True, db_index=True
1436 )
fa1f7426
EMS
1437 fichier = models.FileField(
1438 upload_to=contrat_dispatch, storage=storage_prive, null=True,
1439 blank=True
1440 )
6e4600ef 1441
1442 class Meta:
fc917340 1443 abstract = True
a2c3ad52 1444 ordering = ['dossier__employe__nom']
c1195471
OL
1445 verbose_name = u"Contrat"
1446 verbose_name_plural = u"Contrats"
ca1a7b76 1447
6e4600ef 1448 def __unicode__(self):
8c1ae2b3 1449 return u'%s - %s' % (self.dossier, self.id)
fc917340 1450
fa1f7426 1451
fc917340 1452class Contrat(Contrat_):
4ba84959
EMS
1453 dossier = models.ForeignKey(
1454 Dossier, db_column='dossier', related_name='rh_contrats'
1455 )
f31ddfa0 1456
d104b0ae
EMS
1457reversion.register(Contrat, format='xml')
1458
83b7692b 1459
ca1a7b76 1460### RÉFÉRENCES RH
83b7692b 1461
45066657 1462class CategorieEmploi(models.Model):
fa1f7426
EMS
1463 """
1464 Catégorie utilisée dans la gestion des Postes.
6e4600ef 1465 Catégorie supérieure à TypePoste.
1466 """
e9bbd6ba 1467 nom = models.CharField(max_length=255)
ca1a7b76 1468
8c1ae2b3 1469 class Meta:
321fe481 1470 ordering = ('nom',)
7bf28694
EMS
1471 verbose_name = u"catégorie d'emploi"
1472 verbose_name_plural = u"catégories d'emploi"
ca1a7b76 1473
6e4600ef 1474 def __unicode__(self):
321fe481
EMS
1475 return self.nom
1476
d104b0ae
EMS
1477reversion.register(CategorieEmploi, format='xml')
1478
321fe481
EMS
1479
1480class FamilleProfessionnelle(models.Model):
1481 """
1482 Famille professionnelle d'un poste.
1483 """
1484 nom = models.CharField(max_length=100)
1485
1486 class Meta:
1487 ordering = ('nom',)
1488 verbose_name = u'famille professionnelle'
1489 verbose_name_plural = u'familles professionnelles'
1490
1491 def __unicode__(self):
1492 return self.nom
e9bbd6ba 1493
d104b0ae
EMS
1494reversion.register(FamilleProfessionnelle, format='xml')
1495
fa1f7426 1496
15659516 1497class TypePoste(Archivable):
fa1f7426
EMS
1498 """
1499 Catégorie de Poste.
6e4600ef 1500 """
e9bbd6ba 1501 nom = models.CharField(max_length=255)
fa1f7426
EMS
1502 nom_feminin = models.CharField(u"nom féminin", max_length=255)
1503 is_responsable = models.BooleanField(
1504 u"poste de responsabilité", default=False
1505 )
7bf28694
EMS
1506 categorie_emploi = models.ForeignKey(
1507 CategorieEmploi, db_column='categorie_emploi', related_name='+',
1508 verbose_name=u"catégorie d'emploi"
fa1f7426 1509 )
321fe481
EMS
1510 famille_professionnelle = models.ForeignKey(
1511 FamilleProfessionnelle, related_name='types_de_poste',
1512 verbose_name=u"famille professionnelle", blank=True, null=True
1513 )
e9bbd6ba 1514
6e4600ef 1515 class Meta:
1516 ordering = ['nom']
c1195471
OL
1517 verbose_name = u"Type de poste"
1518 verbose_name_plural = u"Types de poste"
ca1a7b76 1519
e9bbd6ba 1520 def __unicode__(self):
6e4600ef 1521 return u'%s' % (self.nom)
e9bbd6ba 1522
d104b0ae
EMS
1523reversion.register(TypePoste, format='xml')
1524
1525
e9bbd6ba 1526TYPE_PAIEMENT_CHOICES = (
a3e3bde0
JPC
1527 (u'Régulier', u'Régulier'),
1528 (u'Ponctuel', u'Ponctuel'),
e9bbd6ba 1529)
1530
1531NATURE_REMUNERATION_CHOICES = (
f40a4829
BS
1532 (u'Traitement', u'Traitements'),
1533 (u'Indemnité', u'Indemnités autres'),
7d8f6789 1534 (u'Charges', u'Charges patronales'),
f40a4829 1535 (u'Accessoire', u'Accessoires'),
a3e3bde0 1536 (u'RAS', u'Rémunération autre source'),
e9bbd6ba 1537)
1538
fa1f7426 1539
7013d234 1540class TypeRemuneration(Archivable):
fa1f7426
EMS
1541 """
1542 Catégorie de Remuneration.
6e4600ef 1543 """
7ba822a6 1544
156d6b4d
BS
1545 objects = models.Manager()
1546 sans_archives = ArchivableManager()
1547
e9bbd6ba 1548 nom = models.CharField(max_length=255)
fa1f7426
EMS
1549 type_paiement = models.CharField(
1550 u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES
1551 )
6bec5651 1552
fa1f7426
EMS
1553 nature_remuneration = models.CharField(
1554 u"nature de la rémunération", max_length=30,
1555 choices=NATURE_REMUNERATION_CHOICES
1556 )
ca1a7b76 1557
8c1ae2b3 1558 class Meta:
1559 ordering = ['nom']
c1195471
OL
1560 verbose_name = u"Type de rémunération"
1561 verbose_name_plural = u"Types de rémunération"
9afaa55e 1562
1563 def __unicode__(self):
7013d234 1564 return self.nom
ca1a7b76 1565
d104b0ae
EMS
1566reversion.register(TypeRemuneration, format='xml')
1567
fa1f7426 1568
15659516 1569class TypeRevalorisation(Archivable):
fa1f7426
EMS
1570 """
1571 Justification du changement de la Remuneration.
6e4600ef 1572 (Actuellement utilisé dans aucun traitement informatique.)
7abc6d45 1573 """
e9bbd6ba 1574 nom = models.CharField(max_length=255)
ca1a7b76 1575
8c1ae2b3 1576 class Meta:
1577 ordering = ['nom']
c1195471
OL
1578 verbose_name = u"Type de revalorisation"
1579 verbose_name_plural = u"Types de revalorisation"
e9bbd6ba 1580
1581 def __unicode__(self):
6e4600ef 1582 return u'%s' % (self.nom)
ca1a7b76 1583
d104b0ae
EMS
1584reversion.register(TypeRevalorisation, format='xml')
1585
fa1f7426 1586
7013d234 1587class Service(Archivable):
fa1f7426
EMS
1588 """
1589 Unité administrative où les Postes sont rattachés.
6e4600ef 1590 """
1591 nom = models.CharField(max_length=255)
ca1a7b76 1592
9afaa55e 1593 class Meta:
1594 ordering = ['nom']
7013d234
EMS
1595 verbose_name = u"service"
1596 verbose_name_plural = u"services"
e9bbd6ba 1597
6e4600ef 1598 def __unicode__(self):
7013d234 1599 return self.nom
6e4600ef 1600
d104b0ae
EMS
1601reversion.register(Service, format='xml')
1602
e9bbd6ba 1603
1604TYPE_ORGANISME_CHOICES = (
1605 ('MAD', 'Mise à disposition'),
1606 ('DET', 'Détachement'),
1607)
1608
fa1f7426 1609
45066657 1610class OrganismeBstg(models.Model):
fa1f7426
EMS
1611 """
1612 Organisation d'où provient un Employe mis à disposition (MAD) de
6e4600ef 1613 ou détaché (DET) à l'AUF à titre gratuit.
ca1a7b76 1614
6e4600ef 1615 (BSTG = bien et service à titre gratuit.)
1616 """
e9bbd6ba 1617 nom = models.CharField(max_length=255)
1618 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
ca1a7b76 1619 pays = models.ForeignKey(ref.Pays, to_field='code',
6e4600ef 1620 db_column='pays',
1621 related_name='organismes_bstg',
1622 null=True, blank=True)
9afaa55e 1623
1624 class Meta:
1625 ordering = ['type', 'nom']
c1195471
OL
1626 verbose_name = u"Organisme BSTG"
1627 verbose_name_plural = u"Organismes BSTG"
9afaa55e 1628
6e4600ef 1629 def __unicode__(self):
8c1ae2b3 1630 return u'%s (%s)' % (self.nom, self.get_type_display())
83b7692b 1631
d104b0ae
EMS
1632reversion.register(OrganismeBstg, format='xml')
1633
aff1a4c6 1634
15659516 1635class Statut(Archivable):
fa1f7426
EMS
1636 """
1637 Statut de l'Employe dans le cadre d'un Dossier particulier.
6e4600ef 1638 """
9afaa55e 1639 # Identification
fa1f7426
EMS
1640 code = models.CharField(
1641 max_length=25, unique=True,
1642 help_text=(
1643 u"Saisir un code court mais lisible pour ce statut : "
1644 u"le code est utilisé pour associer les statuts aux autres "
1645 u"données tout en demeurant plus lisible qu'un identifiant "
1646 u"numérique."
1647 )
1648 )
e9bbd6ba 1649 nom = models.CharField(max_length=255)
e9bbd6ba 1650
6e4600ef 1651 class Meta:
1652 ordering = ['code']
c1195471
OL
1653 verbose_name = u"Statut d'employé"
1654 verbose_name_plural = u"Statuts d'employé"
ca1a7b76 1655
9afaa55e 1656 def __unicode__(self):
1657 return u'%s : %s' % (self.code, self.nom)
1658
d104b0ae
EMS
1659reversion.register(Statut, format='xml')
1660
83b7692b 1661
e9bbd6ba 1662TYPE_CLASSEMENT_CHOICES = (
6e4600ef 1663 ('S', 'S -Soutien'),
1664 ('T', 'T - Technicien'),
1665 ('P', 'P - Professionel'),
1666 ('C', 'C - Cadre'),
1667 ('D', 'D - Direction'),
1668 ('SO', 'SO - Sans objet [expatriés]'),
1669 ('HG', 'HG - Hors grille [direction]'),
e9bbd6ba 1670)
83b7692b 1671
fa1f7426 1672
f40a4829 1673class ClassementManager(models.Manager):
952ecb37
OL
1674 """
1675 Ordonner les spcéfiquement les classements.
1676 """
1677 def get_query_set(self):
f40a4829 1678 qs = super(ClassementManager, self).get_query_set()
fa1f7426
EMS
1679 qs = qs.extra(select={
1680 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1681 })
6559f73b 1682 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
952ecb37
OL
1683 return qs.all()
1684
6e7c919b 1685
f40a4829
BS
1686class ClassementArchivableManager(ClassementManager,
1687 ArchivableManager):
1688 pass
1689
1690
15659516 1691class Classement_(Archivable):
fa1f7426
EMS
1692 """
1693 Éléments de classement de la
6e4600ef 1694 "Grille générique de classement hiérarchique".
ca1a7b76
EMS
1695
1696 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
6e4600ef 1697 classement dans la grille. Le classement donne le coefficient utilisé dans:
1698
1699 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1700 """
952ecb37 1701 objects = ClassementManager()
f40a4829 1702 sans_archives = ClassementArchivableManager()
952ecb37 1703
9afaa55e 1704 # Identification
e9bbd6ba 1705 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
fa1f7426
EMS
1706 echelon = models.IntegerField(u"échelon", blank=True, default=0)
1707 degre = models.IntegerField(u"degré", blank=True, default=0)
d104b0ae 1708 coefficient = models.FloatField(u"coefficient", blank=True, null=True)
fa1f7426 1709
9afaa55e 1710 # Méta
6e4600ef 1711 # annee # au lieu de date_debut et date_fin
1712 commentaire = models.TextField(null=True, blank=True)
ca1a7b76 1713
6e4600ef 1714 class Meta:
6e7c919b 1715 abstract = True
fa1f7426 1716 ordering = ['type', 'echelon', 'degre', 'coefficient']
c1195471
OL
1717 verbose_name = u"Classement"
1718 verbose_name_plural = u"Classements"
e9bbd6ba 1719
1720 def __unicode__(self):
22343fe7 1721 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
e9bbd6ba 1722
fa1f7426 1723
6e7c919b
NC
1724class Classement(Classement_):
1725 __doc__ = Classement_.__doc__
1726
d104b0ae
EMS
1727reversion.register(Classement, format='xml')
1728
6e7c919b 1729
45066657 1730class TauxChange_(models.Model):
fa1f7426
EMS
1731 """
1732 Taux de change de la devise vers l'euro (EUR)
6e4600ef 1733 pour chaque année budgétaire.
7abc6d45 1734 """
9afaa55e 1735 # Identification
8d3e2fff 1736 devise = models.ForeignKey('Devise', db_column='devise')
fa1f7426
EMS
1737 annee = models.IntegerField(u"année")
1738 taux = models.FloatField(u"taux vers l'euro")
6e7c919b 1739
6e4600ef 1740 class Meta:
6e7c919b 1741 abstract = True
8c1ae2b3 1742 ordering = ['-annee', 'devise__code']
c1195471
OL
1743 verbose_name = u"Taux de change"
1744 verbose_name_plural = u"Taux de change"
8bb6f549 1745 unique_together = ('devise', 'annee')
ca1a7b76 1746
6e4600ef 1747 def __unicode__(self):
8c1ae2b3 1748 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
e9bbd6ba 1749
6e7c919b
NC
1750
1751class TauxChange(TauxChange_):
1752 __doc__ = TauxChange_.__doc__
1753
d104b0ae
EMS
1754reversion.register(TauxChange, format='xml')
1755
fa1f7426 1756
45066657 1757class ValeurPointManager(models.Manager):
105dd778 1758
701f3bea 1759 def get_query_set(self):
fa1f7426 1760 return super(ValeurPointManager, self).get_query_set() \
15659516 1761 .select_related('devise', 'implantation')
701f3bea 1762
6e7c919b 1763
45066657 1764class ValeurPoint_(models.Model):
fa1f7426
EMS
1765 """
1766 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
ca1a7b76 1767 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
8c1ae2b3 1768 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
6e4600ef 1769
1770 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1771 """
ca1a7b76 1772
45066657 1773 objects = models.Manager()
09aa8374 1774 actuelles = ValeurPointManager()
701f3bea 1775
8277a35b 1776 valeur = models.FloatField(null=True)
f614ca5c 1777 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
ca1a7b76 1778 implantation = models.ForeignKey(ref.Implantation,
6e7c919b
NC
1779 db_column='implantation',
1780 related_name='%(app_label)s_valeur_point')
9afaa55e 1781 # Méta
e9bbd6ba 1782 annee = models.IntegerField()
9afaa55e 1783
6e4600ef 1784 class Meta:
701f3bea 1785 ordering = ['-annee', 'implantation__nom']
6e7c919b 1786 abstract = True
c1195471
OL
1787 verbose_name = u"Valeur du point"
1788 verbose_name_plural = u"Valeurs du point"
8bb6f549 1789 unique_together = ('implantation', 'annee')
6e0bbb73 1790
9afaa55e 1791 def __unicode__(self):
fa1f7426
EMS
1792 return u'%s %s %s [%s] %s' % (
1793 self.devise.code, self.annee, self.valeur,
1794 self.implantation.nom_court, self.devise.nom
1795 )
6e7c919b
NC
1796
1797
1798class ValeurPoint(ValeurPoint_):
1799 __doc__ = ValeurPoint_.__doc__
1800
d104b0ae
EMS
1801reversion.register(ValeurPoint, format='xml')
1802
e9bbd6ba 1803
7013d234 1804class Devise(Archivable):
6e4600ef 1805 """
fa1f7426
EMS
1806 Devise monétaire.
1807 """
fa1f7426 1808 code = models.CharField(max_length=10, unique=True)
e9bbd6ba 1809 nom = models.CharField(max_length=255)
1810
6e4600ef 1811 class Meta:
1812 ordering = ['code']
7013d234
EMS
1813 verbose_name = u"devise"
1814 verbose_name_plural = u"devises"
ca1a7b76 1815
e9bbd6ba 1816 def __unicode__(self):
1817 return u'%s - %s' % (self.code, self.nom)
1818
d104b0ae
EMS
1819reversion.register(Devise, format='xml')
1820
fa1f7426 1821
15659516 1822class TypeContrat(Archivable):
fa1f7426
EMS
1823 """
1824 Type de contrat.
6e4600ef 1825 """
e9bbd6ba 1826 nom = models.CharField(max_length=255)
6e4600ef 1827 nom_long = models.CharField(max_length=255)
49f9f116 1828
8c1ae2b3 1829 class Meta:
1830 ordering = ['nom']
c1195471
OL
1831 verbose_name = u"Type de contrat"
1832 verbose_name_plural = u"Types de contrat"
8c1ae2b3 1833
9afaa55e 1834 def __unicode__(self):
1835 return u'%s' % (self.nom)
ca1a7b76 1836
d104b0ae
EMS
1837reversion.register(TypeContrat, format='xml')
1838
ca1a7b76 1839
2d4d2fcf 1840### AUTRES
1841
8c8ffc4f 1842class ResponsableImplantationProxy(ref.Implantation):
7bf28694 1843
6fb68b2f
DB
1844 def save(self):
1845 pass
1846
8c8ffc4f 1847 class Meta:
d7bf0cd3 1848 managed = False
8c8ffc4f
OL
1849 proxy = True
1850 verbose_name = u"Responsable d'implantation"
1851 verbose_name_plural = u"Responsables d'implantation"
1852
1853
1854class ResponsableImplantation(models.Model):
fa1f7426
EMS
1855 """
1856 Le responsable d'une implantation.
30be56d5 1857 Anciennement géré sur le Dossier du responsable.
1858 """
fa1f7426
EMS
1859 employe = models.ForeignKey(
1860 'Employe', db_column='employe', related_name='+', null=True,
1861 blank=True
1862 )
1863 implantation = models.OneToOneField(
1864 "ResponsableImplantationProxy", db_column='implantation',
1865 related_name='responsable', unique=True
1866 )
30be56d5 1867
1868 def __unicode__(self):
1869 return u'%s : %s' % (self.implantation, self.employe)
ca1a7b76 1870
30be56d5 1871 class Meta:
1872 ordering = ['implantation__nom']
8c1ae2b3 1873 verbose_name = "Responsable d'implantation"
1874 verbose_name_plural = "Responsables d'implantation"
d104b0ae
EMS
1875
1876reversion.register(ResponsableImplantation, format='xml')
edbc9e37
OL
1877
1878
e8b6a20c
BS
1879class UserProfile(models.Model):
1880 user = models.OneToOneField(User, related_name='profile')
1881 zones_administratives = models.ManyToManyField(
1882 ref.ZoneAdministrative,
1883 related_name='profiles'
1884 )
1885 class Meta:
1886 verbose_name = "Permissions sur zones administratives"
1887 verbose_name_plural = "Permissions sur zones administratives"
1888
1889 def __unicode__(self):
1890 return self.user.__unicode__()
1891
1892reversion.register(UserProfile, format='xml')
343cfd9c
BS
1893
1894
1895
1896TYPES_CHANGEMENT = (
ea42c057
BS
1897 ('NO', 'Arrivée'),
1898 ('MO', 'Mobilité'),
1899 ('DE', 'Départ'),
343cfd9c
BS
1900 )
1901
1902
1903class ChangementPersonnelNotifications(models.Model):
1904 class Meta:
ea42c057
BS
1905 verbose_name = u"Destinataire pour notices de mouvement de personnel"
1906 verbose_name_plural = u"Destinataires pour notices de mouvement de personnel"
343cfd9c
BS
1907
1908 type = models.CharField(
1909 max_length=2,
1910 choices = TYPES_CHANGEMENT,
1911 unique=True,
1912 )
1913
1914 destinataires = models.ManyToManyField(
1915 ref.Employe,
1916 related_name='changement_notifications',
1917 )
1918
1919 def __unicode__(self):
1920 return '%s: %s' % (
1921 self.get_type_display(), ','.join(
1922 self.destinataires.all().values_list(
1923 'courriel', flat=True))
1924 )
1925
1926
1927class ChangementPersonnel(models.Model):
1928 """
1929 Une notice qui enregistre un changement de personnel, incluant:
1930
1931 * Nouveaux employés
1932 * Mouvement de personnel
1933 * Départ d'employé
1934 """
1935
1936 class Meta:
ea42c057
BS
1937 verbose_name = u"Mouvement de personnel"
1938 verbose_name_plural = u"Mouvements de personnel"
343cfd9c
BS
1939
1940 def __unicode__(self):
1941 return '%s: %s' % (self.dossier.__unicode__(),
1942 self.get_type_display())
1943
1944 @classmethod
1945 def create_changement(cls, dossier, type):
1946 # If this employe has existing Changement, set them to invalid.
1947 cls.objects.filter(dossier__employe=dossier.employe).update(valide=False)
1948
1949 # Create a new one.
1950 cls.objects.create(
1951 dossier=dossier,
1952 type=type,
1953 valide=True,
1954 communique=False,
1955 )
1956
1957
1958 @classmethod
343cfd9c
BS
1959 def post_save_handler(cls,
1960 sender,
1961 instance,
1962 created,
1963 using,
1964 **kw):
1965
1966 # This defines the time limit used when checking in previous
1967 # files to see if an employee if new. Basically, if emloyee
1968 # left his position new_file.date_debut -
1969 # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then
1970 # if a new file is created for this employee, he will bec
1971 # onsidered "NEW" and a notice will be created to this effect.
1972 NEW_EMPLOYE_THRESHOLD = datetime.timedelta(7) # 7 days.
1973
1974 other_dossier_qs = instance.employe.rh_dossiers.exclude(
1975 id=instance.id)
1976 dd = instance.date_debut
1977 df = instance.date_fin
1978
1979 # Here, verify differences between the instance, before and
1980 # after the save.
e535b526
BS
1981 df_has_changed = False
1982
343cfd9c 1983 if created:
e535b526 1984 if df != None:
343cfd9c
BS
1985 df_has_changed = True
1986 else:
1987 df_has_changed = (df != instance.before_save.date_fin and
1988 df != None)
1989
1990
1991 # VERIFICATIONS:
1992
1993 # Date de fin est None et c'est une nouvelle instance de
1994 # Dossier
1995 if not df and created:
1996 # QS for finding other dossiers with a date_fin of None OR
1997 # with a date_fin >= to this dossier's date_debut
1998 exists_recent_file_qs = other_dossier_qs.filter(
1999 Q(date_fin__isnull=True) |
2000 Q(date_fin__gte=dd - NEW_EMPLOYE_THRESHOLD)
2001 )
2002
2003 # 1. If existe un Dossier récent, et c'est une nouvelle
2004 # instance de Dossier:
2005 if exists_recent_file_qs.count() > 0:
2006 cls.create_changement(
2007 instance,
2008 'MO',
2009 )
2010 # 2. Il n'existe un Dossier récent, et c'est une nouvelle
2011 # instance de Dossier:
2012 else:
2013 cls.create_changement(
2014 instance,
2015 'NO',
2016 )
2017
2018
2019 # Date de fin a été modifiée:
2020 if df_has_changed:
2021 # QS for other active files (date_fin == None), excludes
2022 # instance.
2023 exists_active_files_qs = other_dossier_qs.filter(
2024 Q(date_fin__isnull=True))
2025
2026 # 3. Date de fin a été modifiée et il n'existe aucun autre
2027 # dossier actifs: Depart
2028 if exists_active_files_qs.count() == 0:
2029 cls.create_changement(
2030 instance,
2031 'DE',
2032 )
2033 # 4. Dossier a une nouvelle date de fin par contre
2034 # d'autres dossiers actifs existent déjà: Mouvement
2035 else:
2036 cls.create_changement(
2037 instance,
2038 'MO',
2039 )
2040
2041
2042 dossier = models.ForeignKey(
2043 Dossier,
2044 related_name='mouvements',
2045 )
2046
2047 valide = models.BooleanField(default=True)
4e93fcf2
BS
2048 date_creation = models.DateTimeField(
2049 auto_now_add=True)
e0a465f2
BS
2050 communique = models.BooleanField(
2051 u'Communiqué',
2052 default=False,
2053 )
343cfd9c
BS
2054 date_communication = models.DateTimeField(
2055 null=True,
2056 blank=True,
2057 )
2058
2059 type = models.CharField(
2060 max_length=2,
2061 choices = TYPES_CHANGEMENT,
2062 )
2063
2064reversion.register(ChangementPersonnel, format='xml')
2065
2066
9388fbac
BS
2067def dossier_pre_save_handler(sender,
2068 instance,
2069 using,
2070 **kw):
2071 # Store a copy of the model before save is called.
2072 if instance.pk is not None:
2073 instance.before_save = Dossier.objects.get(pk=instance.pk)
2074 else:
2075 instance.before_save = None
2076
2077
2078# Connect a pre_save handler that assigns a copy of the model as an
2079# attribute in order to compare it in post_save.
2080pre_save.connect(dossier_pre_save_handler, sender=Dossier)
2081
343cfd9c 2082post_save.connect(ChangementPersonnel.post_save_handler, sender=Dossier)
9388fbac
BS
2083post_save.connect(RHDossierClassementRecord.post_save_handler, sender=Dossier)
2084
2085