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