X-Git-Url: http://git.auf.org/?p=auf_rh_dae.git;a=blobdiff_plain;f=project%2Frh%2Fmodels.py;h=3c7a53896efbd019ddc3877e1a84b82be6b1bfbc;hp=1b365b6def296585a45dc3fe68d560e187290131;hb=9f6c277ce74205b30ddca1624c727c838644fa6e;hpb=e503e64d837e7434eaa27240f0eb30c0c32ba3d4 diff --git a/project/rh/models.py b/project/rh/models.py index 1b365b6..3c7a538 100644 --- a/project/rh/models.py +++ b/project/rh/models.py @@ -4,56 +4,151 @@ import datetime from datetime import date from decimal import Decimal -from django.db.models import signals +import reversion +from auf.django.emploi.models import \ + GENRE_CHOICES, SITUATION_CHOICES # devrait plutot être dans references +from auf.django.references import models as ref +from django.contrib.auth.models import User from django.core.files.storage import FileSystemStorage +from django.core.exceptions import MultipleObjectsReturned from django.db import models +from django.db.models import Q +from django.db.models.signals import post_save, pre_save from django.conf import settings -from auf.django.emploi.models import GENRE_CHOICES, SITUATION_CHOICES # devrait plutot être dans references -from auf.django.metadata.models import AUFMetadata -from auf.django.metadata.managers import NoDeleteManager -import auf.django.references.models as ref -from validators import validate_date_passee -from managers import PosteManager, DossierManager, DossierComparaisonManager, PosteComparaisonManager, DeviseManager +from project.rh.change_list import \ + RechercheTemporelle, KEY_STATUT, STATUT_ACTIF, STATUT_INACTIF, \ + STATUT_FUTUR +from project import groups +from project.rh.managers import ( + PosteManager, + DossierManager, + EmployeManager, + DossierComparaisonManager, + PosteComparaisonManager, + ContratManager, + RemunerationManager, + ArchivableManager, + ) -# Gruick hack pour déterminer d'ou provient l'instanciation d'une classe pour l'héritage. -# Cela permet de faire du dynamic loading par app sans avoir à redéfinir dans DAE la FK -def app_context(): - import inspect; - models_stack = [s[1].split('/')[-2] for s in inspect.stack() if s[1].endswith('models.py')] - return models_stack[-1] +TWOPLACES = Decimal('0.01') +from project.rh.validators import validate_date_passee + +# import pour relocaliser le modèle selon la convention (models.py pour +# introspection) +from project.rh.historique import ModificationTraite # Constantes HELP_TEXT_DATE = "format: jj-mm-aaaa" REGIME_TRAVAIL_DEFAULT = Decimal('100.00') REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT = Decimal('35.00') +REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT = \ + "Saisir le nombre d'heure de travail à temps complet (100%), " \ + "sans tenir compte du régime de travail" # Upload de fichiers storage_prive = FileSystemStorage(settings.PRIVE_MEDIA_ROOT, base_url=settings.PRIVE_MEDIA_URL) + +class RemunIntegrityException(Exception): + pass + def poste_piece_dispatch(instance, filename): - path = "%s/poste/%s/%s" % (instance._meta.app_label, instance.poste_id, filename) + path = "%s/poste/%s/%s" % ( + instance._meta.app_label, instance.poste_id, filename + ) return path + def dossier_piece_dispatch(instance, filename): - path = "%s/dossier/%s/%s" % (instance._meta.app_label, instance.dossier_id, filename) + path = "%s/dossier/%s/%s" % ( + instance._meta.app_label, instance.dossier_id, filename + ) return path + def employe_piece_dispatch(instance, filename): - path = "%s/employe/%s/%s" % (instance._meta.app_label, instance.employe_id, filename) + path = "%s/employe/%s/%s" % ( + instance._meta.app_label, instance.employe_id, filename + ) return path + def contrat_dispatch(instance, filename): - path = "%s/contrat/%s/%s" % (instance._meta.app_label, instance.dossier_id, filename) + path = "%s/contrat/%s/%s" % ( + instance._meta.app_label, instance.dossier_id, filename + ) return path -class Commentaire(AUFMetadata): +class DateActiviteMixin(models.Model): + """ + Mixin pour mettre à jour l'activité d'un modèle + """ + class Meta: + abstract = True + date_creation = models.DateTimeField(auto_now_add=True, + null=True, blank=True, + verbose_name=u"Date de création",) + date_modification = models.DateTimeField(auto_now=True, + null=True, blank=True, + verbose_name=u"Date de modification",) + + +class Archivable(models.Model): + archive = models.BooleanField(u'archivé', default=False) + + objects = ArchivableManager() + avec_archives = models.Manager() + + class Meta: + abstract = True + + +class DevisableMixin(object): + + def get_annee_pour_taux_devise(self): + return datetime.datetime.now().year + + def taux_devise(self, devise=None): + if devise is None: + devise = self.devise + + if devise is None: + return None + if devise.code == "EUR": + return 1 + + annee = self.get_annee_pour_taux_devise() + taux = TauxChange.objects.filter(devise=devise, annee__lte=annee) \ + .order_by('-annee') + return taux[0].taux + + def montant_euros_float(self): + try: + taux = self.taux_devise() + except Exception, e: + return e + if not taux: + return None + return float(self.montant) * float(taux) + + def montant_euros(self): + return int(round(self.montant_euros_float(), 2)) + + +class Commentaire(models.Model): texte = models.TextField() - owner = models.ForeignKey('auth.User', db_column='owner', related_name='+', verbose_name=u"Commentaire de") + owner = models.ForeignKey( + 'auth.User', db_column='owner', related_name='+', + verbose_name=u"Commentaire de" + ) + date_creation = models.DateTimeField( + u'date', auto_now_add=True, blank=True, null=True + ) class Meta: abstract = True @@ -70,8 +165,10 @@ POSTE_APPEL_CHOICES = ( ('externe', 'Externe'), ) -class Poste_(AUFMetadata): - """Un Poste est un emploi (job) à combler dans une implantation. + +class Poste_( DateActiviteMixin, models.Model,): + """ + Un Poste est un emploi (job) à combler dans une implantation. Un Poste peut être comblé par un Employe, auquel cas un Dossier est créé. Si on veut recruter 2 jardiniers, 2 Postes distincts existent. """ @@ -79,113 +176,152 @@ class Poste_(AUFMetadata): objects = PosteManager() # Identification - nom = models.CharField(max_length=255, - verbose_name = u"Titre du poste", ) - nom_feminin = models.CharField(max_length=255, - verbose_name = u"Titre du poste (au féminin)", - null=True) - implantation = models.ForeignKey(ref.Implantation, help_text=u"Taper le nom de l'implantation ou sa région", - db_column='implantation', related_name='+') - type_poste = models.ForeignKey('TypePoste', db_column='type_poste', help_text=u"Taper le nom du type de poste", - related_name='+', - null=True, - verbose_name=u"type de poste") - service = models.ForeignKey('Service', db_column='service', - related_name='+', - verbose_name = u"direction/service/pôle support", - null=True,) - responsable = models.ForeignKey('Poste', db_column='responsable', - related_name='+', - null=True, - help_text=u"Taper le nom du poste ou du type de poste", - verbose_name = u"Poste du responsable", ) - + nom = models.CharField(u"Titre du poste", max_length=255) + nom_feminin = models.CharField( + u"Titre du poste (au féminin)", max_length=255, null=True + ) + implantation = models.ForeignKey( + ref.Implantation, + help_text=u"Taper le nom de l'implantation ou sa région", + db_column='implantation', related_name='+' + ) + type_poste = models.ForeignKey( + 'TypePoste', db_column='type_poste', + help_text=u"Taper le nom du type de poste", related_name='+', + null=True, verbose_name=u"type de poste" + ) + service = models.ForeignKey( + 'Service', db_column='service', related_name='%(app_label)s_postes', + verbose_name=u"direction/service/pôle support", null=True + ) + responsable = models.ForeignKey( + 'Poste', db_column='responsable', + related_name='+', null=True, + help_text=u"Taper le nom du poste ou du type de poste", + verbose_name=u"Poste du responsable" + ) + # Contrat - regime_travail = models.DecimalField(max_digits=12, decimal_places=2, - default=REGIME_TRAVAIL_DEFAULT, null=True, - verbose_name = u"Temps de travail", - help_text="% du temps complet") - regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12, - decimal_places=2, null=True, - default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT, - verbose_name = u"Nb. heures par semaine") + regime_travail = models.DecimalField( + u"temps de travail", max_digits=12, decimal_places=2, + default=REGIME_TRAVAIL_DEFAULT, null=True, + help_text="% du temps complet" + ) + regime_travail_nb_heure_semaine = models.DecimalField( + u"nb. heures par semaine", max_digits=12, decimal_places=2, + null=True, default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT, + help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT + ) # Recrutement - local = models.NullBooleanField(verbose_name = u"Local", default=True, - null=True, blank=True) - expatrie = models.NullBooleanField(verbose_name = u"Expatrié", default=False, - null=True, blank=True) + local = models.NullBooleanField( + u"local", default=True, null=True, blank=True + ) + expatrie = models.NullBooleanField( + u"expatrié", default=False, null=True, blank=True + ) mise_a_disposition = models.NullBooleanField( - verbose_name = u"Mise à disposition", - null=True, default=False) - appel = models.CharField(max_length=10, null=True, - verbose_name = u"Appel à candidature", - choices=POSTE_APPEL_CHOICES, - default='interne') + u"mise à disposition", null=True, default=False + ) + appel = models.CharField( + u"Appel à candidature", max_length=10, null=True, + choices=POSTE_APPEL_CHOICES, default='interne' + ) # Rémunération - classement_min = models.ForeignKey('Classement', - db_column='classement_min', related_name='+', - null=True, blank=True) - classement_max = models.ForeignKey('Classement', - db_column='classement_max', related_name='+', - null=True, blank=True) - valeur_point_min = models.ForeignKey('ValeurPoint', help_text=u"Taper le code ou le nom de l'implantation", - db_column='valeur_point_min', related_name='+', - null=True, blank=True) - valeur_point_max = models.ForeignKey('ValeurPoint', help_text=u"Taper le code ou le nom de l'implantation", - db_column='valeur_point_max', related_name='+', - null=True, blank=True) - devise_min = models.ForeignKey('Devise', db_column='devise_min', null=True, - related_name='+',) - devise_max = models.ForeignKey('Devise', db_column='devise_max', null=True, - related_name='+',) - salaire_min = models.DecimalField(max_digits=12, decimal_places=2, - null=True, default=0) - salaire_max = models.DecimalField(max_digits=12, decimal_places=2, - null=True, default=0) - indemn_min = models.DecimalField(max_digits=12, decimal_places=2, - null=True, default=0) - indemn_max = models.DecimalField(max_digits=12, decimal_places=2, - null=True, default=0) - autre_min = models.DecimalField(max_digits=12, decimal_places=2, - null=True, default=0) - autre_max = models.DecimalField(max_digits=12, decimal_places=2, - null=True, default=0) + classement_min = models.ForeignKey( + 'Classement', db_column='classement_min', related_name='+', + null=True, blank=True + ) + classement_max = models.ForeignKey( + 'Classement', db_column='classement_max', related_name='+', + null=True, blank=True + ) + valeur_point_min = models.ForeignKey( + 'ValeurPoint', + help_text=u"Taper le code ou le nom de l'implantation", + db_column='valeur_point_min', related_name='+', null=True, + blank=True + ) + valeur_point_max = models.ForeignKey( + 'ValeurPoint', + help_text=u"Taper le code ou le nom de l'implantation", + db_column='valeur_point_max', related_name='+', null=True, + blank=True + ) + devise_min = models.ForeignKey( + 'Devise', db_column='devise_min', null=True, related_name='+' + ) + devise_max = models.ForeignKey( + 'Devise', db_column='devise_max', null=True, related_name='+' + ) + salaire_min = models.DecimalField( + max_digits=12, decimal_places=2, default=0, + ) + salaire_max = models.DecimalField( + max_digits=12, decimal_places=2, default=0, + ) + indemn_min = models.DecimalField( + max_digits=12, decimal_places=2, default=0, + ) + indemn_max = models.DecimalField( + max_digits=12, decimal_places=2, default=0, + ) + autre_min = models.DecimalField( + max_digits=12, decimal_places=2, default=0, + ) + autre_max = models.DecimalField( + max_digits=12, decimal_places=2, default=0, + ) # Comparatifs de rémunération - devise_comparaison = models.ForeignKey('Devise', null=True, blank=True, - db_column='devise_comparaison', - related_name='+', ) - comp_locale_min = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_locale_max = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_universite_min = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_universite_max = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_fonctionpub_min = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_fonctionpub_max = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_ong_min = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_ong_max = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_autre_min = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) - comp_autre_max = models.DecimalField(max_digits=12, decimal_places=2, - null=True, blank=True) + devise_comparaison = models.ForeignKey( + 'Devise', null=True, blank=True, db_column='devise_comparaison', + related_name='+' + ) + comp_locale_min = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_locale_max = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_universite_min = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_universite_max = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_fonctionpub_min = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_fonctionpub_max = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_ong_min = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_ong_max = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_autre_min = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) + comp_autre_max = models.DecimalField( + max_digits=12, decimal_places=2, null=True, blank=True + ) # Justification justification = models.TextField(null=True, blank=True) # Autres Metadata - date_debut = models.DateField(verbose_name=u"Date de début", help_text=HELP_TEXT_DATE, - null=True, blank=True) - date_fin = models.DateField(verbose_name=u"Date de fin", help_text=HELP_TEXT_DATE, - null=True, blank=True) + date_debut = models.DateField( + u"date de début", help_text=HELP_TEXT_DATE, null=True, blank=True, + db_index=True + ) + date_fin = models.DateField( + u"date de fin", help_text=HELP_TEXT_DATE, null=True, blank=True, + db_index=True + ) class Meta: abstract = True @@ -195,21 +331,31 @@ class Poste_(AUFMetadata): ordering = ["nom"] def __unicode__(self): - representation = u'%s - %s [%s]' % (self.implantation, self.nom, - self.id) + representation = u'%s - %s [%s]' % ( + self.implantation, self.nom, self.id + ) return representation + prefix_implantation = "implantation__zone_administrative" + + def get_zones_administratives(self): + return [self.implantation.zone_administrative] - prefix_implantation = "implantation__region" - def get_regions(self): - return [self.implantation.region] + def get_devise(self): + vp = ValeurPoint.objects.filter( + implantation=self.implantation, devise__archive=False + ).order_by('annee') + if len(vp) > 0: + return vp[0].devise + else: + return Devise.objects.get(code='EUR') class Poste(Poste_): __doc__ = Poste_.__doc__ # meta dématérialisation : pour permettre le filtrage - vacant = models.NullBooleanField(verbose_name = u"vacant", null=True, blank=True) + vacant = models.NullBooleanField(u"vacant", null=True, blank=True) def is_vacant(self): vacant = True @@ -218,12 +364,21 @@ class Poste(Poste_): return vacant def occupe_par(self): - """Retourne la liste d'employé occupant ce poste. + """ + Retourne la liste d'employé occupant ce poste. Généralement, retourne une liste d'un élément. Si poste inoccupé, retourne liste vide. UTILISE pour mettre a jour le flag vacant """ - return [d.employe for d in self.rh_dossiers.filter(supprime=False).exclude(date_fin__lt=date.today())] + return [ + d.employe + for d in self.rh_dossiers.exclude(date_fin__lt=date.today()) + ] + +reversion.register(Poste, format='xml', follow=[ + 'rh_financements', 'rh_pieces', 'rh_comparaisons_internes', + 'commentaires' +]) POSTE_FINANCEMENT_CHOICES = ( @@ -234,15 +389,18 @@ POSTE_FINANCEMENT_CHOICES = ( class PosteFinancement_(models.Model): - """Pour un Poste, structure d'informations décrivant comment on prévoit + """ + Pour un Poste, structure d'informations décrivant comment on prévoit financer ce Poste. """ - poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='%(app_label)s_financements') type = models.CharField(max_length=1, choices=POSTE_FINANCEMENT_CHOICES) - pourcentage = models.DecimalField(max_digits=12, decimal_places=2, - help_text="ex.: 33.33 % (décimale avec point)") + pourcentage = models.DecimalField( + max_digits=12, decimal_places=2, + help_text="ex.: 33.33 % (décimale avec point)" + ) commentaire = models.TextField( - help_text="Spécifiez la source de financement.") + help_text="Spécifiez la source de financement." + ) class Meta: abstract = True @@ -256,18 +414,23 @@ class PosteFinancement_(models.Model): class PosteFinancement(PosteFinancement_): - pass + poste = models.ForeignKey( + Poste, db_column='poste', related_name='rh_financements' + ) + +reversion.register(PosteFinancement, format='xml') class PostePiece_(models.Model): - """Documents relatifs au Poste. + """ + Documents relatifs au Poste. Ex.: Description de poste """ - poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='%(app_label)s_pieces') - nom = models.CharField(verbose_name = u"Nom", max_length=255) - fichier = models.FileField(verbose_name = u"Fichier", - upload_to=poste_piece_dispatch, - storage=storage_prive) + nom = models.CharField(u"Nom", max_length=255) + fichier = models.FileField( + u"Fichier", upload_to=poste_piece_dispatch, storage=storage_prive, + max_length=255 + ) class Meta: abstract = True @@ -276,114 +439,135 @@ class PostePiece_(models.Model): def __unicode__(self): return u'%s' % (self.nom) + class PostePiece(PostePiece_): - pass + poste = models.ForeignKey( + Poste, db_column='poste', related_name='rh_pieces' + ) + +reversion.register(PostePiece, format='xml') + -class PosteComparaison_(AUFMetadata): +class PosteComparaison_(models.Model, DevisableMixin): """ - De la même manière qu'un dossier, un poste peut-être comparé à un autre poste. + De la même manière qu'un dossier, un poste peut-être comparé à un autre + poste. """ - poste = models.ForeignKey('%s.Poste' % app_context(), related_name='%(app_label)s_comparaisons_internes') objects = PosteComparaisonManager() - implantation = models.ForeignKey(ref.Implantation, null=True, blank=True, related_name="+") - nom = models.CharField(verbose_name = u"Poste", max_length=255, null=True, blank=True) + implantation = models.ForeignKey( + ref.Implantation, null=True, blank=True, related_name="+" + ) + nom = models.CharField(u"Poste", max_length=255, null=True, blank=True) montant = models.IntegerField(null=True) - devise = models.ForeignKey("Devise", related_name='+', null=True, blank=True) + devise = models.ForeignKey( + "Devise", related_name='+', null=True, blank=True + ) class Meta: abstract = True - def taux_devise(self): - if self.devise.code == "EUR": - return 1 - annee = self.poste.date_debut.year - taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise, annee=annee)] - taux = set(taux) - if len(taux) != 1: - raise Exception(u"Le taux de la devise %s n'a pas n'existe pas pour %s ou il existe plusieurs taux pour la même année" % (self.devise.id, annee)) - else: - return list(taux)[0] - - def montant_euros(self): - return round(float(self.montant) * float(self.taux_devise()), 2) - def __unicode__(self): return self.nom + class PosteComparaison(PosteComparaison_): - pass + poste = models.ForeignKey( + Poste, related_name='rh_comparaisons_internes' + ) -class PosteCommentaire_(Commentaire): - poste = models.ForeignKey('%s.Poste' % app_context(), db_column='poste', related_name='+') +reversion.register(PosteComparaison, format='xml') - class Meta: - abstract = True -class PosteCommentaire(PosteCommentaire_): - pass +class PosteCommentaire(Commentaire): + poste = models.ForeignKey( + Poste, db_column='poste', related_name='commentaires' + ) + +reversion.register(PosteCommentaire, format='xml') ### EMPLOYÉ/PERSONNE -class Employe(AUFMetadata): - """Personne occupant ou ayant occupé un Poste. Un Employe aura autant de +class Employe(models.Model): + """ + Personne occupant ou ayant occupé un Poste. Un Employe aura autant de Dossiers qu'il occupe ou a occupé de Postes. Cette classe aurait pu avantageusement s'appeler Personne car la notion d'employé n'a pas de sens si aucun Dossier n'existe pour une personne. """ + + objects = EmployeManager() + # Identification nom = models.CharField(max_length=255) - prenom = models.CharField(max_length=255, verbose_name = u"Prénom") - nom_affichage = models.CharField(max_length=255, - verbose_name = u"Nom d'affichage", - null=True, blank=True) - nationalite = models.ForeignKey(ref.Pays, to_field='code', - db_column='nationalite', - related_name='employes_nationalite', - verbose_name = u"Nationalité", - blank=True, null=True) - date_naissance = models.DateField(verbose_name = u"Date de naissance", - help_text=HELP_TEXT_DATE, - validators=[validate_date_passee], - null=True, blank=True) + prenom = models.CharField(u"prénom", max_length=255) + nom_affichage = models.CharField( + u"nom d'affichage", max_length=255, null=True, blank=True + ) + nationalite = models.ForeignKey( + ref.Pays, to_field='code', db_column='nationalite', + related_name='employes_nationalite', verbose_name=u"nationalité", + blank=True, null=True + ) + date_naissance = models.DateField( + u"date de naissance", help_text=HELP_TEXT_DATE, + validators=[validate_date_passee], null=True, blank=True + ) genre = models.CharField(max_length=1, choices=GENRE_CHOICES) # Infos personnelles - situation_famille = models.CharField(max_length=1, - choices=SITUATION_CHOICES, - verbose_name = u"Situation familiale", - null=True, blank=True) - date_entree = models.DateField(verbose_name = u"Date d'entrée à l'AUF", - help_text=HELP_TEXT_DATE, - null=True, blank=True) + situation_famille = models.CharField( + u"situation familiale", max_length=1, choices=SITUATION_CHOICES, + null=True, blank=True + ) + date_entree = models.DateField( + u"date d'entrée à l'AUF", help_text=HELP_TEXT_DATE, null=True, + blank=True + ) # Coordonnées - tel_domicile = models.CharField(max_length=255, - verbose_name = u"Tél. domicile", - null=True, blank=True) - tel_cellulaire = models.CharField(max_length=255, - verbose_name = u"Tél. cellulaire", - null=True, blank=True) + tel_domicile = models.CharField( + u"tél. domicile", max_length=255, null=True, blank=True + ) + tel_cellulaire = models.CharField( + u"tél. cellulaire", max_length=255, null=True, blank=True + ) adresse = models.CharField(max_length=255, null=True, blank=True) ville = models.CharField(max_length=255, null=True, blank=True) province = models.CharField(max_length=255, null=True, blank=True) code_postal = models.CharField(max_length=255, null=True, blank=True) - pays = models.ForeignKey(ref.Pays, to_field='code', db_column='pays', - related_name='employes', - null=True, blank=True) + pays = models.ForeignKey( + ref.Pays, to_field='code', db_column='pays', + related_name='employes', null=True, blank=True + ) + courriel_perso = models.EmailField( + u'adresse courriel personnelle', blank=True + ) # meta dématérialisation : pour permettre le filtrage - nb_postes = models.IntegerField(verbose_name = u"nombre de postes", null=True, blank=True) + nb_postes = models.IntegerField(u"nombre de postes", null=True, blank=True) class Meta: - ordering = ['nom','prenom'] + ordering = ['nom', 'prenom'] verbose_name = u"Employé" verbose_name_plural = u"Employés" def __unicode__(self): return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id) + def get_latest_dossier_ordered_by_date_fin_and_principal(self): + res = self.rh_dossiers.order_by( + '-principal', 'date_fin') + + # Retourne en le premier du queryset si la date de fin est None + # Sinon, retourne le plus récent selon la date de fin. + first = res[0] + if first.date_fin == None: + return first + else: + return res.order_by('-principal', '-date_fin')[0] + def civilite(self): civilite = u'' if self.genre.upper() == u'M': @@ -393,32 +577,78 @@ class Employe(AUFMetadata): return civilite def url_photo(self): - """Retourne l'URL du service retournant la photo de l'Employe. + """ + Retourne l'URL du service retournant la photo de l'Employe. Équivalent reverse url 'rh_photo' avec id en param. """ from django.core.urlresolvers import reverse - return reverse('rh_photo', kwargs={'id':self.id}) + return reverse('rh_photo', kwargs={'id': self.id}) def dossiers_passes(self): - today = date.today() - dossiers_passes = self.dossiers.filter(date_fin__lt=today).order_by('-date_fin') - for d in dossiers_passes: - d.archive = True - return dossiers_passes + params = {KEY_STATUT: STATUT_INACTIF, } + search = RechercheTemporelle(params, Dossier) + search.purge_params(params) + q = search.get_q_temporel(self.rh_dossiers) + return self.rh_dossiers.filter(q) def dossiers_futurs(self): - today = date.today() - return self.dossiers.filter(date_debut__gt=today).order_by('-date_fin') + params = {KEY_STATUT: STATUT_FUTUR, } + search = RechercheTemporelle(params, Dossier) + search.purge_params(params) + q = search.get_q_temporel(self.rh_dossiers) + return self.rh_dossiers.filter(q) def dossiers_encours(self): - dossiers_p_f = self.dossiers_passes() | self.dossiers_futurs() - ids_dossiers_p_f = [d.id for d in dossiers_p_f] - dossiers_encours = self.dossiers.exclude(id__in=ids_dossiers_p_f).order_by('-date_fin') + params = {KEY_STATUT: STATUT_ACTIF, } + search = RechercheTemporelle(params, Dossier) + search.purge_params(params) + q = search.get_q_temporel(self.rh_dossiers) + return self.rh_dossiers.filter(q) + + def dossier_principal_pour_annee(self): + return self.dossier_principal(pour_annee=True) + + def dossier_principal(self, pour_annee=False): + """ + Retourne le dossier principal (ou le plus ancien si il y en a + plusieurs) + + Si pour_annee == True, retourne le ou les dossiers principaux + pour l'annee en cours, sinon, le ou les dossiers principaux + pour la journee en cours. - # TODO : supprimer ce code quand related_name fonctionnera ou d.remuneration_set - for d in dossiers_encours: - d.remunerations = Remuneration.objects.filter(dossier=d.id).order_by('-id') - return dossiers_encours + TODO: (Refactoring possible): Utiliser meme logique dans + dae/templatetags/dae.py + """ + + today = date.today() + if pour_annee: + year = today.year + year_start = date(year, 1, 1) + year_end = date(year, 12, 31) + + try: + dossier = self.rh_dossiers.filter( + (Q(date_debut__lte=year_end, date_fin__isnull=True) | + Q(date_debut__isnull=True, date_fin__gte=year_start) | + Q(date_debut__lte=year_end, date_fin__gte=year_start) | + Q(date_debut__isnull=True, date_fin__isnull=True)) & + Q(principal=True)).order_by('date_debut')[0] + except IndexError, Dossier.DoesNotExist: + dossier = None + return dossier + else: + try: + dossier = self.rh_dossiers.filter( + (Q(date_debut__lte=today, date_fin__isnull=True) | + Q(date_debut__isnull=True, date_fin__gte=today) | + Q(date_debut__lte=today, date_fin__gte=today) | + Q(date_debut__isnull=True, date_fin__isnull=True)) & + Q(principal=True)).order_by('date_debut')[0] + except IndexError, Dossier.DoesNotExist: + dossier = None + return dossier + def postes_encours(self): postes_encours = set() @@ -432,6 +662,7 @@ class Employe(AUFMetadata): Idée derrière : si on ajout d'autre Dossiers, c'est pour des Postes secondaires. """ + # DEPRECATED : on a maintenant Dossier.principal poste = Poste.objects.none() try: poste = self.dossiers_encours().order_by('date_debut')[0].poste @@ -439,23 +670,34 @@ class Employe(AUFMetadata): pass return poste - prefix_implantation = "rh_dossiers__poste__implantation__region" - def get_regions(self): - regions = [] - for d in self.dossiers.all(): - regions.append(d.poste.implantation.region) - return regions + prefix_implantation = \ + "rh_dossiers__poste__implantation__zone_administrative" + + def get_zones_administratives(self): + return [ + d.poste.implantation.zone_administrative + for d in self.dossiers.all() + ] + +reversion.register(Employe, format='xml', follow=[ + 'pieces', 'commentaires', 'ayantdroits' +]) class EmployePiece(models.Model): - """Documents relatifs à un employé. + """ + Documents relatifs à un employé. Ex.: CV... """ - employe = models.ForeignKey('Employe', db_column='employe') - nom = models.CharField(verbose_name="Nom", max_length=255) - fichier = models.FileField(verbose_name="Fichier", - upload_to=employe_piece_dispatch, - storage=storage_prive) + employe = models.ForeignKey( + 'Employe', db_column='employe', related_name="pieces", + verbose_name=u"employé" + ) + nom = models.CharField(max_length=255) + fichier = models.FileField( + u"fichier", upload_to=employe_piece_dispatch, storage=storage_prive, + max_length=255 + ) class Meta: ordering = ['nom'] @@ -465,14 +707,20 @@ class EmployePiece(models.Model): def __unicode__(self): return u'%s' % (self.nom) +reversion.register(EmployePiece, format='xml') + + class EmployeCommentaire(Commentaire): - employe = models.ForeignKey('Employe', db_column='employe', - related_name='+') + employe = models.ForeignKey( + 'Employe', db_column='employe', related_name='commentaires' + ) class Meta: verbose_name = u"Employé commentaire" verbose_name_plural = u"Employé commentaires" +reversion.register(EmployeCommentaire, format='xml') + LIEN_PARENTE_CHOICES = ( ('Conjoint', 'Conjoint'), @@ -481,35 +729,37 @@ LIEN_PARENTE_CHOICES = ( ('Fils', 'Fils'), ) -class AyantDroit(AUFMetadata): - """Personne en relation avec un Employe. + +class AyantDroit(models.Model): + """ + Personne en relation avec un Employe. """ # Identification nom = models.CharField(max_length=255) - prenom = models.CharField(max_length=255, - verbose_name = u"Prénom",) - nom_affichage = models.CharField(max_length=255, - verbose_name = u"Nom d'affichage", - null=True, blank=True) - nationalite = models.ForeignKey(ref.Pays, to_field='code', - db_column='nationalite', - related_name='ayantdroits_nationalite', - verbose_name = u"Nationalité", - null=True, blank=True) - date_naissance = models.DateField(verbose_name = u"Date de naissance", - help_text=HELP_TEXT_DATE, - validators=[validate_date_passee], - null=True, blank=True) + prenom = models.CharField(u"prénom", max_length=255) + nom_affichage = models.CharField( + u"nom d'affichage", max_length=255, null=True, blank=True + ) + nationalite = models.ForeignKey( + ref.Pays, to_field='code', db_column='nationalite', + related_name='ayantdroits_nationalite', + verbose_name=u"nationalité", null=True, blank=True + ) + date_naissance = models.DateField( + u"Date de naissance", help_text=HELP_TEXT_DATE, + validators=[validate_date_passee], null=True, blank=True + ) genre = models.CharField(max_length=1, choices=GENRE_CHOICES) # Relation - employe = models.ForeignKey('Employe', db_column='employe', - related_name='ayantdroits', - verbose_name = u"Employé") - lien_parente = models.CharField(max_length=10, - choices=LIEN_PARENTE_CHOICES, - verbose_name = u"Lien de parenté", - null=True, blank=True) + employe = models.ForeignKey( + 'Employe', db_column='employe', related_name='ayantdroits', + verbose_name=u"Employé" + ) + lien_parente = models.CharField( + u"lien de parenté", max_length=10, choices=LIEN_PARENTE_CHOICES, + null=True, blank=True + ) class Meta: ordering = ['nom', ] @@ -517,19 +767,26 @@ class AyantDroit(AUFMetadata): verbose_name_plural = u"Ayants droit" def __unicode__(self): - return u'%s %s [%s]' % (self.nom.upper(), self.prenom, self.id) + return u'%s %s' % (self.nom.upper(), self.prenom, ) + + prefix_implantation = \ + "employe__dossiers__poste__implantation__zone_administrative" + + def get_zones_administratives(self): + return [ + d.poste.implantation.zone_administrative + for d in self.employe.dossiers.all() + ] - prefix_implantation = "employe__dossiers__poste__implantation__region" - def get_regions(self): - regions = [] - for d in self.employe.dossiers.all(): - regions.append(d.poste.implantation.region) - return regions +reversion.register(AyantDroit, format='xml', follow=['commentaires']) class AyantDroitCommentaire(Commentaire): - ayant_droit = models.ForeignKey('AyantDroit', db_column='ayant_droit', - related_name='+') + ayant_droit = models.ForeignKey( + 'AyantDroit', db_column='ayant_droit', related_name='commentaires' + ) + +reversion.register(AyantDroitCommentaire, format='xml') ### DOSSIER @@ -545,8 +802,10 @@ COMPTE_COMPTA_CHOICES = ( ('aucun', 'Aucun'), ) -class Dossier_(AUFMetadata): - """Le Dossier regroupe les informations relatives à l'occupation + +class Dossier_(DateActiviteMixin, models.Model, DevisableMixin,): + """ + Le Dossier regroupe les informations relatives à l'occupation d'un Poste par un Employe. Un seul Dossier existe par Poste occupé par un Employe. @@ -559,47 +818,62 @@ class Dossier_(AUFMetadata): # TODO: OneToOne ?? statut = models.ForeignKey('Statut', related_name='+', null=True) - organisme_bstg = models.ForeignKey('OrganismeBstg', - db_column='organisme_bstg', - related_name='+', - verbose_name = u"Organisme", - help_text="Si détaché (DET) ou \ - mis à disposition (MAD), \ - préciser l'organisme.", - null=True, blank=True) + organisme_bstg = models.ForeignKey( + 'OrganismeBstg', db_column='organisme_bstg', related_name='+', + verbose_name=u"organisme", + help_text=( + u"Si détaché (DET) ou mis à disposition (MAD), " + u"préciser l'organisme." + ), null=True, blank=True + ) # Recrutement remplacement = models.BooleanField(default=False) - remplacement_de = models.ForeignKey('self', related_name='+', - help_text=u"Taper le nom de l'employé", - null=True, blank=True) - statut_residence = models.CharField(max_length=10, default='local', - verbose_name = u"Statut", null=True, - choices=STATUT_RESIDENCE_CHOICES) + remplacement_de = models.ForeignKey( + 'self', related_name='+', help_text=u"Taper le nom de l'employé", + null=True, blank=True + ) + statut_residence = models.CharField( + u"statut", max_length=10, default='local', null=True, + choices=STATUT_RESIDENCE_CHOICES + ) # Rémunération - classement = models.ForeignKey('Classement', db_column='classement', - related_name='+', - null=True, blank=True) - regime_travail = models.DecimalField(max_digits=12, null=True, - decimal_places=2, - default=REGIME_TRAVAIL_DEFAULT, - verbose_name = u"Régime de travail", - help_text="% du temps complet") - regime_travail_nb_heure_semaine = models.DecimalField(max_digits=12, - decimal_places=2, null=True, - default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT, - verbose_name = u"Nb. heures par semaine") + classement = models.ForeignKey( + 'Classement', db_column='classement', related_name='+', null=True, + blank=True + ) + regime_travail = models.DecimalField( + u"régime de travail", max_digits=12, null=True, decimal_places=2, + default=REGIME_TRAVAIL_DEFAULT, help_text="% du temps complet" + ) + regime_travail_nb_heure_semaine = models.DecimalField( + u"nb. heures par semaine", max_digits=12, + decimal_places=2, null=True, + default=REGIME_TRAVAIL_NB_HEURE_SEMAINE_DEFAULT, + help_text=REGIME_TRAVAIL_NB_HEURE_SEMAINE_HELP_TEXT + ) # Occupation du Poste par cet Employe (anciennement "mandat") - date_debut = models.DateField(verbose_name = u"Date de début d'occupation \ - de poste",) - date_fin = models.DateField(verbose_name = u"Date de fin d'occupation \ - de poste", - null=True, blank=True) + date_debut = models.DateField( + u"date de début d'occupation de poste", db_index=True + ) + date_fin = models.DateField( + u"Date de fin d'occupation de poste", null=True, blank=True, + db_index=True + ) + + # Meta-data: + est_cadre = models.BooleanField( + u"Est un cadre?", + default=False, + ) # Comptes - # TODO? + compte_compta = models.CharField(max_length=10, default='aucun', + verbose_name=u'Compte comptabilité', + choices=COMPTE_COMPTA_CHOICES) + compte_courriel = models.BooleanField() class Meta: abstract = True @@ -615,7 +889,7 @@ class Dossier_(AUFMetadata): montant = coeff * point.valeur devise = point.devise - return {'montant':montant, 'devise':devise} + return {'montant': montant, 'devise': devise} def __unicode__(self): poste = self.poste.nom @@ -623,45 +897,369 @@ class Dossier_(AUFMetadata): poste = self.poste.nom_feminin return u'%s - %s' % (self.employe, poste) - prefix_implantation = "poste__implantation__region" - def get_regions(self): - return [self.poste.implantation.region] + prefix_implantation = "poste__implantation__zone_administrative" + def get_zones_administratives(self): + return [self.poste.implantation.zone_administrative] def remunerations(self): - return self.rh_remunerations.all().order_by('date_debut') + key = "%s_remunerations" % self._meta.app_label + remunerations = getattr(self, key) + return remunerations.all().order_by('-date_debut') def remunerations_en_cours(self): - return self.rh_remunerations.all().filter(date_fin__exact=None).order_by('date_debut') + q = Q(date_fin__exact=None) | Q(date_fin__gt=datetime.date.today()) + return self.remunerations().all().filter(q).order_by('date_debut') def get_salaire(self): try: - return [r for r in self.remunerations().order_by('-date_debut') if r.type_id == 1][0] + return [r for r in self.remunerations().order_by('-date_debut') + if r.type_id == 1][0] except: return None + def get_salaire_euros(self): + tx = self.taux_devise() + return (float)(tx) * (float)(self.salaire) + + def get_remunerations_brutes(self): + """ + 1 Salaire de base + 3 Indemnité de base + 4 Indemnité d'expatriation + 5 Indemnité pour frais + 6 Indemnité de logement + 7 Indemnité de fonction + 8 Indemnité de responsabilité + 9 Indemnité de transport + 10 Indemnité compensatrice + 11 Indemnité de subsistance + 12 Indemnité différentielle + 13 Prime d'installation + 14 Billet d'avion + 15 Déménagement + 16 Indemnité de départ + 18 Prime de 13ième mois + 19 Prime d'intérim + """ + ids = [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19] + return [r for r in self.remunerations_en_cours().all() + if r.type_id in ids] + + def get_charges_salariales(self): + """ + 20 Charges salariales ? + """ + ids = [20] + return [r for r in self.remunerations_en_cours().all() + if r.type_id in ids] + + def get_charges_patronales(self): + """ + 17 Charges patronales + """ + ids = [17] + return [r for r in self.remunerations_en_cours().all() + if r.type_id in ids] + + def get_remunerations_tierces(self): + """ + 2 Salaire MAD + """ + return [r for r in self.remunerations_en_cours().all() + if r.type_id in (2,)] + + # DEVISE LOCALE + + def get_total_local_charges_salariales(self): + devise = self.poste.get_devise() + total = 0.0 + for r in self.get_charges_salariales(): + if r.devise != devise: + return None + total += float(r.montant) + return total + + def get_total_local_charges_patronales(self): + devise = self.poste.get_devise() + total = 0.0 + for r in self.get_charges_patronales(): + if r.devise != devise: + return None + total += float(r.montant) + return total + + def get_local_salaire_brut(self): + """ + somme des rémuérations brutes + """ + devise = self.poste.get_devise() + total = 0.0 + for r in self.get_remunerations_brutes(): + if r.devise != devise: + return None + total += float(r.montant) + return total + + def get_local_salaire_net(self): + """ + salaire brut - charges salariales + """ + devise = self.poste.get_devise() + total_charges = 0.0 + for r in self.get_charges_salariales(): + if r.devise != devise: + return None + total_charges += float(r.montant) + return self.get_local_salaire_brut() - total_charges + + def get_local_couts_auf(self): + """ + salaire net + charges patronales + """ + devise = self.poste.get_devise() + total_charges = 0.0 + for r in self.get_charges_patronales(): + if r.devise != devise: + return None + total_charges += float(r.montant) + return self.get_local_salaire_net() + total_charges + + def get_total_local_remunerations_tierces(self): + devise = self.poste.get_devise() + total = 0.0 + for r in self.get_remunerations_tierces(): + if r.devise != devise: + return None + total += float(r.montant) + return total + + # DEVISE EURO + + def get_total_charges_salariales(self): + total = 0.0 + for r in self.get_charges_salariales(): + total += r.montant_euros() + return total + + def get_total_charges_patronales(self): + total = 0.0 + for r in self.get_charges_patronales(): + total += r.montant_euros() + return total + + def get_salaire_brut(self): + """ + somme des rémuérations brutes + """ + total = 0.0 + for r in self.get_remunerations_brutes(): + total += r.montant_euros() + return total + + def get_salaire_net(self): + """ + salaire brut - charges salariales + """ + total_charges = 0.0 + for r in self.get_charges_salariales(): + total_charges += r.montant_euros() + return self.get_salaire_brut() - total_charges + + def get_couts_auf(self): + """ + salaire net + charges patronales + """ + total_charges = 0.0 + for r in self.get_charges_patronales(): + total_charges += r.montant_euros() + return self.get_salaire_net() + total_charges + + def get_total_remunerations_tierces(self): + total = 0.0 + for r in self.get_remunerations_tierces(): + total += r.montant_euros() + return total + + def premier_contrat(self): + """contrat avec plus petite date de début""" + try: + contrat = self.rh_contrats.exclude(date_debut=None) \ + .order_by('date_debut')[0] + except IndexError, Contrat.DoesNotExist: + contrat = None + return contrat + + def dernier_contrat(self): + """contrat avec plus grande date de fin""" + try: + contrat = self.rh_contrats.exclude(date_debut=None) \ + .order_by('-date_debut')[0] + except IndexError, Contrat.DoesNotExist: + contrat = None + return contrat + + def actif(self): + today = date.today() + return (self.date_debut is None or self.date_debut <= today) \ + and (self.date_fin is None or self.date_fin >= today) \ + and not (self.date_fin is None and self.date_debut is None) + + class Dossier(Dossier_): __doc__ = Dossier_.__doc__ - poste = models.ForeignKey('%s.Poste' % app_context(), - db_column='poste', - related_name='%(app_label)s_dossiers', + poste = models.ForeignKey( + Poste, db_column='poste', related_name='rh_dossiers', help_text=u"Taper le nom du poste ou du type de poste", + ) + employe = models.ForeignKey( + 'Employe', db_column='employe', + help_text=u"Taper le nom de l'employé", + related_name='rh_dossiers', verbose_name=u"employé" + ) + principal = models.BooleanField( + u"dossier principal", default=True, + help_text=( + u"Ce dossier est pour le principal poste occupé par l'employé" ) - employe = models.ForeignKey('Employe', db_column='employe', - help_text=u"Taper le nom de l'employé", - related_name='%(app_label)s_dossiers', - verbose_name=u"Employé") + ) + + +reversion.register(Dossier, format='xml', follow=[ + 'rh_dossierpieces', 'rh_comparaisons', 'rh_remunerations', + 'rh_contrats', 'commentaires' +]) + + +class RHDossierClassementRecord(models.Model): + classement = models.ForeignKey( + 'Classement', + related_name='classement_records', + ) + dossier = models.ForeignKey( + 'Dossier', + related_name='classement_records', + ) + date_debut = models.DateField( + u"date de début", + help_text=HELP_TEXT_DATE, + null=True, + blank=True, + db_index=True + ) + date_fin = models.DateField( + u"date de fin", + help_text=HELP_TEXT_DATE, + null=True, + blank=True, + db_index=True + ) + commentaire = models.CharField( + max_length=2048, + blank=True, + null=True, + default='', + ) + + def __unicode__(self): + return self.classement.__unicode__() + + class Meta: + verbose_name = u"Element d'historique de classement" + verbose_name_plural = u"Historique de classement" + + @classmethod + def post_save_handler(cls, + sender, + instance, + created, + using, + **kw): + + today = date.today() + previous_record = None + previous_classement = None + has_changed = False + + # Premièrement, pour les nouvelles instances: + if created: + if not instance.classement: + return + else: + cls.objects.create( + date_debut=instance.date_debut, + classement=instance.classement, + dossier=instance, + ) + return + + # Deuxièmement, pour les instances existantes: + + # Détermine si: + # 1. Est-ce que le classement a changé? + # 2. Est-ce qu'une historique de classement existe déjà + try: + previous_record = cls.objects.get( + dossier=instance, + classement=instance.before_save.classement, + date_fin=None, + ) + except cls.DoesNotExist: + if instance.before_save.classement: + # Il était censé avoir une historique de classement + # donc on le créé. + previous_record = cls.objects.create( + date_debut=instance.before_save.date_debut, + classement=instance.before_save.classement, + dossier=instance, + ) + previous_classement = instance.before_save.classement + except MultipleObjectsReturned: + qs = cls.objects.filter( + dossier=instance, + classement=instance.before_save.classement, + date_fin=None, + ) + latest = qs.latest('date_debut') + qs.exclude(id=latest.id).update(date_fin=today) + previous_record = latest + previous_classement = latest.classement + else: + previous_classement = previous_record.classement + + has_changed = ( + instance.classement != + previous_classement + ) + + # Cas aucun changement: + if not has_changed: + return + + else: + # Classement a changé + if previous_record: + previous_record.date_fin = today + previous_record.save() + + if instance.classement: + cls.objects.create( + date_debut=today, + classement=instance.classement, + dossier=instance, + ) class DossierPiece_(models.Model): - """Documents relatifs au Dossier (à l'occupation de ce poste par employé). + """ + Documents relatifs au Dossier (à l'occupation de ce poste par employé). Ex.: Lettre de motivation. """ - dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='+') - nom = models.CharField(verbose_name = u"Nom", max_length=255) - fichier = models.FileField(verbose_name = u"Fichier", - upload_to=dossier_piece_dispatch, - storage=storage_prive) + nom = models.CharField(max_length=255) + fichier = models.FileField( + upload_to=dossier_piece_dispatch, storage=storage_prive, + max_length=255 + ) class Meta: abstract = True @@ -670,72 +1268,82 @@ class DossierPiece_(models.Model): def __unicode__(self): return u'%s' % (self.nom) + class DossierPiece(DossierPiece_): - pass + dossier = models.ForeignKey( + Dossier, db_column='dossier', related_name='rh_dossierpieces' + ) -class DossierCommentaire_(Commentaire): - dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='+') - class Meta: - abstract = True +reversion.register(DossierPiece, format='xml') + +class DossierCommentaire(Commentaire): + dossier = models.ForeignKey( + Dossier, db_column='dossier', related_name='commentaires' + ) + +reversion.register(DossierCommentaire, format='xml') -class DossierCommentaire(DossierCommentaire_): - pass -class DossierComparaison_(models.Model): +class DossierComparaison_(models.Model, DevisableMixin): """ Photo d'une comparaison salariale au moment de l'embauche. """ - dossier = models.ForeignKey('%s.Dossier' % app_context(), related_name='%(app_label)s_comparaisons') objects = DossierComparaisonManager() - implantation = models.ForeignKey(ref.Implantation, related_name="+", null=True, blank=True) + implantation = models.ForeignKey( + ref.Implantation, related_name="+", null=True, blank=True + ) poste = models.CharField(max_length=255, null=True, blank=True) personne = models.CharField(max_length=255, null=True, blank=True) montant = models.IntegerField(null=True) - devise = models.ForeignKey('Devise', related_name='+', null=True, blank=True) + devise = models.ForeignKey( + 'Devise', related_name='+', null=True, blank=True + ) class Meta: abstract = True - def taux_devise(self): - annee = self.dossier.poste.date_debut.year - taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise, annee=annee)] - taux = set(taux) - if len(taux) != 1: - raise Exception(u"Le taux de la devise %s n'a pas n'existe pas pour %s ou il existe plusieurs taux pour la même année" % (self.devise.id, annee)) - else: - return list(taux)[0] + def __unicode__(self): + return "%s (%s)" % (self.poste, self.personne) - def montant_euros(self): - return round(float(self.montant) * float(self.taux_devise()), 2) class DossierComparaison(DossierComparaison_): - pass + dossier = models.ForeignKey( + Dossier, related_name='rh_comparaisons' + ) + +reversion.register(DossierComparaison, format='xml') + ### RÉMUNÉRATION -class RemunerationMixin(AUFMetadata): - dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_remunerations') +class RemunerationMixin(models.Model): + # Identification - type = models.ForeignKey('TypeRemuneration', db_column='type', - related_name='+', - verbose_name = u"Type de rémunération") - type_revalorisation = models.ForeignKey('TypeRevalorisation', - db_column='type_revalorisation', - related_name='+', - verbose_name = u"Type de revalorisation", - null=True, blank=True) - montant = models.DecimalField(null=True, blank=True, - default=0, max_digits=12, decimal_places=2) - # Annuel (12 mois, 52 semaines, 364 jours?) - devise = models.ForeignKey('Devise', db_column='devise', related_name='+',) + type = models.ForeignKey( + 'TypeRemuneration', db_column='type', related_name='+', + verbose_name=u"type de rémunération" + ) + type_revalorisation = models.ForeignKey( + 'TypeRevalorisation', db_column='type_revalorisation', + related_name='+', verbose_name=u"type de revalorisation", + null=True, blank=True + ) + montant = models.DecimalField( + null=True, blank=True, max_digits=12, decimal_places=2 + ) # Annuel (12 mois, 52 semaines, 364 jours?) + devise = models.ForeignKey('Devise', db_column='devise', related_name='+') + # commentaire = precision commentaire = models.CharField(max_length=255, null=True, blank=True) + # date_debut = anciennement date_effectif - date_debut = models.DateField(verbose_name = u"Date de début", - null=True, blank=True) - date_fin = models.DateField(verbose_name = u"Date de fin", - null=True, blank=True) + date_debut = models.DateField( + u"date de début", null=True, blank=True, db_index=True + ) + date_fin = models.DateField( + u"date de fin", null=True, blank=True, db_index=True + ) class Meta: abstract = True @@ -744,39 +1352,81 @@ class RemunerationMixin(AUFMetadata): def __unicode__(self): return u'%s %s (%s)' % (self.montant, self.devise.code, self.type.nom) -class Remuneration_(RemunerationMixin): - """Structure de rémunération (données budgétaires) en situation normale + +class Remuneration_(RemunerationMixin, DevisableMixin): + """ + Structure de rémunération (données budgétaires) en situation normale pour un Dossier. Si un Evenement existe, utiliser la structure de rémunération EvenementRemuneration de cet événement. """ + objects = RemunerationManager() - def montant_mois(self): - return round(self.montant / 12, 2) + @staticmethod + def find_yearly_range(from_date, to_date, year): + today = date.today() + year = year or date.today().year + year_start = date(year, 1, 1) + year_end = date(year, 12, 31) + + def constrain_to_year(*dates): + """ + S'assure que les dates soient dans le range year_start a + year_end + """ + return [min(max(year_start, d), year_end) + for d in dates] + + start_date = max( + from_date or year_start, year_start) + end_date = min( + to_date or year_end, year_end) + + start_date, end_date = constrain_to_year(start_date, end_date) - def taux_devise(self): - if self.devise.code == "EUR": - return 1 + jours_annee = (year_end - year_start).days + jours_dates = (end_date - start_date).days + factor = Decimal(str(jours_dates)) / Decimal(str(jours_annee)) + + return start_date, end_date, factor + - annee = datetime.datetime.now().year - if self.dossier.poste.date_debut is not None: - annee = self.dossier.poste.date_debut.year - if self.dossier.date_debut is not None: - annee = self.dossier.date_debut.year - if self.date_debut is not None: - annee = self.date_debut.year - - taux = [tc.taux for tc in TauxChange.objects.filter(devise=self.devise_id, annee=annee)] - taux = set(taux) - if len(taux) != 1: - raise Exception(u"Le taux de la devise %s n'a pas n'existe pas pour %s ou il existe plusieurs taux pour la même année %s (%s)" % (self.devise.code, annee, taux, self.dossier)) + def montant_ajuste_euros(self, annee=None): + """ + Le montant ajusté représente le montant annuel, ajusté sur la + période de temps travaillée, multipliée par le ratio de temps + travaillé (en rapport au temps plein). + """ + date_debut, date_fin, factor = self.find_yearly_range( + self.date_debut, + self.date_fin, + annee, + ) + + montant_euros = Decimal(str(self.montant_euros_float()) or '0') + + if self.type.nature_remuneration != u'Accessoire': + dossier = getattr(self, 'dossier', None) + if not dossier: + """ + Dans le cas d'un DossierComparaisonRemuneration, il + n'y a plus de reference au dossier. + """ + regime_travail = REGIME_TRAVAIL_DEFAULT + else: + regime_travail = self.dossier.regime_travail + return (montant_euros * factor * + regime_travail / 100) else: - return list(taux)[0] + return montant_euros + + def montant_mois(self): + return round(self.montant / 12, 2) - def montant_euro(self): - return round(float(self.montant) * float(self.taux_devise()), 2) + def montant_avec_regime(self): + return round(self.montant * (self.dossier.regime_travail / 100), 2) def montant_euro_mois(self): - return round(self.montant_euro() / 12, 2) + return round(self.montant_euros() / 12, 2) def __unicode__(self): try: @@ -792,33 +1442,36 @@ class Remuneration_(RemunerationMixin): class Remuneration(Remuneration_): - pass + dossier = models.ForeignKey( + Dossier, db_column='dossier', related_name='rh_remunerations' + ) +reversion.register(Remuneration, format='xml') -### CONTRATS -class ContratManager(NoDeleteManager): - def get_query_set(self): - return super(ContratManager, self).get_query_set().select_related('dossier', 'dossier__poste') +### CONTRATS - -class Contrat_(AUFMetadata): - """Document juridique qui encadre la relation de travail d'un Employe +class Contrat_(models.Model): + """ + Document juridique qui encadre la relation de travail d'un Employe pour un Poste particulier. Pour un Dossier (qui documente cette relation de travail) plusieurs contrats peuvent être associés. """ objects = ContratManager() - dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', related_name='%(app_label)s_contrats') - type_contrat = models.ForeignKey('TypeContrat', db_column='type_contrat', - related_name='+', - verbose_name = u"type de contrat") - date_debut = models.DateField(verbose_name = u"Date de début") - date_fin = models.DateField(verbose_name = u"Date de fin", - null=True, blank=True) - fichier = models.FileField(verbose_name = u"Fichier", - upload_to=contrat_dispatch, - storage=storage_prive, - null=True, blank=True) + type_contrat = models.ForeignKey( + 'TypeContrat', db_column='type_contrat', + verbose_name=u'type de contrat', related_name='+' + ) + date_debut = models.DateField( + u"date de début", db_index=True + ) + date_fin = models.DateField( + u"date de fin", null=True, blank=True, db_index=True + ) + fichier = models.FileField( + upload_to=contrat_dispatch, storage=storage_prive, null=True, + blank=True, max_length=255 + ) class Meta: abstract = True @@ -829,104 +1482,69 @@ class Contrat_(AUFMetadata): def __unicode__(self): return u'%s - %s' % (self.dossier, self.id) + class Contrat(Contrat_): - pass - + dossier = models.ForeignKey( + Dossier, db_column='dossier', related_name='rh_contrats' + ) -### ÉVÉNEMENTS - -#class Evenement_(AUFMetadata): -# """Un Evenement sert à déclarer une situation temporaire (exceptionnelle) -# d'un Dossier qui vient altérer des informations normales liées à un Dossier -# (ex.: la Remuneration). -# -# Ex.: congé de maternité, maladie... -# -# Lors de ces situations exceptionnelles, l'Employe a un régime de travail -# différent et une rémunération en conséquence. On souhaite toutefois -# conserver le Dossier intact afin d'éviter une re-saisie des données lors -# du retour à la normale. -# """ -# dossier = models.ForeignKey('%s.Dossier' % app_context(), db_column='dossier', -# related_name='+') -# nom = models.CharField(max_length=255) -# date_debut = models.DateField(verbose_name = u"Date de début") -# date_fin = models.DateField(verbose_name = u"Date de fin", -# null=True, blank=True) -# -# class Meta: -# abstract = True -# ordering = ['nom'] -# verbose_name = u"Évènement" -# verbose_name_plural = u"Évènements" -# -# def __unicode__(self): -# return u'%s' % (self.nom) -# -# -#class Evenement(Evenement_): -# __doc__ = Evenement_.__doc__ -# -# -#class EvenementRemuneration_(RemunerationMixin): -# """Structure de rémunération liée à un Evenement qui remplace -# temporairement la Remuneration normale d'un Dossier, pour toute la durée -# de l'Evenement. -# """ -# evenement = models.ForeignKey("Evenement", db_column='evenement', -# related_name='+', -# verbose_name = u"Évènement") -# # TODO : le champ dossier hérité de Remuneration doit être dérivé -# # de l'Evenement associé -# -# class Meta: -# abstract = True -# ordering = ['evenement', 'type__nom', '-date_fin'] -# verbose_name = u"Évènement - rémunération" -# verbose_name_plural = u"Évènements - rémunérations" -# -# -#class EvenementRemuneration(EvenementRemuneration_): -# __doc__ = EvenementRemuneration_.__doc__ -# -# class Meta: -# abstract = True -# -# -#class EvenementRemuneration(EvenementRemuneration_): -# __doc__ = EvenementRemuneration_.__doc__ -# TODO? class ContratPiece(models.Model): +reversion.register(Contrat, format='xml') ### RÉFÉRENCES RH -class FamilleEmploi(AUFMetadata): - """Catégorie utilisée dans la gestion des Postes. +class CategorieEmploi(models.Model): + """ + Catégorie utilisée dans la gestion des Postes. Catégorie supérieure à TypePoste. """ nom = models.CharField(max_length=255) class Meta: - ordering = ['nom'] - verbose_name = u"Famille d'emploi" - verbose_name_plural = u"Familles d'emploi" + ordering = ('nom',) + verbose_name = u"catégorie d'emploi" + verbose_name_plural = u"catégories d'emploi" def __unicode__(self): - return u'%s' % (self.nom) + return self.nom + +reversion.register(CategorieEmploi, format='xml') -class TypePoste(AUFMetadata): - """Catégorie de Poste. + +class FamilleProfessionnelle(models.Model): """ - nom = models.CharField(max_length=255) - nom_feminin = models.CharField(max_length=255, - verbose_name = u"Nom féminin") + Famille professionnelle d'un poste. + """ + nom = models.CharField(max_length=100) + + class Meta: + ordering = ('nom',) + verbose_name = u'famille professionnelle' + verbose_name_plural = u'familles professionnelles' + + def __unicode__(self): + return self.nom - is_responsable = models.BooleanField(default=False, - verbose_name = u"Poste de responsabilité") - famille_emploi = models.ForeignKey('FamilleEmploi', - db_column='famille_emploi', - related_name='+', - verbose_name = u"famille d'emploi") +reversion.register(FamilleProfessionnelle, format='xml') + + +class TypePoste(Archivable): + """ + Catégorie de Poste. + """ + nom = models.CharField(max_length=255) + nom_feminin = models.CharField(u"nom féminin", max_length=255) + is_responsable = models.BooleanField( + u"poste de responsabilité", default=False + ) + categorie_emploi = models.ForeignKey( + CategorieEmploi, db_column='categorie_emploi', related_name='+', + verbose_name=u"catégorie d'emploi" + ) + famille_professionnelle = models.ForeignKey( + FamilleProfessionnelle, related_name='types_de_poste', + verbose_name=u"famille professionnelle", blank=True, null=True + ) class Meta: ordering = ['nom'] @@ -936,6 +1554,8 @@ class TypePoste(AUFMetadata): def __unicode__(self): return u'%s' % (self.nom) +reversion.register(TypePoste, format='xml') + TYPE_PAIEMENT_CHOICES = ( (u'Régulier', u'Régulier'), @@ -943,23 +1563,31 @@ TYPE_PAIEMENT_CHOICES = ( ) NATURE_REMUNERATION_CHOICES = ( - (u'Accessoire', u'Accessoire'), - (u'Charges', u'Charges'), - (u'Indemnité', u'Indemnité'), - (u'RAS', u'Rémunération autre source'), (u'Traitement', u'Traitement'), + (u'Indemnité', u'Indemnités autres'), + (u'Charges', u'Charges patronales'), + (u'Accessoire', u'Accessoires'), + (u'RAS', u'Rémunération autre source'), ) -class TypeRemuneration(AUFMetadata): - """Catégorie de Remuneration. + +class TypeRemuneration(Archivable): + """ + Catégorie de Remuneration. """ + + objects = models.Manager() + sans_archives = ArchivableManager() + nom = models.CharField(max_length=255) - type_paiement = models.CharField(max_length=30, - choices=TYPE_PAIEMENT_CHOICES, - verbose_name = u"Type de paiement") - nature_remuneration = models.CharField(max_length=30, - choices=NATURE_REMUNERATION_CHOICES, - verbose_name = u"Nature de la rémunération") + type_paiement = models.CharField( + u"type de paiement", max_length=30, choices=TYPE_PAIEMENT_CHOICES + ) + + nature_remuneration = models.CharField( + u"nature de la rémunération", max_length=30, + choices=NATURE_REMUNERATION_CHOICES + ) class Meta: ordering = ['nom'] @@ -967,10 +1595,14 @@ class TypeRemuneration(AUFMetadata): verbose_name_plural = u"Types de rémunération" def __unicode__(self): - return u'%s' % (self.nom) + return self.nom -class TypeRevalorisation(AUFMetadata): - """Justification du changement de la Remuneration. +reversion.register(TypeRemuneration, format='xml') + + +class TypeRevalorisation(Archivable): + """ + Justification du changement de la Remuneration. (Actuellement utilisé dans aucun traitement informatique.) """ nom = models.CharField(max_length=255) @@ -983,23 +1615,24 @@ class TypeRevalorisation(AUFMetadata): def __unicode__(self): return u'%s' % (self.nom) -class Service(AUFMetadata): - """Unité administrative où les Postes sont rattachés. +reversion.register(TypeRevalorisation, format='xml') + + +class Service(Archivable): + """ + Unité administrative où les Postes sont rattachés. """ - archive = models.BooleanField(verbose_name=u"Archivé", default=False) nom = models.CharField(max_length=255) class Meta: ordering = ['nom'] - verbose_name = u"Service" - verbose_name_plural = u"Services" + verbose_name = u"service" + verbose_name_plural = u"services" def __unicode__(self): - if self.archive: - archive = u"(archivé)" - else: - archive = "" - return u'%s %s' % (self.nom, archive) + return self.nom + +reversion.register(Service, format='xml') TYPE_ORGANISME_CHOICES = ( @@ -1007,8 +1640,10 @@ TYPE_ORGANISME_CHOICES = ( ('DET', 'Détachement'), ) -class OrganismeBstg(AUFMetadata): - """Organisation d'où provient un Employe mis à disposition (MAD) de + +class OrganismeBstg(models.Model): + """ + Organisation d'où provient un Employe mis à disposition (MAD) de ou détaché (DET) à l'AUF à titre gratuit. (BSTG = bien et service à titre gratuit.) @@ -1028,16 +1663,23 @@ class OrganismeBstg(AUFMetadata): def __unicode__(self): return u'%s (%s)' % (self.nom, self.get_type_display()) - prefix_implantation = "pays__region" - def get_regions(self): - return [self.pays.region] +reversion.register(OrganismeBstg, format='xml') -class Statut(AUFMetadata): - """Statut de l'Employe dans le cadre d'un Dossier particulier. +class Statut(Archivable): + """ + Statut de l'Employe dans le cadre d'un Dossier particulier. """ # Identification - code = models.CharField(max_length=25, unique=True, help_text="Saisir un code court mais lisible pour ce statut : le code est utilisé pour associer les statuts aux autres données tout en demeurant plus lisible qu'un identifiant numérique.") + code = models.CharField( + max_length=25, unique=True, + help_text=( + u"Saisir un code court mais lisible pour ce statut : " + u"le code est utilisé pour associer les statuts aux autres " + u"données tout en demeurant plus lisible qu'un identifiant " + u"numérique." + ) + ) nom = models.CharField(max_length=255) class Meta: @@ -1048,6 +1690,8 @@ class Statut(AUFMetadata): def __unicode__(self): return u'%s : %s' % (self.code, self.nom) +reversion.register(Statut, format='xml') + TYPE_CLASSEMENT_CHOICES = ( ('S', 'S -Soutien'), @@ -1059,19 +1703,28 @@ TYPE_CLASSEMENT_CHOICES = ( ('HG', 'HG - Hors grille [direction]'), ) + class ClassementManager(models.Manager): """ Ordonner les spcéfiquement les classements. """ def get_query_set(self): - qs = super(self.__class__, self).get_query_set() - qs = qs.extra(select={'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")'}) - qs = qs.extra(order_by=('ponderation', )) + qs = super(ClassementManager, self).get_query_set() + qs = qs.extra(select={ + 'ponderation': 'FIND_IN_SET(type,"SO,HG,S,T,P,C,D")' + }) + qs = qs.extra(order_by=('ponderation', 'echelon', 'degre', )) return qs.all() -class Classement_(AUFMetadata): - """Éléments de classement de la +class ClassementArchivableManager(ClassementManager, + ArchivableManager): + pass + + +class Classement_(Archivable): + """ + Éléments de classement de la "Grille générique de classement hiérarchique". Utile pour connaître, pour un Dossier, le salaire de base théorique lié au @@ -1080,44 +1733,50 @@ class Classement_(AUFMetadata): salaire de base = coefficient * valeur du point de l'Implantation du Poste """ objects = ClassementManager() + sans_archives = ClassementArchivableManager() # Identification type = models.CharField(max_length=10, choices=TYPE_CLASSEMENT_CHOICES) - echelon = models.IntegerField(verbose_name=u"Échelon", blank=True, default=0) - degre = models.IntegerField(verbose_name=u"Degré", blank=True, default=0) - coefficient = models.FloatField(default=0, verbose_name=u"Coefficient", - null=True) + echelon = models.IntegerField(u"échelon", blank=True, default=0) + degre = models.IntegerField(u"degré", blank=True, default=0) + coefficient = models.FloatField(u"coefficient", blank=True, null=True) + # Méta # annee # au lieu de date_debut et date_fin commentaire = models.TextField(null=True, blank=True) class Meta: abstract = True - ordering = ['type','echelon','degre','coefficient'] + ordering = ['type', 'echelon', 'degre', 'coefficient'] verbose_name = u"Classement" verbose_name_plural = u"Classements" def __unicode__(self): return u'%s.%s.%s' % (self.type, self.echelon, self.degre, ) + class Classement(Classement_): __doc__ = Classement_.__doc__ +reversion.register(Classement, format='xml') -class TauxChange_(AUFMetadata): - """Taux de change de la devise vers l'euro (EUR) + +class TauxChange_(models.Model): + """ + Taux de change de la devise vers l'euro (EUR) pour chaque année budgétaire. """ # Identification devise = models.ForeignKey('Devise', db_column='devise') - annee = models.IntegerField(verbose_name = u"Année") - taux = models.FloatField(verbose_name = u"Taux vers l'euro") + annee = models.IntegerField(u"année") + taux = models.FloatField(u"taux vers l'euro") class Meta: abstract = True ordering = ['-annee', 'devise__code'] verbose_name = u"Taux de change" verbose_name_plural = u"Taux de change" + unique_together = ('devise', 'annee') def __unicode__(self): return u'%s : %s € (%s)' % (self.devise, self.taux, self.annee) @@ -1126,21 +1785,26 @@ class TauxChange_(AUFMetadata): class TauxChange(TauxChange_): __doc__ = TauxChange_.__doc__ -class ValeurPointManager(NoDeleteManager): +reversion.register(TauxChange, format='xml') + + +class ValeurPointManager(models.Manager): def get_query_set(self): - now = datetime.datetime.now() - return super(ValeurPointManager, self).get_query_set().select_related('devise', 'implantation') + return super(ValeurPointManager, self).get_query_set() \ + .select_related('devise', 'implantation') -class ValeurPoint_(AUFMetadata): - """Utile pour connaître, pour un Dossier, le salaire de base théorique lié +class ValeurPoint_(models.Model): + """ + Utile pour connaître, pour un Dossier, le salaire de base théorique lié au classement dans la grille. La ValeurPoint s'obtient par l'implantation du Poste de ce Dossier : dossier.poste.implantation (pseudo code). salaire de base = coefficient * valeur du point de l'Implantation du Poste """ + objects = models.Manager() actuelles = ValeurPointManager() valeur = models.FloatField(null=True) @@ -1156,36 +1820,42 @@ class ValeurPoint_(AUFMetadata): abstract = True verbose_name = u"Valeur du point" verbose_name_plural = u"Valeurs du point" + unique_together = ('implantation', 'annee') def __unicode__(self): - return u'%s %s %s [%s] %s' % (self.devise.code, self.annee, self.valeur, self.implantation.nom_court, self.devise.nom) + return u'%s %s %s [%s] %s' % ( + self.devise.code, self.annee, self.valeur, + self.implantation.nom_court, self.devise.nom + ) class ValeurPoint(ValeurPoint_): __doc__ = ValeurPoint_.__doc__ +reversion.register(ValeurPoint, format='xml') -class Devise(AUFMetadata): - """Devise monétaire. +class Devise(Archivable): """ - - objects = DeviseManager() - - archive = models.BooleanField(verbose_name=u"Archivé", default=False) - code = models.CharField(max_length=10, unique=True) + Devise monétaire. + """ + code = models.CharField(max_length=10, unique=True) nom = models.CharField(max_length=255) class Meta: ordering = ['code'] - verbose_name = u"Devise" - verbose_name_plural = u"Devises" + verbose_name = u"devise" + verbose_name_plural = u"devises" def __unicode__(self): return u'%s - %s' % (self.code, self.nom) -class TypeContrat(AUFMetadata): - """Type de contrat. +reversion.register(Devise, format='xml') + + +class TypeContrat(Archivable): + """ + Type de contrat. """ nom = models.CharField(max_length=255) nom_long = models.CharField(max_length=255) @@ -1198,19 +1868,36 @@ class TypeContrat(AUFMetadata): def __unicode__(self): return u'%s' % (self.nom) +reversion.register(TypeContrat, format='xml') + ### AUTRES -class ResponsableImplantation(AUFMetadata): - """Le responsable d'une implantation. +class ResponsableImplantationProxy(ref.Implantation): + + def save(self): + pass + + class Meta: + managed = False + proxy = True + verbose_name = u"Responsable d'implantation" + verbose_name_plural = u"Responsables d'implantation" + + +class ResponsableImplantation(models.Model): + """ + Le responsable d'une implantation. Anciennement géré sur le Dossier du responsable. """ - employe = models.ForeignKey('Employe', db_column='employe', - related_name='+', - null=True, blank=True) - implantation = models.ForeignKey(ref.Implantation, - db_column='implantation', related_name='+', - unique=True) + employe = models.ForeignKey( + 'Employe', db_column='employe', related_name='+', null=True, + blank=True + ) + implantation = models.OneToOneField( + "ResponsableImplantationProxy", db_column='implantation', + related_name='responsable', unique=True + ) def __unicode__(self): return u'%s : %s' % (self.implantation, self.employe) @@ -1220,3 +1907,222 @@ class ResponsableImplantation(AUFMetadata): verbose_name = "Responsable d'implantation" verbose_name_plural = "Responsables d'implantation" +reversion.register(ResponsableImplantation, format='xml') + + +class UserProfile(models.Model): + user = models.OneToOneField(User, related_name='profile') + zones_administratives = models.ManyToManyField( + ref.ZoneAdministrative, + related_name='profiles' + ) + class Meta: + verbose_name = "Permissions sur zones administratives" + verbose_name_plural = "Permissions sur zones administratives" + + def __unicode__(self): + return self.user.__unicode__() + +reversion.register(UserProfile, format='xml') + + + +TYPES_CHANGEMENT = ( + ('NO', 'Arrivée'), + ('MO', 'Mobilité'), + ('DE', 'Départ'), + ) + + +class ChangementPersonnelNotifications(models.Model): + class Meta: + verbose_name = u"Destinataire pour notices de mouvement de personnel" + verbose_name_plural = u"Destinataires pour notices de mouvement de personnel" + + type = models.CharField( + max_length=2, + choices = TYPES_CHANGEMENT, + unique=True, + ) + + destinataires = models.ManyToManyField( + ref.Employe, + related_name='changement_notifications', + ) + + def __unicode__(self): + return '%s: %s' % ( + self.get_type_display(), ','.join( + self.destinataires.all().values_list( + 'courriel', flat=True)) + ) + + +class ChangementPersonnel(models.Model): + """ + Une notice qui enregistre un changement de personnel, incluant: + + * Nouveaux employés + * Mouvement de personnel + * Départ d'employé + """ + + class Meta: + verbose_name = u"Mouvement de personnel" + verbose_name_plural = u"Mouvements de personnel" + + def __unicode__(self): + return '%s: %s' % (self.dossier.__unicode__(), + self.get_type_display()) + + @classmethod + def create_changement(cls, dossier, type): + # If this employe has existing Changement, set them to invalid. + cls.objects.filter(dossier__employe=dossier.employe).update(valide=False) + + # Create a new one. + cls.objects.create( + dossier=dossier, + type=type, + valide=True, + communique=False, + ) + + + @classmethod + def post_save_handler(cls, + sender, + instance, + created, + using, + **kw): + + # This defines the time limit used when checking in previous + # files to see if an employee if new. Basically, if emloyee + # left his position new_file.date_debut - + # NEW_EMPLOYE_THRESHOLD +1 ago (compared to date_debut), then + # if a new file is created for this employee, he will bec + # onsidered "NEW" and a notice will be created to this effect. + NEW_EMPLOYE_THRESHOLD = datetime.timedelta(7) # 7 days. + + other_dossier_qs = instance.employe.rh_dossiers.exclude( + id=instance.id) + dd = instance.date_debut + df = instance.date_fin + today = date.today() + + # Here, verify differences between the instance, before and + # after the save. + df_has_changed = False + + if created: + if df != None: + df_has_changed = True + else: + df_has_changed = (df != instance.before_save.date_fin and + df != None) + + + # VERIFICATIONS: + + # Date de fin est None et c'est une nouvelle instance de + # Dossier + if not df and created: + # QS for finding other dossiers with a date_fin of None OR + # with a date_fin >= to this dossier's date_debut + exists_recent_file_qs = other_dossier_qs.filter( + Q(date_fin__isnull=True) | + Q(date_fin__gte=dd - NEW_EMPLOYE_THRESHOLD) + ) + + # 1. If existe un Dossier récent + if exists_recent_file_qs.count() > 0: + cls.create_changement( + instance, + 'MO', + ) + # 2. Il n'existe un Dossier récent, et c'est une nouvelle + # instance de Dossier: + else: + cls.create_changement( + instance, + 'NO', + ) + + elif not df and not created and cls.objects.filter( + valide=True, + date_creation__gte=today - NEW_EMPLOYE_THRESHOLD, + type='DE', + ).count() > 0: + cls.create_changement( + instance, + 'MO', + ) + + # Date de fin a été modifiée: + if df_has_changed: + # QS for other active files (date_fin == None), excludes + # instance. + exists_active_files_qs = other_dossier_qs.filter( + Q(date_fin__isnull=True)) + + # 3. Date de fin a été modifiée et il n'existe aucun autre + # dossier actifs: Depart + if exists_active_files_qs.count() == 0: + cls.create_changement( + instance, + 'DE', + ) + # 4. Dossier a une nouvelle date de fin par contre + # d'autres dossiers actifs existent déjà: Mouvement + else: + cls.create_changement( + instance, + 'MO', + ) + + + dossier = models.ForeignKey( + Dossier, + related_name='mouvements', + ) + + valide = models.BooleanField(default=True) + date_creation = models.DateTimeField( + auto_now_add=True) + communique = models.BooleanField( + u'Communiqué', + default=False, + ) + date_communication = models.DateTimeField( + null=True, + blank=True, + ) + + type = models.CharField( + max_length=2, + choices = TYPES_CHANGEMENT, + ) + +reversion.register(ChangementPersonnel, format='xml') + + +def dossier_pre_save_handler(sender, + instance, + using, + **kw): + # Store a copy of the model before save is called. + if instance.pk is not None: + instance.before_save = Dossier.objects.get(pk=instance.pk) + else: + instance.before_save = None + + +# Connect a pre_save handler that assigns a copy of the model as an +# attribute in order to compare it in post_save. +pre_save.connect(dossier_pre_save_handler, sender=Dossier) + +post_save.connect(ChangementPersonnel.post_save_handler, sender=Dossier) +post_save.connect(RHDossierClassementRecord.post_save_handler, sender=Dossier) + +