2016-01-29 18:06:33 +01:00
|
|
|
|
import json
|
2017-07-19 18:35:54 +02:00
|
|
|
|
from collections import OrderedDict
|
|
|
|
|
|
from datetime import date, timedelta
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2017-08-30 17:58:53 +02:00
|
|
|
|
from django.conf import settings
|
2012-11-06 17:54:33 +01:00
|
|
|
|
from django.db import models
|
|
|
|
|
|
|
2013-04-11 12:45:00 +02:00
|
|
|
|
from . import utils
|
|
|
|
|
|
|
2018-04-26 18:37:47 +02:00
|
|
|
|
CIVILITY_CHOICES = (
|
|
|
|
|
|
('Madame', 'Madame'),
|
|
|
|
|
|
('Monsieur', 'Monsieur'),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Section(models.Model):
|
|
|
|
|
|
""" Filières """
|
2012-11-22 17:48:41 +01:00
|
|
|
|
name = models.CharField(max_length=20, verbose_name='Nom')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Filière"
|
|
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2012-11-06 17:54:33 +01:00
|
|
|
|
return self.name
|
|
|
|
|
|
|
2018-02-06 09:04:10 +01:00
|
|
|
|
def is_fe(self):
|
|
|
|
|
|
"""fe=formation en entreprise"""
|
|
|
|
|
|
return self.name in {'ASA', 'ASE', 'ASSC'}
|
|
|
|
|
|
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2012-11-23 11:31:06 +01:00
|
|
|
|
class Level(models.Model):
|
|
|
|
|
|
name = models.CharField(max_length=10, verbose_name='Nom')
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Niveau"
|
2012-12-13 00:00:04 +01:00
|
|
|
|
verbose_name_plural = "Niveaux"
|
2012-11-23 11:31:06 +01:00
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2012-11-23 11:31:06 +01:00
|
|
|
|
return self.name
|
|
|
|
|
|
|
2013-04-11 12:45:00 +02:00
|
|
|
|
def delta(self, diff):
|
|
|
|
|
|
if diff == 0:
|
|
|
|
|
|
return self
|
|
|
|
|
|
try:
|
|
|
|
|
|
return Level.objects.get(name=str(int(self.name)+diff))
|
|
|
|
|
|
except Level.DoesNotExist:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2012-11-23 11:31:06 +01:00
|
|
|
|
|
2012-11-08 18:27:04 +01:00
|
|
|
|
class Klass(models.Model):
|
2017-07-19 10:44:12 +02:00
|
|
|
|
name = models.CharField(max_length=10, verbose_name='Nom', unique=True)
|
2016-08-31 15:01:00 +02:00
|
|
|
|
section = models.ForeignKey(Section, verbose_name='Filière', on_delete=models.PROTECT)
|
|
|
|
|
|
level = models.ForeignKey(Level, verbose_name='Niveau', on_delete=models.PROTECT)
|
2017-07-11 23:01:46 +02:00
|
|
|
|
teacher = models.ForeignKey('Teacher', blank=True, null=True,
|
|
|
|
|
|
on_delete=models.SET_NULL, verbose_name='Maître de classe')
|
2012-11-08 18:27:04 +01:00
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Classe"
|
|
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2012-11-08 18:27:04 +01:00
|
|
|
|
return self.name
|
|
|
|
|
|
|
2018-04-23 20:47:12 +02:00
|
|
|
|
def is_Ede_pe(self):
|
|
|
|
|
|
return 'EDEpe' in self.name
|
|
|
|
|
|
|
|
|
|
|
|
def is_Ede_ps(self):
|
|
|
|
|
|
return 'EDEps' in self.name
|
|
|
|
|
|
|
2012-11-08 18:27:04 +01:00
|
|
|
|
|
2017-07-11 23:01:46 +02:00
|
|
|
|
class Teacher(models.Model):
|
2018-04-26 18:37:47 +02:00
|
|
|
|
civility = models.CharField(max_length=10, choices=CIVILITY_CHOICES, verbose_name='Civilité')
|
2017-07-11 23:01:46 +02:00
|
|
|
|
first_name = models.CharField(max_length=40, verbose_name='Prénom')
|
|
|
|
|
|
last_name = models.CharField(max_length=40, verbose_name='Nom')
|
|
|
|
|
|
abrev = models.CharField(max_length=10, verbose_name='Sigle')
|
2017-07-14 18:27:43 +02:00
|
|
|
|
birth_date = models.DateField(verbose_name='Date de naissance', blank=True, null=True)
|
2017-07-11 23:01:46 +02:00
|
|
|
|
email = models.EmailField(verbose_name='Courriel', blank=True)
|
|
|
|
|
|
contract = models.CharField(max_length=20, verbose_name='Contrat')
|
|
|
|
|
|
rate = models.DecimalField(default=0.0, max_digits=4, decimal_places=1, verbose_name="Taux d'activité")
|
|
|
|
|
|
ext_id = models.IntegerField(blank=True, null=True)
|
|
|
|
|
|
previous_report = models.IntegerField(default=0, verbose_name='Report précédent')
|
|
|
|
|
|
next_report = models.IntegerField(default=0, verbose_name='Report suivant')
|
2017-07-18 12:48:10 +02:00
|
|
|
|
archived = models.BooleanField(default=False)
|
2017-07-11 23:01:46 +02:00
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name='Enseignant'
|
|
|
|
|
|
ordering = ('last_name', 'first_name')
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return '{0} {1}'.format(self.last_name, self.first_name)
|
|
|
|
|
|
|
2018-04-12 12:17:37 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def full_name(self):
|
|
|
|
|
|
return '{0} {1}'.format(self.first_name, self.last_name)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def civility_full_name(self):
|
|
|
|
|
|
return '{0} {1} {2}'.format(self.civility, self.first_name, self.last_name)
|
|
|
|
|
|
|
2018-04-12 12:37:17 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def role(self):
|
|
|
|
|
|
return {'Monsieur': 'enseignant-formateur', 'Madame': 'enseignante-formatrice'}.get(self.civility, '')
|
|
|
|
|
|
|
2017-07-14 18:47:56 +02:00
|
|
|
|
def calc_activity(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Return a dictionary of calculations relative to teacher courses.
|
2017-08-22 11:26:18 +02:00
|
|
|
|
Store plus/minus periods to self.next_report.
|
2017-07-14 18:47:56 +02:00
|
|
|
|
"""
|
|
|
|
|
|
mandats = self.course_set.filter(subject__startswith='#')
|
|
|
|
|
|
ens = self.course_set.exclude(subject__startswith='#')
|
|
|
|
|
|
tot_mandats = mandats.aggregate(models.Sum('period'))['period__sum'] or 0
|
|
|
|
|
|
tot_ens = ens.aggregate(models.Sum('period'))['period__sum'] or 0
|
2017-08-21 14:01:02 +02:00
|
|
|
|
# formation periods calculated at pro-rata of total charge
|
2017-08-30 17:58:53 +02:00
|
|
|
|
tot_formation = int(round(
|
|
|
|
|
|
(tot_mandats + tot_ens) / settings.MAX_ENS_PERIODS * settings.MAX_ENS_FORMATION
|
|
|
|
|
|
))
|
2017-07-14 18:47:56 +02:00
|
|
|
|
tot_trav = self.previous_report + tot_mandats + tot_ens + tot_formation
|
|
|
|
|
|
tot_paye = tot_trav
|
2017-08-30 17:58:53 +02:00
|
|
|
|
max_periods = settings.MAX_ENS_PERIODS + settings.MAX_ENS_FORMATION
|
2017-08-21 14:01:02 +02:00
|
|
|
|
# Special situations triggering reporting (positive or negative) hours for next year:
|
|
|
|
|
|
# - full-time teacher with a total charge under 100%
|
|
|
|
|
|
# - teachers with a total charge over 100%
|
2017-08-22 11:26:18 +02:00
|
|
|
|
self.next_report = 0
|
2017-08-21 14:01:02 +02:00
|
|
|
|
if (self.rate == 100 and tot_paye < max_periods) or (tot_paye > max_periods):
|
|
|
|
|
|
tot_paye = max_periods
|
2017-08-22 11:26:18 +02:00
|
|
|
|
self.next_report = tot_trav - tot_paye
|
|
|
|
|
|
self.save()
|
2017-08-21 14:01:02 +02:00
|
|
|
|
|
2017-07-14 18:47:56 +02:00
|
|
|
|
return {
|
|
|
|
|
|
'mandats': mandats,
|
|
|
|
|
|
'tot_mandats': tot_mandats,
|
|
|
|
|
|
'tot_ens': tot_ens,
|
|
|
|
|
|
'tot_formation': tot_formation,
|
|
|
|
|
|
'tot_trav': tot_trav,
|
|
|
|
|
|
'tot_paye': tot_paye,
|
2017-08-21 14:01:02 +02:00
|
|
|
|
'report': self.next_report,
|
2017-07-14 18:47:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-07-19 18:35:54 +02:00
|
|
|
|
def calc_imputations(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Return a tuple for accountings charges
|
|
|
|
|
|
"""
|
|
|
|
|
|
activities = self.calc_activity()
|
|
|
|
|
|
imputations = OrderedDict(
|
|
|
|
|
|
[('ASA', 0), ('ASSC', 0), ('ASE', 0), ('MP', 0), ('EDEpe', 0), ('EDEps', 0),
|
2017-08-21 10:25:49 +02:00
|
|
|
|
('EDS', 0), ('CAS_FPP', 0), ('Direction', 0)]
|
2017-07-19 18:35:54 +02:00
|
|
|
|
)
|
|
|
|
|
|
courses = self.course_set.all()
|
|
|
|
|
|
|
|
|
|
|
|
for key in imputations:
|
|
|
|
|
|
imputations[key] = courses.filter(imputation__contains=key).aggregate(models.Sum('period'))['period__sum'] or 0
|
|
|
|
|
|
|
|
|
|
|
|
tot = sum(imputations.values())
|
|
|
|
|
|
if tot > 0:
|
|
|
|
|
|
for key in imputations:
|
|
|
|
|
|
imputations[key] += round(imputations[key] / tot * activities['tot_formation'])
|
|
|
|
|
|
|
2017-08-18 09:22:39 +02:00
|
|
|
|
# Split EDE periods in EDEpe and EDEps columns, in proportion
|
2017-07-19 18:35:54 +02:00
|
|
|
|
ede = courses.filter(imputation='EDE').aggregate(models.Sum('period'))['period__sum'] or 0
|
|
|
|
|
|
if ede > 0:
|
|
|
|
|
|
pe = imputations['EDEpe']
|
|
|
|
|
|
ps = imputations['EDEps']
|
2017-08-18 09:22:39 +02:00
|
|
|
|
pe_percent = (pe / (pe + ps)) if (pe + ps) > 0 else 0.5
|
|
|
|
|
|
pe_plus = round(ede * pe_percent)
|
2017-07-19 18:35:54 +02:00
|
|
|
|
imputations['EDEpe'] += pe_plus
|
|
|
|
|
|
imputations['EDEps'] += ede - pe_plus
|
|
|
|
|
|
|
|
|
|
|
|
return (self.calc_activity(), imputations)
|
|
|
|
|
|
|
2018-02-19 10:28:14 +01:00
|
|
|
|
def total_logbook(self):
|
|
|
|
|
|
return LogBook.objects.filter(teacher=self).aggregate(models.Sum('nb_period'))['nb_period__sum']
|
|
|
|
|
|
total_logbook.short_description = 'Solde du carnet du lait'
|
|
|
|
|
|
|
2017-07-11 23:01:46 +02:00
|
|
|
|
|
2018-02-19 10:08:46 +01:00
|
|
|
|
class LogBookReason(models.Model):
|
|
|
|
|
|
name = models.CharField('Motif', max_length=50, unique=True)
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = 'Motif de carnet du lait'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LogBook(models.Model):
|
|
|
|
|
|
teacher = models.ForeignKey(Teacher, on_delete=models.CASCADE, verbose_name='Enseignant')
|
|
|
|
|
|
reason = models.ForeignKey(LogBookReason, on_delete=models.PROTECT, verbose_name='Catégorie de motif')
|
|
|
|
|
|
input_date = models.DateField('Date de saisie', auto_now_add=True)
|
|
|
|
|
|
start_date = models.DateField('Date de début')
|
|
|
|
|
|
end_date = models.DateField('Date de fin')
|
|
|
|
|
|
nb_period = models.IntegerField('Périodes')
|
|
|
|
|
|
comment = models.CharField('Commentaire motif', max_length=200, blank=True)
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return '{} : {} pér. - {}'.format(self.teacher, self.nb_period, self.comment)
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = 'Carnet du lait'
|
|
|
|
|
|
verbose_name_plural = 'Carnets du lait'
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-08-29 16:25:34 +02:00
|
|
|
|
class Option(models.Model):
|
|
|
|
|
|
name = models.CharField("Nom", max_length=100, unique=True)
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-03-19 18:36:28 +01:00
|
|
|
|
class ExamEDESession(models.Model):
|
|
|
|
|
|
year = models.PositiveIntegerField()
|
|
|
|
|
|
season = models.CharField('saison', max_length=10)
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Session d’examen EDE"
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return '{0} {1}'.format(self.year, self.season)
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-10-25 08:54:22 +02:00
|
|
|
|
GENDER_CHOICES = (
|
|
|
|
|
|
('M', 'Masculin'),
|
|
|
|
|
|
('F', 'Féminin'),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2012-11-06 17:54:33 +01:00
|
|
|
|
class Student(models.Model):
|
2012-11-12 20:02:06 +01:00
|
|
|
|
ext_id = models.IntegerField(null=True, unique=True, verbose_name='ID externe')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
first_name = models.CharField(max_length=40, verbose_name='Prénom')
|
|
|
|
|
|
last_name = models.CharField(max_length=40, verbose_name='Nom')
|
2017-10-25 08:54:22 +02:00
|
|
|
|
gender = models.CharField('Genre', max_length=3, blank=True, choices=GENDER_CHOICES)
|
2017-12-06 11:19:22 +01:00
|
|
|
|
birth_date = models.DateField('Date de naissance', null=True, blank=True)
|
2013-01-02 14:40:24 +01:00
|
|
|
|
street = models.CharField(max_length=150, blank=True, verbose_name='Rue')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
pcode = models.CharField(max_length=4, verbose_name='Code postal')
|
|
|
|
|
|
city = models.CharField(max_length=40, verbose_name='Localité')
|
2017-07-11 23:15:00 +02:00
|
|
|
|
district = models.CharField(max_length=20, blank=True, verbose_name='Canton')
|
2012-12-13 14:41:13 +01:00
|
|
|
|
tel = models.CharField(max_length=40, blank=True, verbose_name='Téléphone')
|
2013-01-02 14:40:24 +01:00
|
|
|
|
mobile = models.CharField(max_length=40, blank=True, verbose_name='Portable')
|
2012-12-13 14:41:13 +01:00
|
|
|
|
email = models.EmailField(verbose_name='Courriel', blank=True)
|
2018-02-06 09:29:48 +01:00
|
|
|
|
avs = models.CharField(max_length=20, blank=True, verbose_name='No AVS')
|
2017-08-29 16:25:34 +02:00
|
|
|
|
option_ase = models.ForeignKey(Option, null=True, blank=True, on_delete=models.SET_NULL)
|
2017-07-11 23:15:00 +02:00
|
|
|
|
dispense_ecg = models.BooleanField(default=False)
|
|
|
|
|
|
dispense_eps = models.BooleanField(default=False)
|
|
|
|
|
|
soutien_dys = models.BooleanField(default=False)
|
|
|
|
|
|
corporation = models.ForeignKey('Corporation', null=True, blank=True,
|
|
|
|
|
|
on_delete=models.SET_NULL, verbose_name='Employeur')
|
|
|
|
|
|
instructor = models.ForeignKey('CorpContact', null=True, blank=True,
|
|
|
|
|
|
on_delete=models.SET_NULL, verbose_name='FEE/FPP')
|
2018-02-14 18:13:22 +01:00
|
|
|
|
supervisor = models.ForeignKey('CorpContact', related_name='rel_supervisor', verbose_name='Superviseur',
|
|
|
|
|
|
null=True, blank=True, on_delete=models.SET_NULL)
|
2018-04-26 08:07:14 +02:00
|
|
|
|
supervision_attest_received = models.BooleanField('Attest. supervision reçue',
|
|
|
|
|
|
default=False)
|
2018-02-14 18:13:22 +01:00
|
|
|
|
mentor = models.ForeignKey('CorpContact', related_name='rel_mentor', verbose_name='Mentor',
|
|
|
|
|
|
null=True, blank=True, on_delete=models.SET_NULL)
|
2018-03-19 18:36:28 +01:00
|
|
|
|
expert = models.ForeignKey('CorpContact', related_name='rel_expert', verbose_name='Expert externe',
|
2018-02-15 16:05:25 +01:00
|
|
|
|
null=True, blank=True, on_delete=models.SET_NULL)
|
2016-08-31 14:49:49 +02:00
|
|
|
|
klass = models.ForeignKey(Klass, verbose_name='Classe', blank=True, null=True,
|
2017-07-19 10:44:12 +02:00
|
|
|
|
on_delete=models.PROTECT)
|
2018-02-05 13:59:29 +01:00
|
|
|
|
report_sem1 = models.FileField('Bulletin 1er sem.', null=True, blank=True, upload_to='bulletins')
|
|
|
|
|
|
report_sem1_sent = models.DateTimeField('Date envoi bull. sem 1', null=True, blank=True)
|
|
|
|
|
|
report_sem2 = models.FileField('Bulletin 2e sem.', null=True, blank=True, upload_to='bulletins')
|
|
|
|
|
|
report_sem2_sent = models.DateTimeField('Date envoi bull. sem 2', null=True, blank=True)
|
2012-11-12 20:02:06 +01:00
|
|
|
|
archived = models.BooleanField(default=False, verbose_name='Archivé')
|
2016-01-29 18:06:33 +01:00
|
|
|
|
archived_text = models.TextField(blank=True)
|
2018-03-19 18:36:28 +01:00
|
|
|
|
# ============== Fields for examination ======================
|
2018-04-26 09:31:04 +02:00
|
|
|
|
subject = models.TextField('TD: titre provisoire', blank=True)
|
|
|
|
|
|
title = models.TextField('TD: Titre définitif', blank=True)
|
2018-03-19 18:36:28 +01:00
|
|
|
|
training_referent = models.ForeignKey(Teacher, null=True, blank=True, related_name='rel_training_referent',
|
|
|
|
|
|
on_delete=models.SET_NULL, verbose_name='Référent de stage')
|
|
|
|
|
|
referent = models.ForeignKey(Teacher, null=True, blank=True, related_name='rel_referent',
|
|
|
|
|
|
on_delete=models.SET_NULL, verbose_name='Référent avant-projet')
|
|
|
|
|
|
internal_expert = models.ForeignKey(Teacher, related_name='rel_internal_expert', verbose_name='Expert interne',
|
|
|
|
|
|
null=True, blank=True, on_delete=models.SET_NULL)
|
|
|
|
|
|
session = models.ForeignKey(ExamEDESession, null=True, blank=True, on_delete=models.SET_NULL)
|
|
|
|
|
|
date_exam = models.DateTimeField(blank=True, null=True)
|
|
|
|
|
|
last_appointment = models.DateField(blank=True, null=True)
|
|
|
|
|
|
room = models.CharField('Salle', max_length=15, blank=True)
|
|
|
|
|
|
mark = models.DecimalField('Note', max_digits=3, decimal_places=2, blank=True, null=True)
|
2018-04-23 15:14:36 +02:00
|
|
|
|
date_soutenance_mailed = models.DateTimeField("Convoc. env.", blank=True, null=True)
|
|
|
|
|
|
date_confirm_received = models.DateTimeField("Récept. confirm", blank=True, null=True)
|
2018-03-19 18:36:28 +01:00
|
|
|
|
# ============== Fields for examination ======================
|
2012-11-12 20:02:06 +01:00
|
|
|
|
support_tabimport = True
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Étudiant"
|
|
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2012-11-06 17:54:33 +01:00
|
|
|
|
return '%s %s' % (self.last_name, self.first_name)
|
|
|
|
|
|
|
2017-08-08 16:11:55 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def civility(self):
|
2018-01-11 16:57:47 +01:00
|
|
|
|
return {'M': 'Monsieur', 'F': 'Madame'}.get(self.gender, '')
|
2017-08-08 16:11:55 +02:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def full_name(self):
|
|
|
|
|
|
return '{0} {1}'.format(self.first_name, self.last_name)
|
|
|
|
|
|
|
2018-04-12 12:17:37 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def civility_full_name(self):
|
|
|
|
|
|
return '{0} {1} {2}'.format(self.civility, self.first_name, self.last_name)
|
|
|
|
|
|
|
2017-08-08 16:11:55 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def pcode_city(self):
|
|
|
|
|
|
return '{0} {1}'.format(self.pcode, self.city)
|
|
|
|
|
|
|
2018-04-12 12:37:17 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def role(self):
|
|
|
|
|
|
if self.klass.section.is_fe():
|
|
|
|
|
|
return {'M': 'apprenti', 'F': 'apprentie'}.get(self.gender, '')
|
|
|
|
|
|
else:
|
|
|
|
|
|
return {'M': 'étudiant', 'F': 'étudiante'}.get(self.gender, '')
|
|
|
|
|
|
|
2016-01-29 18:06:33 +01:00
|
|
|
|
def save(self, **kwargs):
|
|
|
|
|
|
if self.archived and not self.archived_text:
|
|
|
|
|
|
# Fill archived_text with training data, JSON-formatted
|
|
|
|
|
|
trainings = [
|
|
|
|
|
|
tr.serialize() for tr in self.training_set.all().select_related('availability')
|
|
|
|
|
|
]
|
|
|
|
|
|
self.archived_text = json.dumps(trainings)
|
|
|
|
|
|
if self.archived_text and not self.archived:
|
|
|
|
|
|
self.archived_text = ""
|
|
|
|
|
|
super().save(**kwargs)
|
|
|
|
|
|
|
2016-01-15 18:17:35 +01:00
|
|
|
|
def age_at(self, date_):
|
|
|
|
|
|
"""Return age of student at `date_` time, as a string."""
|
|
|
|
|
|
age = (date.today() - self.birth_date) / timedelta(days=365.2425)
|
|
|
|
|
|
age_y = int(age)
|
|
|
|
|
|
age_m = int((age - age_y) * 12)
|
|
|
|
|
|
return '%d ans%s' % (age_y, ' %d m.' % age_m if age_m > 0 else '')
|
|
|
|
|
|
|
2018-04-24 09:22:43 +02:00
|
|
|
|
def missing_examination_data(self):
|
|
|
|
|
|
missing = []
|
|
|
|
|
|
if not self.date_exam:
|
|
|
|
|
|
missing.append("La date d’examen est manquante")
|
|
|
|
|
|
if not self.room:
|
|
|
|
|
|
missing.append("La salle d’examen n’est pas définie")
|
|
|
|
|
|
if not self.expert:
|
|
|
|
|
|
missing.append("L’expert externe n’est pas défini")
|
|
|
|
|
|
if not self.internal_expert:
|
|
|
|
|
|
missing.append("L’expert interne n’est pas défini")
|
|
|
|
|
|
return missing
|
|
|
|
|
|
|
2012-11-13 09:40:31 +01:00
|
|
|
|
@classmethod
|
2017-07-19 11:33:54 +02:00
|
|
|
|
def prepare_import(cls, student_values):
|
2012-11-13 09:40:31 +01:00
|
|
|
|
''' Hook for tabimport, before new object get created '''
|
2017-07-12 09:53:25 +02:00
|
|
|
|
if 'klass' in student_values:
|
2012-11-13 09:40:31 +01:00
|
|
|
|
try:
|
2017-07-12 09:53:25 +02:00
|
|
|
|
k = Klass.objects.get(name=student_values['klass'])
|
2012-11-13 09:40:31 +01:00
|
|
|
|
except Klass.DoesNotExist:
|
2017-07-12 09:53:25 +02:00
|
|
|
|
raise Exception("La classe '%s' n'existe pas encore" % student_values['klass'])
|
|
|
|
|
|
student_values['klass'] = k
|
|
|
|
|
|
|
2013-08-23 16:41:51 +02:00
|
|
|
|
# See if postal code included in city, and split them
|
2017-07-19 11:33:54 +02:00
|
|
|
|
if 'city' in student_values and utils.is_int(student_values['city'][:4]):
|
2017-07-12 09:53:25 +02:00
|
|
|
|
student_values['pcode'], _, student_values['city'] = student_values['city'].partition(' ')
|
|
|
|
|
|
student_values['archived'] = False
|
|
|
|
|
|
return student_values
|
2012-11-13 09:40:31 +01:00
|
|
|
|
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Corporation(models.Model):
|
2015-10-01 17:02:19 +02:00
|
|
|
|
ext_id = models.IntegerField(null=True, blank=True, verbose_name='ID externe')
|
2017-07-19 17:41:11 +02:00
|
|
|
|
name = models.CharField(max_length=100, verbose_name='Nom')
|
2015-10-01 17:02:19 +02:00
|
|
|
|
short_name = models.CharField(max_length=40, blank=True, verbose_name='Nom court')
|
2017-07-11 23:15:00 +02:00
|
|
|
|
district = models.CharField(max_length=20, blank=True, verbose_name='Canton')
|
2016-08-31 14:49:49 +02:00
|
|
|
|
parent = models.ForeignKey('self', null=True, blank=True, verbose_name='Institution mère',
|
|
|
|
|
|
on_delete=models.SET_NULL)
|
2015-10-01 17:02:19 +02:00
|
|
|
|
sector = models.CharField(max_length=40, blank=True, verbose_name='Secteur')
|
2012-11-12 13:19:41 +01:00
|
|
|
|
typ = models.CharField(max_length=40, blank=True, verbose_name='Type de structure')
|
2012-11-09 15:32:00 +01:00
|
|
|
|
street = models.CharField(max_length=100, blank=True, verbose_name='Rue')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
pcode = models.CharField(max_length=4, verbose_name='Code postal')
|
|
|
|
|
|
city = models.CharField(max_length=40, verbose_name='Localité')
|
|
|
|
|
|
tel = models.CharField(max_length=20, blank=True, verbose_name='Téléphone')
|
2012-11-12 13:19:41 +01:00
|
|
|
|
email = models.EmailField(blank=True, verbose_name='Courriel')
|
|
|
|
|
|
web = models.URLField(blank=True, verbose_name='Site Web')
|
2012-11-12 20:02:06 +01:00
|
|
|
|
archived = models.BooleanField(default=False, verbose_name='Archivé')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Institution"
|
2012-12-13 14:41:13 +01:00
|
|
|
|
ordering = ('name',)
|
2017-08-08 15:21:10 +02:00
|
|
|
|
unique_together = (('name', 'city'),)
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2015-10-01 17:02:19 +02:00
|
|
|
|
sect = ' (%s)' % self.sector if self.sector else ''
|
|
|
|
|
|
return "%s%s, %s %s" % (self.name, sect, self.pcode, self.city)
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2017-08-08 16:11:55 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def pcode_city(self):
|
|
|
|
|
|
return '{0} {1}'.format(self.pcode, self.city)
|
|
|
|
|
|
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class CorpContact(models.Model):
|
2018-03-16 13:55:15 +01:00
|
|
|
|
corporation = models.ForeignKey(
|
|
|
|
|
|
Corporation, verbose_name='Institution', null=True, blank=True,
|
|
|
|
|
|
on_delete=models.CASCADE
|
|
|
|
|
|
)
|
2015-10-01 14:47:43 +02:00
|
|
|
|
ext_id = models.IntegerField(null=True, blank=True, verbose_name='ID externe')
|
2012-12-13 14:41:13 +01:00
|
|
|
|
is_main = models.BooleanField(default=False, verbose_name='Contact principal')
|
2013-09-30 15:06:36 +02:00
|
|
|
|
always_cc = models.BooleanField(default=False, verbose_name='Toujours en copie')
|
2018-04-24 12:01:44 +02:00
|
|
|
|
civility = models.CharField(max_length=40, blank=True, verbose_name='Civilité')
|
2012-11-09 15:32:00 +01:00
|
|
|
|
first_name = models.CharField(max_length=40, blank=True, verbose_name='Prénom')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
last_name = models.CharField(max_length=40, verbose_name='Nom')
|
2018-04-12 10:12:24 +02:00
|
|
|
|
birth_date = models.DateField(blank=True, null=True, verbose_name='Date de naissance')
|
2017-07-11 23:15:00 +02:00
|
|
|
|
role = models.CharField(max_length=40, blank=True, verbose_name='Fonction')
|
2018-04-12 10:12:24 +02:00
|
|
|
|
street = models.CharField(max_length=100, blank=True, verbose_name='Rue')
|
|
|
|
|
|
pcode = models.CharField(max_length=4, blank=True, verbose_name='Code postal')
|
|
|
|
|
|
city = models.CharField(max_length=40, blank=True, verbose_name='Localité')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
tel = models.CharField(max_length=20, blank=True, verbose_name='Téléphone')
|
2016-01-15 20:30:29 +01:00
|
|
|
|
email = models.CharField(max_length=100, blank=True, verbose_name='Courriel')
|
2016-01-18 12:55:57 +01:00
|
|
|
|
archived = models.BooleanField(default=False, verbose_name='Archivé')
|
2016-01-15 21:01:07 +01:00
|
|
|
|
sections = models.ManyToManyField(Section, blank=True)
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2018-04-12 10:12:24 +02:00
|
|
|
|
ccp = models.CharField('Compte de chèque postal', max_length=15, blank=True)
|
|
|
|
|
|
bank = models.CharField('Banque (nom et ville)', max_length=200, blank=True)
|
|
|
|
|
|
clearing = models.CharField('No clearing', max_length=5, blank=True)
|
|
|
|
|
|
iban = models.CharField('iban', max_length=21, blank=True)
|
|
|
|
|
|
qualification = models.TextField('Titres obtenus', blank=True)
|
|
|
|
|
|
fields_of_interest = models.TextField("Domaines d’intérêts", blank=True)
|
|
|
|
|
|
|
2012-11-06 17:54:33 +01:00
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Contact"
|
|
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2018-03-16 13:55:15 +01:00
|
|
|
|
return '{0} {1}, {2}'.format(self.last_name, self.first_name, self.corporation or '-')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2018-04-12 12:17:37 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def full_name(self):
|
|
|
|
|
|
return '{0} {1}'.format(self.first_name, self.last_name)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def civility_full_name(self):
|
2018-04-24 12:01:44 +02:00
|
|
|
|
return '{0} {1} {2}'.format(self.civility, self.first_name, self.last_name)
|
2018-04-12 12:17:37 +02:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def pcode_city(self):
|
|
|
|
|
|
return '{0} {1}'.format(self.pcode, self.city)
|
|
|
|
|
|
|
2018-04-12 12:37:17 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def adjective_ending(self):
|
2018-04-24 12:01:44 +02:00
|
|
|
|
return 'e' if self.civility == 'Madame' else ''
|
2018-04-12 12:37:17 +02:00
|
|
|
|
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Domain(models.Model):
|
|
|
|
|
|
name = models.CharField(max_length=50, verbose_name='Nom')
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Domaine"
|
2012-11-13 16:55:46 +01:00
|
|
|
|
ordering = ('name',)
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2012-11-06 17:54:33 +01:00
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Period(models.Model):
|
|
|
|
|
|
""" Périodes de stages """
|
2012-11-12 13:19:41 +01:00
|
|
|
|
title = models.CharField(max_length=150, verbose_name='Titre')
|
2017-08-21 16:41:19 +02:00
|
|
|
|
section = models.ForeignKey(Section, verbose_name='Filière', on_delete=models.PROTECT,
|
|
|
|
|
|
limit_choices_to={'name__startswith': 'MP'})
|
2016-08-31 15:01:00 +02:00
|
|
|
|
level = models.ForeignKey(Level, verbose_name='Niveau', on_delete=models.PROTECT)
|
2012-11-06 17:54:33 +01:00
|
|
|
|
start_date = models.DateField(verbose_name='Date de début')
|
|
|
|
|
|
end_date = models.DateField(verbose_name='Date de fin')
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Période de stage"
|
2013-04-08 14:52:55 +02:00
|
|
|
|
ordering = ('-start_date',)
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2013-04-08 14:52:55 +02:00
|
|
|
|
return '%s (%s)' % (self.dates, self.title)
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def dates(self):
|
|
|
|
|
|
return '%s - %s' % (self.start_date, self.end_date)
|
|
|
|
|
|
|
2013-04-08 14:19:23 +02:00
|
|
|
|
@property
|
|
|
|
|
|
def school_year(self):
|
2013-04-11 12:45:00 +02:00
|
|
|
|
return utils.school_year(self.start_date)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def relative_level(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Return the level depending on current school year. For example, if the
|
|
|
|
|
|
period is planned for next school year, level will be level - 1.
|
|
|
|
|
|
"""
|
|
|
|
|
|
diff = (utils.school_year(self.start_date, as_tuple=True)[0] -
|
|
|
|
|
|
utils.school_year(date.today(), as_tuple=True)[0])
|
|
|
|
|
|
return self.level.delta(-diff)
|
2013-04-08 14:19:23 +02:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def weeks(self):
|
|
|
|
|
|
""" Return the number of weeks of this period """
|
|
|
|
|
|
return (self.end_date - self.start_date).days // 7
|
|
|
|
|
|
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Availability(models.Model):
|
|
|
|
|
|
""" Disponibilités des institutions """
|
2016-08-31 15:01:00 +02:00
|
|
|
|
corporation = models.ForeignKey(Corporation, verbose_name='Institution', on_delete=models.CASCADE)
|
|
|
|
|
|
period = models.ForeignKey(Period, verbose_name='Période', on_delete=models.CASCADE)
|
|
|
|
|
|
domain = models.ForeignKey(Domain, verbose_name='Domaine', on_delete=models.CASCADE)
|
|
|
|
|
|
contact = models.ForeignKey(CorpContact, null=True, blank=True, verbose_name='Contact institution',
|
|
|
|
|
|
on_delete=models.SET_NULL)
|
2016-01-19 12:07:14 +01:00
|
|
|
|
priority = models.BooleanField('Prioritaire', default=False)
|
2012-11-07 17:45:53 +01:00
|
|
|
|
comment = models.TextField(blank=True, verbose_name='Remarques')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Disponibilité"
|
|
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2015-10-01 17:36:57 +02:00
|
|
|
|
return '%s - %s (%s) - %s' % (self.period, self.corporation, self.domain, self.contact)
|
2012-11-07 17:45:53 +01:00
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def free(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.training
|
|
|
|
|
|
except Training.DoesNotExist:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Training(models.Model):
|
|
|
|
|
|
""" Stages """
|
2016-08-31 15:01:00 +02:00
|
|
|
|
student = models.ForeignKey(Student, verbose_name='Étudiant', on_delete=models.CASCADE)
|
|
|
|
|
|
availability = models.OneToOneField(Availability, verbose_name='Disponibilité', on_delete=models.CASCADE)
|
2017-07-18 12:48:10 +02:00
|
|
|
|
referent = models.ForeignKey(Teacher, null=True, blank=True, verbose_name='Référent',
|
2016-08-31 15:01:00 +02:00
|
|
|
|
on_delete=models.SET_NULL)
|
2012-11-07 17:45:53 +01:00
|
|
|
|
comment = models.TextField(blank=True, verbose_name='Remarques')
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = "Stage"
|
2016-01-19 18:01:18 +01:00
|
|
|
|
ordering = ("-availability__period",)
|
2012-11-06 17:54:33 +01:00
|
|
|
|
|
2015-09-22 20:52:35 +02:00
|
|
|
|
def __str__(self):
|
2012-11-07 17:45:53 +01:00
|
|
|
|
return '%s chez %s (%s)' % (self.student, self.availability.corporation, self.availability.period)
|
2016-01-29 18:06:33 +01:00
|
|
|
|
|
|
|
|
|
|
def serialize(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Compute a summary of the training as a dict representation (for archiving purpose).
|
|
|
|
|
|
"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
'period': str(self.availability.period),
|
|
|
|
|
|
'corporation': str(self.availability.corporation),
|
|
|
|
|
|
'referent': str(self.referent),
|
|
|
|
|
|
'comment': self.comment,
|
|
|
|
|
|
'contact': str(self.availability.contact),
|
|
|
|
|
|
'comment_avail': self.availability.comment,
|
|
|
|
|
|
'domain': str(self.availability.domain),
|
|
|
|
|
|
}
|
2017-07-14 10:19:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
IMPUTATION_CHOICES = (
|
|
|
|
|
|
('ASAFE', 'ASAFE'),
|
|
|
|
|
|
('ASEFE', 'ASEFE'),
|
2017-08-08 15:21:10 +02:00
|
|
|
|
('ASSCFE', 'ASSCFE'),
|
2017-08-21 10:25:49 +02:00
|
|
|
|
('MP', 'MP'),
|
2017-07-14 10:19:12 +02:00
|
|
|
|
('EDEpe', 'EDEpe'),
|
|
|
|
|
|
('EDEps', 'EDEps'),
|
|
|
|
|
|
('EDE', 'EDE'),
|
|
|
|
|
|
('EDS', 'EDS'),
|
2017-08-21 10:25:49 +02:00
|
|
|
|
('CAS_FPP', 'CAS_FPP'),
|
2017-07-14 10:19:12 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
2017-08-08 15:21:10 +02:00
|
|
|
|
|
2017-07-14 10:19:12 +02:00
|
|
|
|
class Course(models.Model):
|
|
|
|
|
|
"""Cours et mandats attribués aux enseignants"""
|
|
|
|
|
|
teacher = models.ForeignKey(Teacher, blank=True, null=True,
|
|
|
|
|
|
verbose_name="Enseignant-e", on_delete=models.SET_NULL)
|
2017-08-18 09:28:17 +02:00
|
|
|
|
public = models.CharField("Classe(s)", max_length=200, default='')
|
2017-07-14 10:19:12 +02:00
|
|
|
|
subject = models.CharField("Sujet", max_length=100, default='')
|
|
|
|
|
|
period = models.IntegerField("Nb de périodes", default=0)
|
|
|
|
|
|
# Imputation comptable: compte dans lequel les frais du cours seront imputés
|
|
|
|
|
|
imputation = models.CharField("Imputation", max_length=10, choices=IMPUTATION_CHOICES)
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = 'Cours'
|
|
|
|
|
|
verbose_name_plural = 'Cours'
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2017-07-19 18:35:54 +02:00
|
|
|
|
return '{0} - {1} - {2} - {3}'.format(
|
|
|
|
|
|
self.teacher, self.public, self.subject, self.period
|
2017-07-14 10:19:12 +02:00
|
|
|
|
)
|
2018-04-26 07:54:29 +02:00
|
|
|
|
|
|
|
|
|
|
class SupervisionBill(models.Model):
|
|
|
|
|
|
student = models.ForeignKey(Student, verbose_name='étudiant', on_delete=models.CASCADE)
|
|
|
|
|
|
supervisor = models.ForeignKey(CorpContact, verbose_name='superviseur', on_delete=models.CASCADE)
|
|
|
|
|
|
period = models.SmallIntegerField('période', default=0)
|
|
|
|
|
|
date = models.DateField()
|
|
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
|
verbose_name = 'Facture de supervision'
|
|
|
|
|
|
verbose_name_plural = 'Factures de supervision'
|
|
|
|
|
|
ordering = ['date']
|
|
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return '{0} : {1}'.format(self.student.full_name, self.supervisor.full_name)
|