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