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