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