epcstages/stages/models.py

619 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
from collections import OrderedDict
from datetime import date, timedelta
from django.conf import settings
from django.db import models
from django.db.models import Case, Count, When
from . import utils
CIVILITY_CHOICES = (
('Madame', 'Madame'),
('Monsieur', 'Monsieur'),
)
class Section(models.Model):
""" Filières """
name = models.CharField(max_length=20, verbose_name='Nom')
class Meta:
verbose_name = "Filière"
def __str__(self):
return self.name
def is_fe(self):
"""fe=formation en entreprise"""
return self.name in {'ASA', 'ASE', 'ASSC'}
class Level(models.Model):
name = models.CharField(max_length=10, verbose_name='Nom')
class Meta:
verbose_name = "Niveau"
verbose_name_plural = "Niveaux"
def __str__(self):
return self.name
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
class ActiveKlassManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
num_students=Count(Case(When(student__archived=False, then=1)))
).filter(num_students__gt=0)
class Klass(models.Model):
name = models.CharField(max_length=10, verbose_name='Nom', unique=True)
section = models.ForeignKey(Section, verbose_name='Filière', on_delete=models.PROTECT)
level = models.ForeignKey(Level, verbose_name='Niveau', on_delete=models.PROTECT)
teacher = models.ForeignKey('Teacher', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name='Maître de classe')
objects = models.Manager()
active = ActiveKlassManager()
class Meta:
verbose_name = "Classe"
def __str__(self):
return self.name
def is_Ede_pe(self):
return 'EDEpe' in self.name
def is_Ede_ps(self):
return 'EDEps' in self.name
class Teacher(models.Model):
civility = models.CharField(max_length=10, choices=CIVILITY_CHOICES, verbose_name='Civilité')
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')
birth_date = models.DateField(verbose_name='Date de naissance', blank=True, null=True)
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')
archived = models.BooleanField(default=False)
class Meta:
verbose_name='Enseignant'
ordering = ('last_name', 'first_name')
def __str__(self):
return '{0} {1}'.format(self.last_name, self.first_name)
@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)
@property
def role(self):
return {'Monsieur': 'enseignant-formateur', 'Madame': 'enseignante-formatrice'}.get(self.civility, '')
def calc_activity(self):
"""
Return a dictionary of calculations relative to teacher courses.
Store plus/minus periods to self.next_report.
"""
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
# formation periods calculated at pro-rata of total charge
tot_formation = int(round(
(tot_mandats + tot_ens) / settings.MAX_ENS_PERIODS * settings.MAX_ENS_FORMATION
))
tot_trav = self.previous_report + tot_mandats + tot_ens + tot_formation
tot_paye = tot_trav
max_periods = settings.MAX_ENS_PERIODS + settings.MAX_ENS_FORMATION
# 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%
self.next_report = 0
if (self.rate == 100 and tot_paye < max_periods) or (tot_paye > max_periods):
tot_paye = max_periods
self.next_report = tot_trav - tot_paye
self.save()
return {
'mandats': mandats,
'tot_mandats': tot_mandats,
'tot_ens': tot_ens,
'tot_formation': tot_formation,
'tot_trav': tot_trav,
'tot_paye': tot_paye,
'report': self.next_report,
}
def calc_imputations(self, ratios):
"""
Return a tuple for accountings charges
"""
activities = self.calc_activity()
imputations = OrderedDict(
[('ASAFE', 0), ('ASSCFE', 0), ('ASEFE', 0), ('MPTS', 0), ('MPS', 0), ('EDEpe', 0), ('EDEps', 0),
('EDS', 0), ('CAS_FPP', 0)]
)
courses = self.course_set.all()
for key in imputations:
imputations[key] = courses.filter(imputation__contains=key).aggregate(models.Sum('period'))['period__sum'] or 0
# Spliting imputations for EDE, ASE and ASSC
ede = courses.filter(imputation='EDE').aggregate(models.Sum('period'))['period__sum'] or 0
if ede > 0:
pe = int(round(ede * ratios['edepe'], 0))
imputations['EDEpe'] += pe
imputations['EDEps'] += ede - pe
ase = courses.filter(imputation='ASE').aggregate(models.Sum('period'))['period__sum'] or 0
if ase > 0:
asefe = int(round(ase * ratios['asefe'], 0))
imputations['ASEFE'] += asefe
imputations['MPTS'] += ase - asefe
assc = courses.filter(imputation='ASSC').aggregate(models.Sum('period'))['period__sum'] or 0
if assc > 0:
asscfe = int(round(assc * ratios['asscfe'], 0))
imputations['ASSCFE'] += asscfe
imputations['MPS'] += assc - asscfe
# Split formation periods in proportions
tot = sum(imputations.values())
if tot > 0:
for key in imputations:
imputations[key] += round(imputations[key] / tot * activities['tot_formation'],0)
return (activities, imputations)
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'
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'
class Option(models.Model):
name = models.CharField("Nom", max_length=100, unique=True)
def __str__(self):
return self.name
class ExamEDESession(models.Model):
year = models.PositiveIntegerField()
season = models.CharField('saison', max_length=10)
class Meta:
verbose_name = "Session dexamen EDE"
def __str__(self):
return '{0} {1}'.format(self.year, self.season)
GENDER_CHOICES = (
('M', 'Masculin'),
('F', 'Féminin'),
)
class Student(models.Model):
ext_id = models.IntegerField(null=True, unique=True, verbose_name='ID externe')
first_name = models.CharField(max_length=40, verbose_name='Prénom')
last_name = models.CharField(max_length=40, verbose_name='Nom')
gender = models.CharField('Genre', max_length=3, blank=True, choices=GENDER_CHOICES)
birth_date = models.DateField('Date de naissance', null=True, blank=True)
street = models.CharField(max_length=150, blank=True, verbose_name='Rue')
pcode = models.CharField(max_length=4, verbose_name='Code postal')
city = models.CharField(max_length=40, verbose_name='Localité')
district = models.CharField(max_length=20, blank=True, verbose_name='Canton')
tel = models.CharField(max_length=40, blank=True, verbose_name='Téléphone')
mobile = models.CharField(max_length=40, blank=True, verbose_name='Portable')
email = models.EmailField(verbose_name='Courriel', blank=True)
login_rpn = models.CharField(max_length=40, blank=True)
avs = models.CharField(max_length=20, blank=True, verbose_name='No AVS')
option_ase = models.ForeignKey(Option, null=True, blank=True, on_delete=models.SET_NULL)
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')
supervisor = models.ForeignKey('CorpContact', related_name='rel_supervisor', verbose_name='Superviseur',
null=True, blank=True, on_delete=models.SET_NULL)
supervision_attest_received = models.BooleanField('Attest. supervision reçue',
default=False)
mentor = models.ForeignKey('CorpContact', related_name='rel_mentor', verbose_name='Mentor',
null=True, blank=True, on_delete=models.SET_NULL)
expert = models.ForeignKey('CorpContact', related_name='rel_expert', verbose_name='Expert externe',
null=True, blank=True, on_delete=models.SET_NULL)
klass = models.ForeignKey(Klass, verbose_name='Classe', blank=True, null=True,
on_delete=models.PROTECT)
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)
archived = models.BooleanField(default=False, verbose_name='Archivé')
archived_text = models.TextField(blank=True)
# ============== Fields for examination ======================
subject = models.TextField('TD: titre provisoire', blank=True)
title = models.TextField('TD: Titre définitif', blank=True)
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)
date_soutenance_mailed = models.DateTimeField("Convoc. env.", blank=True, null=True)
date_confirm_received = models.DateTimeField("Récept. confirm", blank=True, null=True)
# ============== Fields for examination ======================
support_tabimport = True
class Meta:
verbose_name = "Étudiant"
def __str__(self):
return '%s %s' % (self.last_name, self.first_name)
@property
def civility(self):
return {'M': 'Monsieur', 'F': 'Madame'}.get(self.gender, '')
@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)
@property
def pcode_city(self):
return '{0} {1}'.format(self.pcode, self.city)
@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, '')
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)
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 '')
def missing_examination_data(self):
missing = []
if not self.date_exam:
missing.append("La date dexamen est manquante")
if not self.room:
missing.append("La salle dexamen nest pas définie")
if not self.expert:
missing.append("Lexpert externe nest pas défini")
if not self.internal_expert:
missing.append("Lexpert interne nest pas défini")
return missing
@classmethod
def prepare_import(cls, student_values):
''' Hook for tabimport, before new object get created '''
if 'klass' in student_values:
try:
k = Klass.objects.get(name=student_values['klass'])
except Klass.DoesNotExist:
raise Exception("La classe '%s' n'existe pas encore" % student_values['klass'])
student_values['klass'] = k
# See if postal code included in city, and split them
if 'city' in student_values and utils.is_int(student_values['city'][:4]):
student_values['pcode'], _, student_values['city'] = student_values['city'].partition(' ')
student_values['archived'] = False
return student_values
class Corporation(models.Model):
ext_id = models.IntegerField(null=True, blank=True, verbose_name='ID externe')
name = models.CharField(max_length=100, verbose_name='Nom')
short_name = models.CharField(max_length=40, blank=True, verbose_name='Nom court')
district = models.CharField(max_length=20, blank=True, verbose_name='Canton')
parent = models.ForeignKey('self', null=True, blank=True, verbose_name='Institution mère',
on_delete=models.SET_NULL)
sector = models.CharField(max_length=40, blank=True, verbose_name='Secteur')
typ = models.CharField(max_length=40, blank=True, verbose_name='Type de structure')
street = models.CharField(max_length=100, blank=True, verbose_name='Rue')
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')
email = models.EmailField(blank=True, verbose_name='Courriel')
web = models.URLField(blank=True, verbose_name='Site Web')
archived = models.BooleanField(default=False, verbose_name='Archivé')
class Meta:
verbose_name = "Institution"
ordering = ('name',)
unique_together = (('name', 'city'),)
def __str__(self):
sect = ' (%s)' % self.sector if self.sector else ''
return "%s%s, %s %s" % (self.name, sect, self.pcode, self.city)
@property
def pcode_city(self):
return '{0} {1}'.format(self.pcode, self.city)
class CorpContact(models.Model):
corporation = models.ForeignKey(
Corporation, verbose_name='Institution', null=True, blank=True,
on_delete=models.CASCADE
)
ext_id = models.IntegerField(null=True, blank=True, verbose_name='ID externe')
is_main = models.BooleanField(default=False, verbose_name='Contact principal')
always_cc = models.BooleanField(default=False, verbose_name='Toujours en copie')
civility = models.CharField(max_length=40, blank=True, verbose_name='Civilité')
first_name = models.CharField(max_length=40, blank=True, verbose_name='Prénom')
last_name = models.CharField(max_length=40, verbose_name='Nom')
birth_date = models.DateField(blank=True, null=True, verbose_name='Date de naissance')
role = models.CharField(max_length=40, blank=True, verbose_name='Fonction')
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é')
tel = models.CharField(max_length=20, blank=True, verbose_name='Téléphone')
email = models.CharField(max_length=100, blank=True, verbose_name='Courriel')
archived = models.BooleanField(default=False, verbose_name='Archivé')
sections = models.ManyToManyField(Section, blank=True)
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 dintérêts", blank=True)
class Meta:
verbose_name = "Contact"
def __str__(self):
return '{0} {1}, {2}'.format(self.last_name, self.first_name, self.corporation or '-')
@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)
@property
def pcode_city(self):
return '{0} {1}'.format(self.pcode, self.city)
@property
def adjective_ending(self):
return 'e' if self.civility == 'Madame' else ''
class Domain(models.Model):
name = models.CharField(max_length=50, verbose_name='Nom')
class Meta:
verbose_name = "Domaine"
ordering = ('name',)
def __str__(self):
return self.name
class Period(models.Model):
""" Périodes de stages """
title = models.CharField(max_length=150, verbose_name='Titre')
section = models.ForeignKey(Section, verbose_name='Filière', on_delete=models.PROTECT,
limit_choices_to={'name__startswith': 'MP'})
level = models.ForeignKey(Level, verbose_name='Niveau', on_delete=models.PROTECT)
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"
ordering = ('-start_date',)
def __str__(self):
return '%s (%s)' % (self.dates, self.title)
@property
def dates(self):
return '%s - %s' % (self.start_date, self.end_date)
@property
def school_year(self):
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)
@property
def weeks(self):
""" Return the number of weeks of this period """
return (self.end_date - self.start_date).days // 7
class Availability(models.Model):
""" Disponibilités des institutions """
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)
priority = models.BooleanField('Prioritaire', default=False)
comment = models.TextField(blank=True, verbose_name='Remarques')
class Meta:
verbose_name = "Disponibilité"
def __str__(self):
return '%s - %s (%s) - %s' % (self.period, self.corporation, self.domain, self.contact)
@property
def free(self):
try:
self.training
except Training.DoesNotExist:
return True
return False
class Training(models.Model):
""" Stages """
student = models.ForeignKey(Student, verbose_name='Étudiant', on_delete=models.CASCADE)
availability = models.OneToOneField(Availability, verbose_name='Disponibilité', on_delete=models.CASCADE)
referent = models.ForeignKey(Teacher, null=True, blank=True, verbose_name='Référent',
on_delete=models.SET_NULL)
comment = models.TextField(blank=True, verbose_name='Remarques')
class Meta:
verbose_name = "Stage"
ordering = ("-availability__period",)
def __str__(self):
return '%s chez %s (%s)' % (self.student, self.availability.corporation, self.availability.period)
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),
}
IMPUTATION_CHOICES = (
('ASAFE', 'ASAFE'),
('ASEFE', 'ASEFE'),
('ASSCFE', 'ASSCFE'),
('MPTS', 'MPTS'),
('MPS', 'MPS'),
('EDEpe', 'EDEpe'),
('EDEps', 'EDEps'),
('EDS', 'EDS'),
('CAS_FPP', 'CAS_FPP'),
# To split afterwards
('EDE', 'EDE'),
('#Mandat_ASA', 'ASA'),
('#Mandat_ASE', 'ASE'),
('#Mandat_ASSC', 'ASSC'),
)
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)
public = models.CharField("Classe(s)", max_length=200, default='')
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):
return '{0} - {1} - {2} - {3}'.format(
self.teacher, self.public, self.subject, self.period
)
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)