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