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