Merge branch 'hotfix/print_bug'
[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
89d105e3
BS
858 compte_compta = models.CharField(max_length=10, default='aucun',
859 verbose_name=u'Compte comptabilité',
860 choices=COMPTE_COMPTA_CHOICES)
861 compte_courriel = models.BooleanField()
ca1a7b76 862
6e4600ef 863 class Meta:
37868f0b 864 abstract = True
49449367 865 ordering = ['employe__nom', ]
3f5f3898 866 verbose_name = u"Dossier"
8c1ae2b3 867 verbose_name_plural = "Dossiers"
ca1a7b76 868
65f9fac8 869 def salaire_theorique(self):
870 annee = date.today().year
871 coeff = self.classement.coefficient
872 implantation = self.poste.implantation
873 point = ValeurPoint.objects.get(implantation=implantation, annee=annee)
ca1a7b76 874
65f9fac8 875 montant = coeff * point.valeur
876 devise = point.devise
fa1f7426 877 return {'montant': montant, 'devise': devise}
ca1a7b76 878
83b7692b 879 def __unicode__(self):
8c1ae2b3 880 poste = self.poste.nom
881 if self.employe.genre == 'F':
ca1a7b76 882 poste = self.poste.nom_feminin
8c1ae2b3 883 return u'%s - %s' % (self.employe, poste)
83b7692b 884
b0cf30b8 885 prefix_implantation = "poste__implantation__zone_administrative"
fa1f7426 886
b0cf30b8
EMS
887 def get_zones_administratives(self):
888 return [self.poste.implantation.zone_administrative]
aff1a4c6 889
3ebc0952 890 def remunerations(self):
838bc59d
OL
891 key = "%s_remunerations" % self._meta.app_label
892 remunerations = getattr(self, key)
893 return remunerations.all().order_by('-date_debut')
3ebc0952 894
02e69aa2 895 def remunerations_en_cours(self):
838bc59d
OL
896 q = Q(date_fin__exact=None) | Q(date_fin__gt=datetime.date.today())
897 return self.remunerations().all().filter(q).order_by('date_debut')
02e69aa2 898
09aa8374
OL
899 def get_salaire(self):
900 try:
fa1f7426
EMS
901 return [r for r in self.remunerations().order_by('-date_debut')
902 if r.type_id == 1][0]
09aa8374
OL
903 except:
904 return None
3ebc0952 905
838bc59d
OL
906 def get_salaire_euros(self):
907 tx = self.taux_devise()
908 return (float)(tx) * (float)(self.salaire)
909
910 def get_remunerations_brutes(self):
911 """
912 1 Salaire de base
913 3 Indemnité de base
914 4 Indemnité d'expatriation
915 5 Indemnité pour frais
916 6 Indemnité de logement
917 7 Indemnité de fonction
918 8 Indemnité de responsabilité
919 9 Indemnité de transport
920 10 Indemnité compensatrice
921 11 Indemnité de subsistance
922 12 Indemnité différentielle
923 13 Prime d'installation
924 14 Billet d'avion
925 15 Déménagement
926 16 Indemnité de départ
927 18 Prime de 13ième mois
928 19 Prime d'intérim
929 """
fa1f7426
EMS
930 ids = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19]
931 return [r for r in self.remunerations_en_cours().all()
932 if r.type_id in ids]
838bc59d
OL
933
934 def get_charges_salariales(self):
935 """
936 20 Charges salariales ?
937 """
fa1f7426
EMS
938 ids = [20]
939 return [r for r in self.remunerations_en_cours().all()
940 if r.type_id in ids]
838bc59d 941
838bc59d
OL
942 def get_charges_patronales(self):
943 """
944 17 Charges patronales
945 """
fa1f7426
EMS
946 ids = [17]
947 return [r for r in self.remunerations_en_cours().all()
948 if r.type_id in ids]
838bc59d 949
552d0db7
OL
950 def get_remunerations_tierces(self):
951 """
952 2 Salaire MAD
953 """
fa1f7426
EMS
954 return [r for r in self.remunerations_en_cours().all()
955 if r.type_id in (2,)]
552d0db7
OL
956
957 # DEVISE LOCALE
958
959 def get_total_local_charges_salariales(self):
bc17b82c 960 devise = self.poste.get_devise()
552d0db7
OL
961 total = 0.0
962 for r in self.get_charges_salariales():
bc17b82c
OL
963 if r.devise != devise:
964 return None
965 total += float(r.montant)
552d0db7
OL
966 return total
967
968 def get_total_local_charges_patronales(self):
bc17b82c 969 devise = self.poste.get_devise()
552d0db7
OL
970 total = 0.0
971 for r in self.get_charges_patronales():
bc17b82c
OL
972 if r.devise != devise:
973 return None
552d0db7
OL
974 total += float(r.montant)
975 return total
976
977 def get_local_salaire_brut(self):
978 """
979 somme des rémuérations brutes
980 """
981 devise = self.poste.get_devise()
982 total = 0.0
983 for r in self.get_remunerations_brutes():
984 if r.devise != devise:
985 return None
986 total += float(r.montant)
987 return total
988
989 def get_local_salaire_net(self):
990 """
991 salaire brut - charges salariales
992 """
993 devise = self.poste.get_devise()
994 total_charges = 0.0
995 for r in self.get_charges_salariales():
996 if r.devise != devise:
997 return None
998 total_charges += float(r.montant)
999 return self.get_local_salaire_brut() - total_charges
1000
1001 def get_local_couts_auf(self):
1002 """
1003 salaire net + charges patronales
1004 """
1005 devise = self.poste.get_devise()
1006 total_charges = 0.0
1007 for r in self.get_charges_patronales():
1008 if r.devise != devise:
1009 return None
1010 total_charges += float(r.montant)
1011 return self.get_local_salaire_net() + total_charges
1012
1013 def get_total_local_remunerations_tierces(self):
1014 devise = self.poste.get_devise()
1015 total = 0.0
1016 for r in self.get_remunerations_tierces():
1017 if r.devise != devise:
1018 return None
1019 total += float(r.montant)
1020 return total
1021
1022 # DEVISE EURO
1023
1024 def get_total_charges_salariales(self):
1025 total = 0.0
1026 for r in self.get_charges_salariales():
1027 total += r.montant_euros()
1028 return total
1029
838bc59d
OL
1030 def get_total_charges_patronales(self):
1031 total = 0.0
1032 for r in self.get_charges_patronales():
1033 total += r.montant_euros()
1034 return total
1035
1036 def get_salaire_brut(self):
1037 """
1038 somme des rémuérations brutes
1039 """
1040 total = 0.0
1041 for r in self.get_remunerations_brutes():
1042 total += r.montant_euros()
1043 return total
1044
1045 def get_salaire_net(self):
1046 """
1047 salaire brut - charges salariales
1048 """
1049 total_charges = 0.0
1050 for r in self.get_charges_salariales():
1051 total_charges += r.montant_euros()
1052 return self.get_salaire_brut() - total_charges
1053
1054 def get_couts_auf(self):
1055 """
1056 salaire net + charges patronales
1057 """
1058 total_charges = 0.0
1059 for r in self.get_charges_patronales():
1060 total_charges += r.montant_euros()
1061 return self.get_salaire_net() + total_charges
1062
838bc59d
OL
1063 def get_total_remunerations_tierces(self):
1064 total = 0.0
1065 for r in self.get_remunerations_tierces():
1066 total += r.montant_euros()
1067 return total
1068
5db1c5a3
DB
1069 def premier_contrat(self):
1070 """contrat avec plus petite date de début"""
1071 try:
db265492
EMS
1072 contrat = self.rh_contrats.exclude(date_debut=None) \
1073 .order_by('date_debut')[0]
5db1c5a3
DB
1074 except IndexError, Contrat.DoesNotExist:
1075 contrat = None
1076 return contrat
db265492 1077
5db1c5a3
DB
1078 def dernier_contrat(self):
1079 """contrat avec plus grande date de fin"""
1080 try:
db265492
EMS
1081 contrat = self.rh_contrats.exclude(date_debut=None) \
1082 .order_by('-date_debut')[0]
5db1c5a3
DB
1083 except IndexError, Contrat.DoesNotExist:
1084 contrat = None
1085 return contrat
1086
bfb5e43e
EMS
1087 def actif(self):
1088 today = date.today()
1089 return (self.date_debut is None or self.date_debut <= today) \
1090 and (self.date_fin is None or self.date_fin >= today) \
1091 and not (self.date_fin is None and self.date_debut is None)
1092
22343fe7 1093
37868f0b
NC
1094class Dossier(Dossier_):
1095 __doc__ = Dossier_.__doc__
4ba84959
EMS
1096 poste = models.ForeignKey(
1097 Poste, db_column='poste', related_name='rh_dossiers',
0b0545bd 1098 help_text=u"Taper le nom du poste ou du type de poste",
4ba84959 1099 )
fa1f7426
EMS
1100 employe = models.ForeignKey(
1101 'Employe', db_column='employe',
1102 help_text=u"Taper le nom de l'employé",
4ba84959
EMS
1103 related_name='rh_dossiers', verbose_name=u"employé"
1104 )
fa1f7426 1105 principal = models.BooleanField(
c1f5d83c 1106 u"dossier principal", default=True,
fa1f7426
EMS
1107 help_text=(
1108 u"Ce dossier est pour le principal poste occupé par l'employé"
1109 )
1110 )
343cfd9c 1111
37868f0b 1112
d104b0ae
EMS
1113reversion.register(Dossier, format='xml', follow=[
1114 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations',
1115 'rh_contrats', 'commentaires'
1116])
1117
37868f0b 1118
9388fbac
BS
1119class RHDossierClassementRecord(models.Model):
1120 classement = models.ForeignKey(
1121 'Classement',
1122 related_name='classement_records',
1123 )
1124 dossier = models.ForeignKey(
1125 'Dossier',
1126 related_name='classement_records',
1127 )
1128 date_debut = models.DateField(
1129 u"date de début",
1130 help_text=HELP_TEXT_DATE,
1131 null=True,
1132 blank=True,
1133 db_index=True
1134 )
1135 date_fin = models.DateField(
1136 u"date de fin",
1137 help_text=HELP_TEXT_DATE,
1138 null=True,
1139 blank=True,
1140 db_index=True
1141 )
1142
1143 def __unicode__(self):
1144 return self.classement.__unicode__()
1145
1146 class Meta:
1147 verbose_name = u"Element d'historique de classement"
1148 verbose_name_plural = u"Historique de classement"
1149
1150 @classmethod
1151 def post_save_handler(cls,
1152 sender,
1153 instance,
1154 created,
1155 using,
1156 **kw):
1157
990c9e12
BS
1158 today = date.today()
1159 previous_record = None
9388fbac
BS
1160 previous_classement = None
1161 has_changed = False
1162
990c9e12
BS
1163 # Premièrement, pour les nouvelles instances:
1164 if created:
182fb8f9 1165 if not instance.classement:
990c9e12
BS
1166 return
1167 else:
1168 cls.objects.create(
1169 date_debut=instance.date_debut,
1170 classement=instance.classement,
9388fbac 1171 dossier=instance,
9388fbac 1172 )
990c9e12 1173 return
9388fbac 1174
990c9e12 1175 # Deuxièmement, pour les instances existantes:
9388fbac 1176
990c9e12
BS
1177 # Détermine si:
1178 # 1. Est-ce que le classement a changé?
1179 # 2. Est-ce qu'une historique de classement existe déjà
1180 try:
1181 previous_record = cls.objects.get(
9388fbac 1182 dossier=instance,
990c9e12
BS
1183 classement=instance.before_save.classement,
1184 date_fin=None,
9388fbac 1185 )
990c9e12
BS
1186 except cls.DoesNotExist:
1187 if instance.before_save.classement:
1188 # Il était censé avoir une historique de classement
1189 # donc on le créé.
1190 previous_record = cls.objects.create(
1191 date_debut=instance.before_save.date_debut,
1192 classement=instance.before_save.classement,
1193 dossier=instance,
1194 )
1195 previous_classement = instance.before_save.classement
9388fbac 1196
990c9e12
BS
1197 else:
1198 previous_classement = previous_record.classement
9388fbac 1199
990c9e12
BS
1200 has_changed = (
1201 instance.classement !=
1202 previous_classement
1203 )
1204
1205 # Cas aucun changement:
1206 if not has_changed:
1207 return
1208
1209 else:
1210 # Classement a changé
1211 if previous_record:
1212 previous_record.date_fin = today
1213 previous_record.save()
9388fbac 1214
990c9e12
BS
1215 if instance.classement:
1216 cls.objects.create(
1217 date_debut=today,
1218 classement=instance.classement,
1219 dossier=instance,
1220 )
9388fbac
BS
1221
1222
fc917340 1223class DossierPiece_(models.Model):
fa1f7426
EMS
1224 """
1225 Documents relatifs au Dossier (à l'occupation de ce poste par employé).
7abc6d45 1226 Ex.: Lettre de motivation.
1227 """
fa1f7426
EMS
1228 nom = models.CharField(max_length=255)
1229 fichier = models.FileField(
1230 upload_to=dossier_piece_dispatch, storage=storage_prive
1231 )
83b7692b 1232
6e4600ef 1233 class Meta:
fc917340 1234 abstract = True
6e4600ef 1235 ordering = ['nom']
ca1a7b76 1236
6e4600ef 1237 def __unicode__(self):
1238 return u'%s' % (self.nom)
1239
fa1f7426 1240
fc917340 1241class DossierPiece(DossierPiece_):
fa1f7426 1242 dossier = models.ForeignKey(
4ba84959 1243 Dossier, db_column='dossier', related_name='rh_dossierpieces'
fa1f7426
EMS
1244 )
1245
d104b0ae 1246reversion.register(DossierPiece, format='xml')
fc917340 1247
4ba84959
EMS
1248class DossierCommentaire(Commentaire):
1249 dossier = models.ForeignKey(
1250 Dossier, db_column='dossier', related_name='commentaires'
1251 )
fc917340 1252
d104b0ae
EMS
1253reversion.register(DossierCommentaire, format='xml')
1254
fa1f7426 1255
e84c8ef1 1256class DossierComparaison_(models.Model, DevisableMixin):
1d0f4eef
OL
1257 """
1258 Photo d'une comparaison salariale au moment de l'embauche.
1259 """
16b1454e
OL
1260 objects = DossierComparaisonManager()
1261
fa1f7426
EMS
1262 implantation = models.ForeignKey(
1263 ref.Implantation, related_name="+", null=True, blank=True
1264 )
1d0f4eef
OL
1265 poste = models.CharField(max_length=255, null=True, blank=True)
1266 personne = models.CharField(max_length=255, null=True, blank=True)
1267 montant = models.IntegerField(null=True)
fa1f7426
EMS
1268 devise = models.ForeignKey(
1269 'Devise', related_name='+', null=True, blank=True
1270 )
1d0f4eef 1271
fc917340
OL
1272 class Meta:
1273 abstract = True
1274
3b14230d
OL
1275 def __unicode__(self):
1276 return "%s (%s)" % (self.poste, self.personne)
1277
1d0f4eef 1278
fc917340 1279class DossierComparaison(DossierComparaison_):
4ba84959
EMS
1280 dossier = models.ForeignKey(
1281 Dossier, related_name='rh_comparaisons'
1282 )
2d4d2fcf 1283
d104b0ae
EMS
1284reversion.register(DossierComparaison, format='xml')
1285
fa1f7426 1286
07b40eda 1287### RÉMUNÉRATION
ca1a7b76 1288
45066657 1289class RemunerationMixin(models.Model):
fa1f7426 1290
9afaa55e 1291 # Identification
fa1f7426
EMS
1292 type = models.ForeignKey(
1293 'TypeRemuneration', db_column='type', related_name='+',
1294 verbose_name=u"type de rémunération"
1295 )
1296 type_revalorisation = models.ForeignKey(
1297 'TypeRevalorisation', db_column='type_revalorisation',
1298 related_name='+', verbose_name=u"type de revalorisation",
1299 null=True, blank=True
1300 )
1301 montant = models.DecimalField(
d104b0ae 1302 null=True, blank=True, max_digits=12, decimal_places=2
fa1f7426
EMS
1303 ) # Annuel (12 mois, 52 semaines, 364 jours?)
1304 devise = models.ForeignKey('Devise', db_column='devise', related_name='+')
1305
2d4d2fcf 1306 # commentaire = precision
1307 commentaire = models.CharField(max_length=255, null=True, blank=True)
fa1f7426 1308
2d4d2fcf 1309 # date_debut = anciennement date_effectif
8bb6f549
EMS
1310 date_debut = models.DateField(
1311 u"date de début", null=True, blank=True, db_index=True
1312 )
1313 date_fin = models.DateField(
1314 u"date de fin", null=True, blank=True, db_index=True
1315 )
ca1a7b76
EMS
1316
1317 class Meta:
2d4d2fcf 1318 abstract = True
6e4600ef 1319 ordering = ['type__nom', '-date_fin']
ca1a7b76 1320
6e4600ef 1321 def __unicode__(self):
1322 return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom)
ca1a7b76 1323
fa1f7426 1324
e84c8ef1 1325class Remuneration_(RemunerationMixin, DevisableMixin):
fa1f7426
EMS
1326 """
1327 Structure de rémunération (données budgétaires) en situation normale
ca1a7b76
EMS
1328 pour un Dossier. Si un Evenement existe, utiliser la structure de
1329 rémunération EvenementRemuneration de cet événement.
2d4d2fcf 1330 """
c3550a05 1331 objects = RemunerationManager()
83b7692b 1332
7d8f6789
BS
1333 @staticmethod
1334 def find_yearly_range(from_date, to_date, year):
1335 today = date.today()
1336 year = year or date.today().year
1337 year_start = date(year, 1, 1)
1338 year_end = date(year, 12, 31)
1339
1340 def constrain_to_year(*dates):
1341 """
1342 S'assure que les dates soient dans le range year_start a
1343 year_end
1344 """
1345 return [min(max(year_start, d), year_end)
1346 for d in dates]
1347
1348 start_date = max(
1349 from_date or year_start, year_start)
1350 end_date = min(
1351 to_date or year_end, year_end)
1352
1353 start_date, end_date = constrain_to_year(start_date, end_date)
1354
1355 jours_annee = (year_end - year_start).days
1356 jours_dates = (end_date - start_date).days
1357 factor = Decimal(str(jours_dates)) / Decimal(str(jours_annee))
1358
1359 return start_date, end_date, factor
1360
1361
e0a465f2
BS
1362 def montant_ajuste_euros(self, annee=None):
1363 """
1364 Le montant ajusté représente le montant annuel, ajusté sur la
1365 période de temps travaillée, multipliée par le ratio de temps
1366 travaillé (en rapport au temps plein).
1367 """
7d8f6789
BS
1368 date_debut, date_fin, factor = self.find_yearly_range(
1369 self.date_debut,
1370 self.date_fin,
1371 annee,
1372 )
1373
1374 montant_euros = Decimal(str(self.montant_euros_float()) or '0')
1375
e0a465f2 1376 if self.type.nature_remuneration != u'Accessoire':
182fb8f9
BS
1377 dossier = getattr(self, 'dossier', None)
1378 if not dossier:
1379 """
1380 Dans le cas d'un DossierComparaisonRemuneration, il
1381 n'y a plus de reference au dossier.
1382 """
1383 regime_travail = REGIME_TRAVAIL_DEFAULT
1384 else:
1385 regime_travail = self.dossier.regime_travail
7d8f6789 1386 return (montant_euros * factor *
182fb8f9 1387 regime_travail / 100)
e0a465f2 1388 else:
7d8f6789
BS
1389 return montant_euros
1390
83b7692b 1391 def montant_mois(self):
1392 return round(self.montant / 12, 2)
1393
626beb4d 1394 def montant_avec_regime(self):
fa1f7426 1395 return round(self.montant * (self.dossier.regime_travail / 100), 2)
626beb4d 1396
83b7692b 1397 def montant_euro_mois(self):
e84c8ef1 1398 return round(self.montant_euros() / 12, 2)
ca1a7b76 1399
9afaa55e 1400 def __unicode__(self):
1401 try:
1402 devise = self.devise.code
1403 except:
1404 devise = "???"
1405 return "%s %s" % (self.montant, devise)
83b7692b 1406
6e7c919b
NC
1407 class Meta:
1408 abstract = True
c1195471
OL
1409 verbose_name = u"Rémunération"
1410 verbose_name_plural = u"Rémunérations"
6e7c919b
NC
1411
1412
1413class Remuneration(Remuneration_):
4ba84959
EMS
1414 dossier = models.ForeignKey(
1415 Dossier, db_column='dossier', related_name='rh_remunerations'
1416 )
6e7c919b 1417
d104b0ae
EMS
1418reversion.register(Remuneration, format='xml')
1419
2d4d2fcf 1420
1421### CONTRATS
c41b7fcc 1422
45066657 1423class Contrat_(models.Model):
fa1f7426
EMS
1424 """
1425 Document juridique qui encadre la relation de travail d'un Employe
ca1a7b76 1426 pour un Poste particulier. Pour un Dossier (qui documente cette
2d4d2fcf 1427 relation de travail) plusieurs contrats peuvent être associés.
1428 """
c41b7fcc 1429 objects = ContratManager()
fa1f7426
EMS
1430 type_contrat = models.ForeignKey(
1431 'TypeContrat', db_column='type_contrat',
1432 verbose_name=u'type de contrat', related_name='+'
1433 )
8bb6f549
EMS
1434 date_debut = models.DateField(
1435 u"date de début", db_index=True
1436 )
1437 date_fin = models.DateField(
1438 u"date de fin", null=True, blank=True, db_index=True
1439 )
fa1f7426
EMS
1440 fichier = models.FileField(
1441 upload_to=contrat_dispatch, storage=storage_prive, null=True,
1442 blank=True
1443 )
6e4600ef 1444
1445 class Meta:
fc917340 1446 abstract = True
a2c3ad52 1447 ordering = ['dossier__employe__nom']
c1195471
OL
1448 verbose_name = u"Contrat"
1449 verbose_name_plural = u"Contrats"
ca1a7b76 1450
6e4600ef 1451 def __unicode__(self):
8c1ae2b3 1452 return u'%s - %s' % (self.dossier, self.id)
fc917340 1453
fa1f7426 1454
fc917340 1455class Contrat(Contrat_):
4ba84959
EMS
1456 dossier = models.ForeignKey(
1457 Dossier, db_column='dossier', related_name='rh_contrats'
1458 )
f31ddfa0 1459
d104b0ae
EMS
1460reversion.register(Contrat, format='xml')
1461
83b7692b 1462
ca1a7b76 1463### RÉFÉRENCES RH
83b7692b 1464
45066657 1465class CategorieEmploi(models.Model):
fa1f7426
EMS
1466 """
1467 Catégorie utilisée dans la gestion des Postes.
6e4600ef 1468 Catégorie supérieure à TypePoste.
1469 """
e9bbd6ba 1470 nom = models.CharField(max_length=255)
ca1a7b76 1471
8c1ae2b3 1472 class Meta:
321fe481 1473 ordering = ('nom',)
7bf28694
EMS
1474 verbose_name = u"catégorie d'emploi"
1475 verbose_name_plural = u"catégories d'emploi"
ca1a7b76 1476
6e4600ef 1477 def __unicode__(self):
321fe481
EMS
1478 return self.nom
1479
d104b0ae
EMS
1480reversion.register(CategorieEmploi, format='xml')
1481
321fe481
EMS
1482
1483class FamilleProfessionnelle(models.Model):
1484 """
1485 Famille professionnelle d'un poste.
1486 """
1487 nom = models.CharField(max_length=100)
1488
1489 class Meta:
1490 ordering = ('nom',)
1491 verbose_name = u'famille professionnelle'
1492 verbose_name_plural = u'familles professionnelles'
1493
1494 def __unicode__(self):
1495 return self.nom
e9bbd6ba 1496
d104b0ae
EMS
1497reversion.register(FamilleProfessionnelle, format='xml')
1498
fa1f7426 1499
15659516 1500class TypePoste(Archivable):
fa1f7426
EMS
1501 """
1502 Catégorie de Poste.
6e4600ef 1503 """
e9bbd6ba 1504 nom = models.CharField(max_length=255)
fa1f7426
EMS
1505 nom_feminin = models.CharField(u"nom féminin", max_length=255)
1506 is_responsable = models.BooleanField(
1507 u"poste de responsabilité", default=False
1508 )
7bf28694
EMS
1509 categorie_emploi = models.ForeignKey(
1510 CategorieEmploi, db_column='categorie_emploi', related_name='+',
1511 verbose_name=u"catégorie d'emploi"
fa1f7426 1512 )
321fe481
EMS
1513 famille_professionnelle = models.ForeignKey(
1514 FamilleProfessionnelle, related_name='types_de_poste',
1515 verbose_name=u"famille professionnelle", blank=True, null=True
1516 )
e9bbd6ba 1517
6e4600ef 1518 class Meta:
1519 ordering = ['nom']
c1195471
OL
1520 verbose_name = u"Type de poste"
1521 verbose_name_plural = u"Types de poste"
ca1a7b76 1522
e9bbd6ba 1523 def __unicode__(self):
6e4600ef 1524 return u'%s' % (self.nom)
e9bbd6ba 1525
d104b0ae
EMS
1526reversion.register(TypePoste, format='xml')
1527
1528
e9bbd6ba 1529TYPE_PAIEMENT_CHOICES = (
a3e3bde0
JPC
1530 (u'Régulier', u'Régulier'),
1531 (u'Ponctuel', u'Ponctuel'),
e9bbd6ba 1532)
1533
1534NATURE_REMUNERATION_CHOICES = (
f40a4829
BS
1535 (u'Traitement', u'Traitements'),
1536 (u'Indemnité', u'Indemnités autres'),
7d8f6789 1537 (u'Charges', u'Charges patronales'),
f40a4829 1538 (u'Accessoire', u'Accessoires'),
a3e3bde0 1539 (u'RAS', u'Rémunération autre source'),
e9bbd6ba 1540)
1541
fa1f7426 1542
7013d234 1543class TypeRemuneration(Archivable):
fa1f7426
EMS
1544 """
1545 Catégorie de Remuneration.
6e4600ef 1546 """
7ba822a6 1547
156d6b4d
BS
1548 objects = models.Manager()
1549 sans_archives = ArchivableManager()
1550
e9bbd6ba 1551 nom = models.CharField(max_length=255)
fa1f7426
EMS
1552 type_paiement = models.CharField(
1553 u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES
1554 )
6bec5651 1555
fa1f7426
EMS
1556 nature_remuneration = models.CharField(
1557 u"nature de la rémunération", max_length=30,
1558 choices=NATURE_REMUNERATION_CHOICES
1559 )
ca1a7b76 1560
8c1ae2b3 1561 class Meta:
1562 ordering = ['nom']
c1195471
OL
1563 verbose_name = u"Type de rémunération"
1564 verbose_name_plural = u"Types de rémunération"
9afaa55e 1565
1566 def __unicode__(self):
7013d234 1567 return self.nom
ca1a7b76 1568
d104b0ae
EMS
1569reversion.register(TypeRemuneration, format='xml')
1570
fa1f7426 1571
15659516 1572class TypeRevalorisation(Archivable):
fa1f7426
EMS
1573 """
1574 Justification du changement de la Remuneration.
6e4600ef 1575 (Actuellement utilisé dans aucun traitement informatique.)
7abc6d45 1576 """
e9bbd6ba 1577 nom = models.CharField(max_length=255)
ca1a7b76 1578
8c1ae2b3 1579 class Meta:
1580 ordering = ['nom']
c1195471
OL
1581 verbose_name = u"Type de revalorisation"
1582 verbose_name_plural = u"Types de revalorisation"
e9bbd6ba 1583
1584 def __unicode__(self):
6e4600ef 1585 return u'%s' % (self.nom)
ca1a7b76 1586
d104b0ae
EMS
1587reversion.register(TypeRevalorisation, format='xml')
1588
fa1f7426 1589
7013d234 1590class Service(Archivable):
fa1f7426
EMS
1591 """
1592 Unité administrative où les Postes sont rattachés.
6e4600ef 1593 """
1594 nom = models.CharField(max_length=255)
ca1a7b76 1595
9afaa55e 1596 class Meta:
1597 ordering = ['nom']
7013d234
EMS
1598 verbose_name = u"service"
1599 verbose_name_plural = u"services"
e9bbd6ba 1600
6e4600ef 1601 def __unicode__(self):
7013d234 1602 return self.nom
6e4600ef 1603
d104b0ae
EMS
1604reversion.register(Service, format='xml')
1605
e9bbd6ba 1606
1607TYPE_ORGANISME_CHOICES = (
1608 ('MAD', 'Mise à disposition'),
1609 ('DET', 'Détachement'),
1610)
1611
fa1f7426 1612
45066657 1613class OrganismeBstg(models.Model):
fa1f7426
EMS
1614 """
1615 Organisation d'où provient un Employe mis à disposition (MAD) de
6e4600ef 1616 ou détaché (DET) à l'AUF à titre gratuit.
ca1a7b76 1617
6e4600ef 1618 (BSTG = bien et service à titre gratuit.)
1619 """
e9bbd6ba 1620 nom = models.CharField(max_length=255)
1621 type = models.CharField(max_length=10, choices=TYPE_ORGANISME_CHOICES)
ca1a7b76 1622 pays = models.ForeignKey(ref.Pays, to_field='code',
6e4600ef 1623 db_column='pays',
1624 related_name='organismes_bstg',
1625 null=True, blank=True)
9afaa55e 1626
1627 class Meta:
1628 ordering = ['type', 'nom']
c1195471
OL
1629 verbose_name = u"Organisme BSTG"
1630 verbose_name_plural = u"Organismes BSTG"
9afaa55e 1631
6e4600ef 1632 def __unicode__(self):
8c1ae2b3 1633 return u'%s (%s)' % (self.nom, self.get_type_display())
83b7692b 1634
d104b0ae
EMS
1635reversion.register(OrganismeBstg, format='xml')
1636
aff1a4c6 1637
15659516 1638class Statut(Archivable):
fa1f7426
EMS
1639 """
1640 Statut de l'Employe dans le cadre d'un Dossier particulier.
6e4600ef 1641 """
9afaa55e 1642 # Identification
fa1f7426
EMS
1643 code = models.CharField(
1644 max_length=25, unique=True,
1645 help_text=(
1646 u"Saisir un code court mais lisible pour ce statut : "
1647 u"le code est utilisé pour associer les statuts aux autres "
1648 u"données tout en demeurant plus lisible qu'un identifiant "
1649 u"numérique."
1650 )
1651 )
e9bbd6ba 1652 nom = models.CharField(max_length=255)
e9bbd6ba 1653
6e4600ef 1654 class Meta:
1655 ordering = ['code']
c1195471
OL
1656 verbose_name = u"Statut d'employé"
1657 verbose_name_plural = u"Statuts d'employé"
ca1a7b76 1658
9afaa55e 1659 def __unicode__(self):
1660 return u'%s : %s' % (self.code, self.nom)
1661
d104b0ae
EMS
1662reversion.register(Statut, format='xml')
1663
83b7692b 1664
e9bbd6ba 1665TYPE_CLASSEMENT_CHOICES = (
6e4600ef 1666 ('S', 'S -Soutien'),
1667 ('T', 'T - Technicien'),
1668 ('P', 'P - Professionel'),
1669 ('C', 'C - Cadre'),
1670 ('D', 'D - Direction'),
1671 ('SO', 'SO - Sans objet [expatriés]'),
1672 ('HG', 'HG - Hors grille [direction]'),
e9bbd6ba 1673)
83b7692b 1674
fa1f7426 1675
f40a4829 1676class ClassementManager(models.Manager):
952ecb37
OL
1677 """
1678 Ordonner les spcéfiquement les classements.
1679 """
1680 def get_query_set(self):
f40a4829 1681 qs = super(ClassementManager, self).get_query_set()
fa1f7426
EMS
1682 qs = qs.extra(select={
1683 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'
1684 })
6559f73b 1685 qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', ))
952ecb37
OL
1686 return qs.all()
1687
6e7c919b 1688
f40a4829
BS
1689class ClassementArchivableManager(ClassementManager,
1690 ArchivableManager):
1691 pass
1692
1693
15659516 1694class Classement_(Archivable):
fa1f7426
EMS
1695 """
1696 Éléments de classement de la
6e4600ef 1697 "Grille générique de classement hiérarchique".
ca1a7b76
EMS
1698
1699 Utile pour connaître, pour un Dossier, le salaire de base théorique lié au
6e4600ef 1700 classement dans la grille. Le classement donne le coefficient utilisé dans:
1701
1702 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1703 """
952ecb37 1704 objects = ClassementManager()
f40a4829 1705 sans_archives = ClassementArchivableManager()
952ecb37 1706
9afaa55e 1707 # Identification
e9bbd6ba 1708 type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES)
fa1f7426
EMS
1709 echelon = models.IntegerField(u"échelon", blank=True, default=0)
1710 degre = models.IntegerField(u"degré", blank=True, default=0)
d104b0ae 1711 coefficient = models.FloatField(u"coefficient", blank=True, null=True)
fa1f7426 1712
9afaa55e 1713 # Méta
6e4600ef 1714 # annee # au lieu de date_debut et date_fin
1715 commentaire = models.TextField(null=True, blank=True)
ca1a7b76 1716
6e4600ef 1717 class Meta:
6e7c919b 1718 abstract = True
fa1f7426 1719 ordering = ['type', 'echelon', 'degre', 'coefficient']
c1195471
OL
1720 verbose_name = u"Classement"
1721 verbose_name_plural = u"Classements"
e9bbd6ba 1722
1723 def __unicode__(self):
22343fe7 1724 return u'%s.%s.%s' % (self.type, self.echelon, self.degre, )
e9bbd6ba 1725
fa1f7426 1726
6e7c919b
NC
1727class Classement(Classement_):
1728 __doc__ = Classement_.__doc__
1729
d104b0ae
EMS
1730reversion.register(Classement, format='xml')
1731
6e7c919b 1732
45066657 1733class TauxChange_(models.Model):
fa1f7426
EMS
1734 """
1735 Taux de change de la devise vers l'euro (EUR)
6e4600ef 1736 pour chaque année budgétaire.
7abc6d45 1737 """
9afaa55e 1738 # Identification
8d3e2fff 1739 devise = models.ForeignKey('Devise', db_column='devise')
fa1f7426
EMS
1740 annee = models.IntegerField(u"année")
1741 taux = models.FloatField(u"taux vers l'euro")
6e7c919b 1742
6e4600ef 1743 class Meta:
6e7c919b 1744 abstract = True
8c1ae2b3 1745 ordering = ['-annee', 'devise__code']
c1195471
OL
1746 verbose_name = u"Taux de change"
1747 verbose_name_plural = u"Taux de change"
8bb6f549 1748 unique_together = ('devise', 'annee')
ca1a7b76 1749
6e4600ef 1750 def __unicode__(self):
8c1ae2b3 1751 return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee)
e9bbd6ba 1752
6e7c919b
NC
1753
1754class TauxChange(TauxChange_):
1755 __doc__ = TauxChange_.__doc__
1756
d104b0ae
EMS
1757reversion.register(TauxChange, format='xml')
1758
fa1f7426 1759
45066657 1760class ValeurPointManager(models.Manager):
105dd778 1761
701f3bea 1762 def get_query_set(self):
fa1f7426 1763 return super(ValeurPointManager, self).get_query_set() \
15659516 1764 .select_related('devise', 'implantation')
701f3bea 1765
6e7c919b 1766
45066657 1767class ValeurPoint_(models.Model):
fa1f7426
EMS
1768 """
1769 Utile pour connaître, pour un Dossier, le salaire de base théorique lié
ca1a7b76 1770 au classement dans la grille. La ValeurPoint s'obtient par l'implantation
8c1ae2b3 1771 du Poste de ce Dossier : dossier.poste.implantation (pseudo code).
6e4600ef 1772
1773 salaire de base = coefficient * valeur du point de l'Implantation du Poste
1774 """
ca1a7b76 1775
45066657 1776 objects = models.Manager()
09aa8374 1777 actuelles = ValeurPointManager()
701f3bea 1778
8277a35b 1779 valeur = models.FloatField(null=True)
f614ca5c 1780 devise = models.ForeignKey('Devise', db_column='devise', related_name='+',)
ca1a7b76 1781 implantation = models.ForeignKey(ref.Implantation,
6e7c919b
NC
1782 db_column='implantation',
1783 related_name='%(app_label)s_valeur_point')
9afaa55e 1784 # Méta
e9bbd6ba 1785 annee = models.IntegerField()
9afaa55e 1786
6e4600ef 1787 class Meta:
701f3bea 1788 ordering = ['-annee', 'implantation__nom']
6e7c919b 1789 abstract = True
c1195471
OL
1790 verbose_name = u"Valeur du point"
1791 verbose_name_plural = u"Valeurs du point"
8bb6f549 1792 unique_together = ('implantation', 'annee')
6e0bbb73 1793
9afaa55e 1794 def __unicode__(self):
fa1f7426
EMS
1795 return u'%s %s %s [%s] %s' % (
1796 self.devise.code, self.annee, self.valeur,
1797 self.implantation.nom_court, self.devise.nom
1798 )
6e7c919b
NC
1799
1800
1801class ValeurPoint(ValeurPoint_):
1802 __doc__ = ValeurPoint_.__doc__
1803
d104b0ae
EMS
1804reversion.register(ValeurPoint, format='xml')
1805
e9bbd6ba 1806
7013d234 1807class Devise(Archivable):
6e4600ef 1808 """
fa1f7426
EMS
1809 Devise monétaire.
1810 """
fa1f7426 1811 code = models.CharField(max_length=10, unique=True)
e9bbd6ba 1812 nom = models.CharField(max_length=255)
1813
6e4600ef 1814 class Meta:
1815 ordering = ['code']
7013d234
EMS
1816 verbose_name = u"devise"
1817 verbose_name_plural = u"devises"
ca1a7b76 1818
e9bbd6ba 1819 def __unicode__(self):
1820 return u'%s - %s' % (self.code, self.nom)
1821
d104b0ae
EMS
1822reversion.register(Devise, format='xml')
1823
fa1f7426 1824
15659516 1825class TypeContrat(Archivable):
fa1f7426
EMS
1826 """
1827 Type de contrat.
6e4600ef 1828 """
e9bbd6ba 1829 nom = models.CharField(max_length=255)
6e4600ef 1830 nom_long = models.CharField(max_length=255)
49f9f116 1831
8c1ae2b3 1832 class Meta:
1833 ordering = ['nom']
c1195471
OL
1834 verbose_name = u"Type de contrat"
1835 verbose_name_plural = u"Types de contrat"
8c1ae2b3 1836
9afaa55e 1837 def __unicode__(self):
1838 return u'%s' % (self.nom)
ca1a7b76 1839
d104b0ae
EMS
1840reversion.register(TypeContrat, format='xml')
1841
ca1a7b76 1842
2d4d2fcf 1843### AUTRES
1844
8c8ffc4f 1845class ResponsableImplantationProxy(ref.Implantation):
7bf28694 1846
6fb68b2f
DB
1847 def save(self):
1848 pass
1849
8c8ffc4f 1850 class Meta:
d7bf0cd3 1851 managed = False
8c8ffc4f
OL
1852 proxy = True
1853 verbose_name = u"Responsable d'implantation"
1854 verbose_name_plural = u"Responsables d'implantation"
1855
1856
1857class ResponsableImplantation(models.Model):
fa1f7426
EMS
1858 """
1859 Le responsable d'une implantation.
30be56d5 1860 Anciennement géré sur le Dossier du responsable.
1861 """
fa1f7426
EMS
1862 employe = models.ForeignKey(
1863 'Employe', db_column='employe', related_name='+', null=True,
1864 blank=True
1865 )
1866 implantation = models.OneToOneField(
1867 "ResponsableImplantationProxy", db_column='implantation',
1868 related_name='responsable', unique=True
1869 )
30be56d5 1870
1871 def __unicode__(self):
1872 return u'%s : %s' % (self.implantation, self.employe)
ca1a7b76 1873
30be56d5 1874 class Meta:
1875 ordering = ['implantation__nom']
8c1ae2b3 1876 verbose_name = "Responsable d'implantation"
1877 verbose_name_plural = "Responsables d'implantation"
d104b0ae
EMS
1878
1879reversion.register(ResponsableImplantation, format='xml')
edbc9e37
OL
1880
1881
e8b6a20c
BS
1882class UserProfile(models.Model):
1883 user = models.OneToOneField(User, related_name='profile')
1884 zones_administratives = models.ManyToManyField(
1885 ref.ZoneAdministrative,
1886 related_name='profiles'
1887 )
1888 class Meta:
1889 verbose_name = "Permissions sur zones administratives"
1890 verbose_name_plural = "Permissions sur zones administratives"
1891
1892 def __unicode__(self):
1893 return self.user.__unicode__()
1894
1895reversion.register(UserProfile, format='xml')
343cfd9c
BS
1896
1897
1898
1899TYPES_CHANGEMENT = (
ea42c057
BS
1900 ('NO', 'Arrivée'),
1901 ('MO', 'Mobilité'),
1902 ('DE', 'Départ'),
343cfd9c
BS
1903 )
1904
1905
1906class ChangementPersonnelNotifications(models.Model):
1907 class Meta:
ea42c057
BS
1908 verbose_name = u"Destinataire pour notices de mouvement de personnel"
1909 verbose_name_plural = u"Destinataires pour notices de mouvement de personnel"
343cfd9c
BS
1910
1911 type = models.CharField(
1912 max_length=2,
1913 choices = TYPES_CHANGEMENT,
1914 unique=True,
1915 )
1916
1917 destinataires = models.ManyToManyField(
1918 ref.Employe,
1919 related_name='changement_notifications',
1920 )
1921
1922 def __unicode__(self):
1923 return '%s: %s' % (
1924 self.get_type_display(), ','.join(
1925 self.destinataires.all().values_list(
1926 'courriel', flat=True))
1927 )
1928
1929
1930class ChangementPersonnel(models.Model):
1931 """
1932 Une notice qui enregistre un changement de personnel, incluant:
1933
1934 * Nouveaux employés
1935 * Mouvement de personnel
1936 * Départ d'employé
1937 """
1938
1939 class Meta:
ea42c057
BS
1940 verbose_name = u"Mouvement de personnel"
1941 verbose_name_plural = u"Mouvements de personnel"
343cfd9c
BS
1942
1943 def __unicode__(self):
1944 return '%s: %s' % (self.dossier.__unicode__(),
1945 self.get_type_display())
1946
1947 @classmethod
1948 def create_changement(cls, dossier, type):
1949 # If this employe has existing Changement, set them to invalid.
1950 cls.objects.filter(dossier__employe=dossier.employe).update(valide=False)
1951
1952 # Create a new one.
1953 cls.objects.create(
1954 dossier=dossier,
1955 type=type,
1956 valide=True,
1957 communique=False,
1958 )
1959
1960
1961 @classmethod
343cfd9c
BS
1962 def post_save_handler(cls,
1963 sender,
1964 instance,
1965 created,
1966 using,
1967 **kw):
1968
1969 # This defines the time limit used when checking in previous
1970 # files to see if an employee if new. Basically, if emloyee
1971 # left his position new_file.date_debut -
1972 # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then
1973 # if a new file is created for this employee, he will bec
1974 # onsidered "NEW" and a notice will be created to this effect.
1975 NEW_EMPLOYE_THRESHOLD = datetime.timedelta(7) # 7 days.
1976
1977 other_dossier_qs = instance.employe.rh_dossiers.exclude(
1978 id=instance.id)
1979 dd = instance.date_debut
1980 df = instance.date_fin
1981
1982 # Here, verify differences between the instance, before and
1983 # after the save.
e535b526
BS
1984 df_has_changed = False
1985
343cfd9c 1986 if created:
e535b526 1987 if df != None:
343cfd9c
BS
1988 df_has_changed = True
1989 else:
1990 df_has_changed = (df != instance.before_save.date_fin and
1991 df != None)
1992
1993
1994 # VERIFICATIONS:
1995
1996 # Date de fin est None et c'est une nouvelle instance de
1997 # Dossier
1998 if not df and created:
1999 # QS for finding other dossiers with a date_fin of None OR
2000 # with a date_fin >= to this dossier's date_debut
2001 exists_recent_file_qs = other_dossier_qs.filter(
2002 Q(date_fin__isnull=True) |
2003 Q(date_fin__gte=dd - NEW_EMPLOYE_THRESHOLD)
2004 )
2005
2006 # 1. If existe un Dossier récent, et c'est une nouvelle
2007 # instance de Dossier:
2008 if exists_recent_file_qs.count() > 0:
2009 cls.create_changement(
2010 instance,
2011 'MO',
2012 )
2013 # 2. Il n'existe un Dossier récent, et c'est une nouvelle
2014 # instance de Dossier:
2015 else:
2016 cls.create_changement(
2017 instance,
2018 'NO',
2019 )
2020
2021
2022 # Date de fin a été modifiée:
2023 if df_has_changed:
2024 # QS for other active files (date_fin == None), excludes
2025 # instance.
2026 exists_active_files_qs = other_dossier_qs.filter(
2027 Q(date_fin__isnull=True))
2028
2029 # 3. Date de fin a été modifiée et il n'existe aucun autre
2030 # dossier actifs: Depart
2031 if exists_active_files_qs.count() == 0:
2032 cls.create_changement(
2033 instance,
2034 'DE',
2035 )
2036 # 4. Dossier a une nouvelle date de fin par contre
2037 # d'autres dossiers actifs existent déjà: Mouvement
2038 else:
2039 cls.create_changement(
2040 instance,
2041 'MO',
2042 )
2043
2044
2045 dossier = models.ForeignKey(
2046 Dossier,
2047 related_name='mouvements',
2048 )
2049
2050 valide = models.BooleanField(default=True)
4e93fcf2
BS
2051 date_creation = models.DateTimeField(
2052 auto_now_add=True)
e0a465f2
BS
2053 communique = models.BooleanField(
2054 u'Communiqué',
2055 default=False,
2056 )
343cfd9c
BS
2057 date_communication = models.DateTimeField(
2058 null=True,
2059 blank=True,
2060 )
2061
2062 type = models.CharField(
2063 max_length=2,
2064 choices = TYPES_CHANGEMENT,
2065 )
2066
2067reversion.register(ChangementPersonnel, format='xml')
2068
2069
9388fbac
BS
2070def dossier_pre_save_handler(sender,
2071 instance,
2072 using,
2073 **kw):
2074 # Store a copy of the model before save is called.
2075 if instance.pk is not None:
2076 instance.before_save = Dossier.objects.get(pk=instance.pk)
2077 else:
2078 instance.before_save = None
2079
2080
2081# Connect a pre_save handler that assigns a copy of the model as an
2082# attribute in order to compare it in post_save.
2083pre_save.connect(dossier_pre_save_handler, sender=Dossier)
2084
343cfd9c 2085post_save.connect(ChangementPersonnel.post_save_handler, sender=Dossier)
9388fbac
BS
2086post_save.connect(RHDossierClassementRecord.post_save_handler, sender=Dossier)
2087
2088