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