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