1447 lines
55 KiB
Python
1447 lines
55 KiB
Python
|
from collections import OrderedDict, namedtuple
|
|||
|
from datetime import date, timedelta
|
|||
|
from operator import attrgetter
|
|||
|
from pathlib import Path
|
|||
|
|
|||
|
from django.contrib.auth.models import AbstractUser, Group, UserManager
|
|||
|
from django.contrib.postgres.aggregates import StringAgg
|
|||
|
from django.core.validators import MaxValueValidator
|
|||
|
from django.db import models
|
|||
|
from django.db.models import Count, F, OuterRef, Q, Subquery, Sum
|
|||
|
from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear
|
|||
|
from django.db.utils import IntegrityError
|
|||
|
from django.urls import reverse
|
|||
|
from django.utils import timezone
|
|||
|
from django.utils.functional import cached_property
|
|||
|
|
|||
|
from django_countries.fields import CountryField
|
|||
|
|
|||
|
from common import choices
|
|||
|
from common.fields import ChoiceArrayField
|
|||
|
|
|||
|
from .utils import format_adresse, format_contact, format_d_m_Y, random_string_generator
|
|||
|
|
|||
|
|
|||
|
class CercleScolaire(models.Model):
|
|||
|
""" Les 7 cercles scolaires du canton"""
|
|||
|
|
|||
|
nom = models.CharField(max_length=50, unique=True)
|
|||
|
telephone = models.CharField('tél.', max_length=35, blank=True)
|
|||
|
|
|||
|
class Meta:
|
|||
|
verbose_name = 'Cercle scolaire'
|
|||
|
verbose_name_plural = 'Cercles scolaires'
|
|||
|
ordering = ('nom',)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return self.nom
|
|||
|
|
|||
|
|
|||
|
class Service(models.Model):
|
|||
|
""" Services sociaux en lien avec la Fondation Transit """
|
|||
|
|
|||
|
sigle = models.CharField('Sigle', max_length=20, unique=True)
|
|||
|
nom_complet = models.CharField('Nom complet', max_length=80, blank=True)
|
|||
|
|
|||
|
class Meta:
|
|||
|
ordering = ('sigle',)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return self.sigle
|
|||
|
|
|||
|
def save(self, *args, **kwargs):
|
|||
|
self.sigle = self.sigle.upper()
|
|||
|
super().save(*args, **kwargs)
|
|||
|
|
|||
|
|
|||
|
class RoleManager(models.Manager):
|
|||
|
def get_by_natural_key(self, nom):
|
|||
|
return self.get(nom=nom)
|
|||
|
|
|||
|
|
|||
|
class Role(models.Model):
|
|||
|
NON_EDITABLE = ['Père', 'Mère', 'Enfant suivi', 'Enfant non-suivi']
|
|||
|
ROLES_PARENTS = ['Père', 'Mère']
|
|||
|
|
|||
|
nom = models.CharField("Nom", max_length=50, unique=True)
|
|||
|
est_famille = models.BooleanField("Famille", default=False)
|
|||
|
est_intervenant = models.BooleanField("Intervenant", default=False)
|
|||
|
est_editeur = models.BooleanField("Éditeur", default=False, help_text=(
|
|||
|
"Un rôle éditeur donne le droit de modification des dossiers familles si "
|
|||
|
"la personne est intervenante."
|
|||
|
))
|
|||
|
|
|||
|
objects = RoleManager()
|
|||
|
|
|||
|
class Meta:
|
|||
|
ordering = ('nom',)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return self.nom
|
|||
|
|
|||
|
@property
|
|||
|
def editable(self):
|
|||
|
return self.nom not in self.NON_EDITABLE
|
|||
|
|
|||
|
def natural_key(self):
|
|||
|
return (self.nom,)
|
|||
|
|
|||
|
|
|||
|
class Contact(models.Model):
|
|||
|
prenom = models.CharField('Prénom', max_length=30)
|
|||
|
nom = models.CharField('Nom', max_length=30)
|
|||
|
rue = models.CharField('Rue', max_length=30, blank=True)
|
|||
|
npa = models.CharField('NPA', max_length=4, blank=True)
|
|||
|
localite = models.CharField('Localité', max_length=30, blank=True)
|
|||
|
tel_prive = models.CharField('Tél. privé', max_length=30, blank=True)
|
|||
|
tel_prof = models.CharField('Tél. prof.', max_length=30, blank=True)
|
|||
|
email = models.EmailField(max_length=100, blank=True)
|
|||
|
service = models.ForeignKey(Service, null=True, blank=True, on_delete=models.PROTECT)
|
|||
|
roles = models.ManyToManyField(Role, related_name='contacts', blank=True, verbose_name='Rôles')
|
|||
|
profession = models.CharField('Activité/prof.', max_length=100, blank=True)
|
|||
|
remarque = models.TextField("Remarque", blank=True)
|
|||
|
est_actif = models.BooleanField('actif', default=True)
|
|||
|
|
|||
|
class Meta:
|
|||
|
verbose_name = 'Contact'
|
|||
|
ordering = ('nom', 'prenom')
|
|||
|
unique_together = ('nom', 'prenom', 'service')
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
sigle = self.service.sigle if self.service else ''
|
|||
|
return '{} ({})'.format(self.nom_prenom, sigle) if self.service else self.nom_prenom
|
|||
|
|
|||
|
@property
|
|||
|
def nom_prenom(self):
|
|||
|
return '{} {}'.format(self.nom, self.prenom)
|
|||
|
|
|||
|
@property
|
|||
|
def adresse(self):
|
|||
|
return format_adresse(self.rue, self.npa, self.localite)
|
|||
|
|
|||
|
@cached_property
|
|||
|
def all_roles(self):
|
|||
|
return self.roles.all()
|
|||
|
|
|||
|
def has_role(self, roles=()):
|
|||
|
"""Return True if user has at least one of the `roles` name."""
|
|||
|
return bool(set(r.nom for r in self.all_roles).intersection(set(roles)))
|
|||
|
|
|||
|
def roles_str(self, sep=', '):
|
|||
|
return sep.join([role.nom for role in self.all_roles])
|
|||
|
|
|||
|
@property
|
|||
|
def contact(self):
|
|||
|
return format_contact(self.tel_prof, self.email)
|
|||
|
|
|||
|
@classmethod
|
|||
|
def membres_ope(cls):
|
|||
|
return cls.objects.filter(service__sigle__startswith='OPE', est_actif=True).select_related('service')
|
|||
|
|
|||
|
|
|||
|
class EquipeChoices(models.TextChoices):
|
|||
|
MONTAGNES = 'montagnes', 'Montagnes et V-d-T'
|
|||
|
LITTORAL = 'littoral', 'Littoral et V-d-R'
|
|||
|
|
|||
|
|
|||
|
class Utilisateur(Contact, AbstractUser):
|
|||
|
sigle = models.CharField(max_length=5, blank=True)
|
|||
|
equipe = models.CharField("Équipe", max_length=10, choices=EquipeChoices.choices, blank=True)
|
|||
|
date_desactivation = models.DateField('Date désactivation', null=True, blank=True)
|
|||
|
taux_activite = models.PositiveSmallIntegerField(
|
|||
|
"Taux d’activité (en %)", blank=True, default=0, validators=[MaxValueValidator(100)]
|
|||
|
)
|
|||
|
decharge = models.PositiveSmallIntegerField("Heures de décharge", blank=True, null=True)
|
|||
|
|
|||
|
objects = UserManager()
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return self.nom_prenom
|
|||
|
|
|||
|
@property
|
|||
|
def groupes(self):
|
|||
|
return [item.name for item in self.groups.all()]
|
|||
|
|
|||
|
@property
|
|||
|
def is_psy_or_educ(self):
|
|||
|
return self.has_role({'Psy', 'Educ'})
|
|||
|
|
|||
|
@property
|
|||
|
def is_responsable(self):
|
|||
|
return self.has_role({'Responsable/coordinateur'})
|
|||
|
|
|||
|
@property
|
|||
|
def is_admin_ipe(self):
|
|||
|
return 'admin ipe' in self.groupes
|
|||
|
|
|||
|
@property
|
|||
|
def initiales(self):
|
|||
|
return self.sigle if self.sigle else f'{self.prenom[0]}{self.nom[0]}'
|
|||
|
|
|||
|
@property
|
|||
|
def charge_max(self):
|
|||
|
"""
|
|||
|
Renvoie la charge maximale d'heures hebodmadaires de charge dossier en
|
|||
|
fonction du taux d'activité.
|
|||
|
"""
|
|||
|
charge_map = {
|
|||
|
100: 32, 90: 28, 80: 24, 75: 22, 70: 20, 60: 16, 0: 0,
|
|||
|
}
|
|||
|
charge = charge_map.get(self.taux_activite, int(32 * self.taux_activite / 100))
|
|||
|
if self.decharge:
|
|||
|
charge -= self.decharge
|
|||
|
return charge
|
|||
|
|
|||
|
def prestations_gen(self):
|
|||
|
"""Renvoie les prestations générales de l’utilisateur."""
|
|||
|
return self.prestations.filter(famille__isnull=True)
|
|||
|
|
|||
|
def temps_total_prestations(self, unite):
|
|||
|
return self.prestations.model.temps_total(self.prestations, tous_utilisateurs=False)
|
|||
|
|
|||
|
def total_mensuel(self, unite, month, year):
|
|||
|
prestations = self.prestations.filter(
|
|||
|
date_prestation__year=year, date_prestation__month=month
|
|||
|
)
|
|||
|
return self.prestations.model.temps_total(prestations, tous_utilisateurs=False)
|
|||
|
|
|||
|
def totaux_mensuels(self, unite, year):
|
|||
|
return [self.total_mensuel(unite, m, year) for m in range(1, 13)]
|
|||
|
|
|||
|
def total_annuel(self, unite, year):
|
|||
|
prestations = self.prestations.filter(date_prestation__year=year)
|
|||
|
return self.prestations.model.temps_total(prestations, tous_utilisateurs=False)
|
|||
|
|
|||
|
@classmethod
|
|||
|
def _intervenants(cls, groupes=(), annee=None):
|
|||
|
if annee is None:
|
|||
|
# Utilisateurs actuellement actifs
|
|||
|
utils = Utilisateur.objects.filter(date_desactivation__isnull=True)
|
|||
|
else:
|
|||
|
utils = Utilisateur.objects.filter(
|
|||
|
Q(date_desactivation__isnull=True) | Q(date_desactivation__year__gte=annee)
|
|||
|
)
|
|||
|
return utils.filter(
|
|||
|
groups__name__in=groupes,
|
|||
|
is_superuser=False
|
|||
|
).exclude(
|
|||
|
groups__name__in=['direction']
|
|||
|
).distinct()
|
|||
|
|
|||
|
@classmethod
|
|||
|
def intervenants(cls, annee=None):
|
|||
|
return cls._intervenants(
|
|||
|
groupes=['aemo'],
|
|||
|
annee=annee
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
class GroupInfo(models.Model):
|
|||
|
group = models.OneToOneField(Group, on_delete=models.CASCADE)
|
|||
|
description = models.TextField(blank=True)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return f"Description of group {self.group.name}"
|
|||
|
|
|||
|
|
|||
|
class Region(models.Model):
|
|||
|
nom = models.CharField(max_length=30, unique=True)
|
|||
|
rue = models.CharField("Rue", max_length=100, blank=True)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return self.nom
|
|||
|
|
|||
|
def __lt__(self, other):
|
|||
|
return self.nom < (other.nom if other else '')
|
|||
|
|
|||
|
|
|||
|
class FamilleQuerySet(models.QuerySet):
|
|||
|
def with_duos(self):
|
|||
|
"""
|
|||
|
Renvoie une liste des groupes Psy/Educ différents, sous forme de chaînes
|
|||
|
de sigles séparées par "/".
|
|||
|
Peut aussi renvoyer 1 ou 3 intervenants, le cas échéant.
|
|||
|
"""
|
|||
|
return self.annotate(
|
|||
|
duo=StringAgg(
|
|||
|
F('suivi__intervenant__intervenant__sigle'),
|
|||
|
delimiter='/',
|
|||
|
filter=(
|
|||
|
Q(suivi__intervenant__date_fin__isnull=True) &
|
|||
|
Q(suivi__intervenant__role__nom__in=['Educ', 'Psy'])
|
|||
|
),
|
|||
|
ordering='suivi__intervenant__intervenant__sigle',
|
|||
|
)
|
|||
|
)
|
|||
|
|
|||
|
def with_niveau_interv(self):
|
|||
|
"""
|
|||
|
Annote les familles avec le niveau d’intervention le plus récent.
|
|||
|
"""
|
|||
|
return self.annotate(
|
|||
|
niveau_interv=Subquery(
|
|||
|
Niveau.objects.filter(
|
|||
|
famille=OuterRef('pk')
|
|||
|
).order_by('-date_debut').values('niveau_interv')[:1]
|
|||
|
)
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
class FamilleManager(models.Manager):
|
|||
|
def get_queryset(self):
|
|||
|
return FamilleQuerySet(self.model, using=self._db).filter(
|
|||
|
suivi__isnull=False
|
|||
|
).prefetch_related(
|
|||
|
models.Prefetch('membres', queryset=Personne.objects.select_related('role'))
|
|||
|
)
|
|||
|
|
|||
|
def create_famille(self, equipe='', **kwargs):
|
|||
|
kwargs.pop('_state', None)
|
|||
|
famille = self.create(**kwargs)
|
|||
|
Suivi.objects.create(
|
|||
|
famille=famille,
|
|||
|
equipe=equipe
|
|||
|
)
|
|||
|
famille.suivi.date_demande = date.today()
|
|||
|
famille.suivi.save()
|
|||
|
return famille
|
|||
|
|
|||
|
|
|||
|
class Famille(models.Model):
|
|||
|
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
|||
|
archived_at = models.DateTimeField('Archivée le', blank=True, null=True)
|
|||
|
nom = models.CharField('Nom de famille', max_length=40)
|
|||
|
rue = models.CharField('Rue', max_length=60, blank=True)
|
|||
|
npa = models.CharField('NPA', max_length=4, blank=True)
|
|||
|
localite = models.CharField('Localité', max_length=30, blank=True)
|
|||
|
region = models.ForeignKey(to=Region, null=True, blank=True, default=None, on_delete=models.SET_NULL)
|
|||
|
telephone = models.CharField('Tél.', max_length=60, blank=True)
|
|||
|
autorite_parentale = models.CharField("Autorité parentale", max_length=20,
|
|||
|
choices=choices.AUTORITE_PARENTALE_CHOICES, blank=True)
|
|||
|
monoparentale = models.BooleanField('Famille monoparent.', default=None, blank=True, null=True)
|
|||
|
statut_marital = models.CharField("Statut marital", max_length=20, choices=choices.STATUT_MARITAL_CHOICES,
|
|||
|
blank=True)
|
|||
|
connue = models.BooleanField("famille déjà suivie", default=False)
|
|||
|
accueil = models.BooleanField("famille d'accueil", default=False)
|
|||
|
# Pour CIPE
|
|||
|
besoins_part = models.BooleanField("famille à besoins particuliers", default=False)
|
|||
|
sap = models.BooleanField(default=False, verbose_name="famille s@p")
|
|||
|
garde = models.CharField(
|
|||
|
'Type de garde', max_length=20, choices=choices.TYPE_GARDE_CHOICES, blank=True
|
|||
|
)
|
|||
|
provenance = models.CharField(
|
|||
|
'Provenance', max_length=30, choices=choices.PROVENANCE_DESTINATION_CHOICES, blank=True)
|
|||
|
destination = models.CharField(
|
|||
|
'Destination', max_length=30, choices=choices.PROVENANCE_DESTINATION_CHOICES, blank=True)
|
|||
|
statut_financier = models.CharField(
|
|||
|
'Statut financier', max_length=30, choices=choices.STATUT_FINANCIER_CHOICES, blank=True)
|
|||
|
remarques = models.TextField(blank=True)
|
|||
|
|
|||
|
objects = FamilleManager()
|
|||
|
|
|||
|
class Meta:
|
|||
|
ordering = ('nom', 'npa')
|
|||
|
permissions = (
|
|||
|
("can_manage_waiting_list", "Gérer la liste d'attente"),
|
|||
|
("can_archive", "Archiver les dossiers AEMO"),
|
|||
|
('export_stats', 'Exporter les statistiques'),
|
|||
|
)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return '{} - {}'.format(self.nom, self.adresse)
|
|||
|
|
|||
|
@property
|
|||
|
def adresse(self):
|
|||
|
return format_adresse(self.rue, self.npa, self.localite)
|
|||
|
|
|||
|
@property
|
|||
|
def suivi(self):
|
|||
|
return getattr(self, self.suivi_name) if self.suivi_name else None
|
|||
|
|
|||
|
@property
|
|||
|
def suivi_url(self):
|
|||
|
return reverse('famille-suivi', args=[self.pk])
|
|||
|
|
|||
|
@property
|
|||
|
def edit_url(self):
|
|||
|
return reverse('famille-edit', args=[self.pk])
|
|||
|
|
|||
|
@property
|
|||
|
def add_person_url(self):
|
|||
|
return reverse('personne-add', args=[self.pk])
|
|||
|
|
|||
|
def redirect_after_personne_creation(self, personne):
|
|||
|
return self.edit_url
|
|||
|
|
|||
|
@property
|
|||
|
def print_coords_url(self):
|
|||
|
return reverse('print-coord-famille', args=[self.pk])
|
|||
|
|
|||
|
@classmethod
|
|||
|
def actives(cls, date=None):
|
|||
|
qs = Famille.objects.filter(suivi__isnull=False).order_by('nom', 'npa')
|
|||
|
if date is not None:
|
|||
|
return qs.filter(suivi__date_demande__lte=date).filter(
|
|||
|
models.Q(suivi__date_fin_suivi__isnull=True) |
|
|||
|
models.Q(suivi__date_fin_suivi__gte=date)
|
|||
|
)
|
|||
|
else:
|
|||
|
return qs.filter(suivi__date_fin_suivi__isnull=True)
|
|||
|
|
|||
|
def interventions_actives(self, dt=None):
|
|||
|
dt = dt or date.today()
|
|||
|
return self.suivi.intervenant_set.filter(
|
|||
|
Q(date_debut__lte=dt) & (
|
|||
|
Q(date_fin__isnull=True) | Q(date_fin__gte=dt)
|
|||
|
)
|
|||
|
).select_related('intervenant', 'role')
|
|||
|
|
|||
|
def niveau_actuel(self):
|
|||
|
try:
|
|||
|
return [
|
|||
|
niv.niveau_interv for niv in sorted(
|
|||
|
self.niveaux.all(), key=attrgetter('date_debut')
|
|||
|
)
|
|||
|
][-1]
|
|||
|
except IndexError:
|
|||
|
return None
|
|||
|
|
|||
|
def membres_suivis(self):
|
|||
|
if 'membres' in getattr(self, '_prefetched_objects_cache', {}):
|
|||
|
return sorted([
|
|||
|
pers for pers in self._prefetched_objects_cache['membres'] if pers.role.nom == "Enfant suivi"
|
|||
|
], key=lambda p: p.date_naissance or date(1950, 1, 1))
|
|||
|
else:
|
|||
|
return self.membres.filter(role__nom="Enfant suivi").order_by('date_naissance')
|
|||
|
|
|||
|
def enfants_non_suivis(self):
|
|||
|
if 'membres' in getattr(self, '_prefetched_objects_cache', {}):
|
|||
|
return sorted([
|
|||
|
pers for pers in self._prefetched_objects_cache['membres'] if pers.role.nom == "Enfant non-suivi"
|
|||
|
], key=lambda p: p.date_naissance or date(1950, 1, 1))
|
|||
|
else:
|
|||
|
return self.membres.filter(role__nom='Enfant non-suivi').order_by('date_naissance')
|
|||
|
|
|||
|
def parents(self):
|
|||
|
if 'membres' in getattr(self, '_prefetched_objects_cache', {}):
|
|||
|
parents = [
|
|||
|
pers for pers in self._prefetched_objects_cache['membres']
|
|||
|
if pers.role.nom in Role.ROLES_PARENTS
|
|||
|
]
|
|||
|
else:
|
|||
|
parents = [
|
|||
|
pers for pers in self.membres.filter(role__nom__in=Role.ROLES_PARENTS)
|
|||
|
]
|
|||
|
return sorted(parents, key=lambda p: p.role.nom) # Mère avant Père
|
|||
|
|
|||
|
def autres_parents(self):
|
|||
|
excluded_roles = Role.ROLES_PARENTS + ['Enfant suivi', 'Enfant non-suivi']
|
|||
|
if 'membres' in getattr(self, '_prefetched_objects_cache', {}):
|
|||
|
return [
|
|||
|
pers for pers in self._prefetched_objects_cache['membres']
|
|||
|
if pers.role.nom not in excluded_roles
|
|||
|
]
|
|||
|
else:
|
|||
|
return self.membres.exclude(role__nom__in=excluded_roles)
|
|||
|
|
|||
|
def access_ok(self, user):
|
|||
|
if user.has_perm('aemo.change_famille') or user.is_responsable:
|
|||
|
return True
|
|||
|
intervs = self.interventions_actives()
|
|||
|
return not intervs or user in [interv.intervenant for interv in intervs]
|
|||
|
|
|||
|
def can_view(self, user):
|
|||
|
return user.has_perm('aemo.view_famille')
|
|||
|
|
|||
|
def can_edit(self, user):
|
|||
|
if user.has_perm('aemo.change_famille') or user.is_responsable:
|
|||
|
return True
|
|||
|
if not self.suivi.date_fin_suivi or self.suivi.date_fin_suivi >= date.today():
|
|||
|
intervs = self.interventions_actives()
|
|||
|
if (
|
|||
|
self.can_view(user) and
|
|||
|
(not intervs or user in [
|
|||
|
interv.intervenant for interv in intervs if interv.role.est_editeur
|
|||
|
])
|
|||
|
):
|
|||
|
return True
|
|||
|
return False
|
|||
|
|
|||
|
def can_be_deleted(self, user):
|
|||
|
if not user.has_perm('aemo.change_famille'):
|
|||
|
return False
|
|||
|
if self.suivi.motif_fin_suivi == 'erreur':
|
|||
|
return True
|
|||
|
return False
|
|||
|
|
|||
|
def can_be_archived(self, user):
|
|||
|
if self.archived_at:
|
|||
|
return False
|
|||
|
closed_since = date.today() - self.suivi.date_fin_suivi
|
|||
|
return (
|
|||
|
user.has_perm('aemo.can_archive') and
|
|||
|
self.suivi.date_fin_suivi is not None and
|
|||
|
closed_since.days > 180 and
|
|||
|
self.suivi.date_fin_suivi.year < date.today().year and
|
|||
|
(date.today().month > 1 or closed_since.days > 400)
|
|||
|
)
|
|||
|
|
|||
|
def temps_total_prestations(self, interv=None):
|
|||
|
"""
|
|||
|
Temps total des prestations liées à la famille, quel que soit le nombre
|
|||
|
de membres suivis.
|
|||
|
Filtré facultativement par intervenant.
|
|||
|
"""
|
|||
|
prest = self.prestations if interv is None else self.prestations.filter(intervenant=interv)
|
|||
|
return prest.annotate(num_util=Count('intervenants')).aggregate(
|
|||
|
total=Sum(F('duree') * F('num_util'), output_field=models.DurationField())
|
|||
|
)['total'] or timedelta()
|
|||
|
|
|||
|
def temps_total_prestations_reparti(self):
|
|||
|
"""
|
|||
|
Temps total des prestations liées à la famille, divisé par le nombre d'enfants suivis.
|
|||
|
"""
|
|||
|
duree = self.temps_total_prestations()
|
|||
|
duree //= (len(self.membres_suivis()) or 1)
|
|||
|
return duree
|
|||
|
|
|||
|
def total_mensuel(self, date_debut_mois):
|
|||
|
"""
|
|||
|
Temps total de prestations sur un mois donné
|
|||
|
"""
|
|||
|
return self.prestations.filter(
|
|||
|
date_prestation__month=date_debut_mois.month,
|
|||
|
date_prestation__year=date_debut_mois.year
|
|||
|
).aggregate(total=Sum('duree'))['total'] or timedelta()
|
|||
|
|
|||
|
def total_mensuel_par_prestation(self, prest_codes, date_debut_mois):
|
|||
|
"""
|
|||
|
Temps total d'évaluation sur un mois donné par prestation.
|
|||
|
"""
|
|||
|
return self.prestations.annotate(
|
|||
|
num_util=Count('intervenants')
|
|||
|
).filter(
|
|||
|
lib_prestation__code__in=prest_codes,
|
|||
|
date_prestation__month=date_debut_mois.month,
|
|||
|
date_prestation__year=date_debut_mois.year
|
|||
|
).aggregate(
|
|||
|
total=Sum(F('duree') * F('num_util'), output_field=models.DurationField())
|
|||
|
)['total'] or timedelta()
|
|||
|
|
|||
|
def total_mensuel_evaluation(self, date_debut_mois):
|
|||
|
return self.total_mensuel_par_prestation(['aemo01'], date_debut_mois)
|
|||
|
|
|||
|
def total_mensuel_suivi(self, date_debut_mois):
|
|||
|
return self.total_mensuel_par_prestation(['aemo02', 'aemo04', 'aemo05', 'aemo06', 'aemo07'], date_debut_mois)
|
|||
|
|
|||
|
def prestations_historiques(self):
|
|||
|
return Prestation.prestations_historiques(self.prestations.all())
|
|||
|
|
|||
|
def prestations_du_mois(self):
|
|||
|
"""
|
|||
|
Retourne le détail des prestations pour cette famille
|
|||
|
à partir du mois courant
|
|||
|
"""
|
|||
|
date_ref = date(date.today().year, date.today().month, 1)
|
|||
|
return self.prestations.filter(date_prestation__gte=date_ref)
|
|||
|
|
|||
|
@classmethod
|
|||
|
def suivis_en_cours(cls, debut, fin):
|
|||
|
base = cls.objects.annotate(
|
|||
|
date_deb=Coalesce(
|
|||
|
'suivi__date_demande',
|
|||
|
'suivi__date_debut_evaluation', 'suivi__date_debut_suivi'
|
|||
|
),
|
|||
|
).prefetch_related('membres')
|
|||
|
return base.filter(
|
|||
|
date_deb__lte=fin
|
|||
|
).filter(
|
|||
|
Q(**{'suivi__date_fin_suivi__isnull': True}) |
|
|||
|
Q(**{'suivi__date_fin_suivi__gte': debut})
|
|||
|
).exclude(**{'suivi__motif_fin_suivi': 'erreur'})
|
|||
|
|
|||
|
@classmethod
|
|||
|
def suivis_nouveaux(cls, annee, mois):
|
|||
|
base = cls.objects.annotate(
|
|||
|
date_dem=F('suivi__date_demande')
|
|||
|
).prefetch_related('membres')
|
|||
|
return base.filter(**{
|
|||
|
'date_dem__year': annee, 'date_dem__month': mois,
|
|||
|
}).exclude(**{'suivi__motif_fin_suivi': 'erreur'})
|
|||
|
|
|||
|
@classmethod
|
|||
|
def suivis_termines(cls, annee, mois):
|
|||
|
base = cls.objects.all().prefetch_related('membres')
|
|||
|
return base.filter(**{
|
|||
|
'suivi__date_fin_suivi__year': annee,
|
|||
|
'suivi__date_fin_suivi__month': mois,
|
|||
|
}).exclude(**{'suivi__motif_fin_suivi': 'erreur'})
|
|||
|
|
|||
|
def can_be_reactivated(self, user):
|
|||
|
return user.has_perm("aemo.change_famille") and self.suivi.date_fin_suivi is not None
|
|||
|
|
|||
|
def anonymiser(self):
|
|||
|
# Famille
|
|||
|
self.nom = random_string_generator()
|
|||
|
self.rue = ''
|
|||
|
self.telephone = ''
|
|||
|
self.remarques = ''
|
|||
|
self.archived_at = timezone.now()
|
|||
|
self.save()
|
|||
|
|
|||
|
# Personne
|
|||
|
for personne in self.membres.all().select_related('role'):
|
|||
|
if personne.role.nom == 'Enfant suivi':
|
|||
|
personne.nom = random_string_generator()
|
|||
|
personne.prenom = random_string_generator()
|
|||
|
personne.validite = None
|
|||
|
personne.contractant = False
|
|||
|
fields = ['rue', 'npa', 'localite', 'telephone', 'email', 'remarque', 'remarque_privee',
|
|||
|
'profession', 'filiation', 'allergies', 'employeur', 'permis', 'animaux']
|
|||
|
[setattr(personne, field, '') for field in fields]
|
|||
|
personne.save()
|
|||
|
personne.reseaux.clear()
|
|||
|
if hasattr(personne, 'formation'):
|
|||
|
personne.formation.cercle_scolaire = None
|
|||
|
fields = ['college', 'classe', 'enseignant', 'creche', 'creche_resp', 'entreprise',
|
|||
|
'maitre_apprentissage', 'remarque']
|
|||
|
[setattr(personne.formation, field, '') for field in fields]
|
|||
|
personne.formation.save()
|
|||
|
else:
|
|||
|
personne.delete()
|
|||
|
# Suivi
|
|||
|
fields = ['difficultes', 'aides', 'competences', 'autres_contacts', 'disponibilites', 'remarque',
|
|||
|
'remarque_privee', 'motif_detail', 'referent_note', 'collaboration', 'ressources', 'crise',
|
|||
|
'pers_famille_presentes', 'ref_presents', 'autres_pers_presentes']
|
|||
|
[setattr(self.suivi, field, '') for field in fields]
|
|||
|
self.suivi.save()
|
|||
|
|
|||
|
# Related
|
|||
|
for doc in self.documents.all():
|
|||
|
doc.delete()
|
|||
|
for bilan in self.bilans.all():
|
|||
|
bilan.delete()
|
|||
|
for rapport in self.rapports.all():
|
|||
|
rapport.delete()
|
|||
|
self.prestations.all().update(texte='')
|
|||
|
for prestation in self.prestations.exclude(fichier=''):
|
|||
|
prestation.fichier.delete()
|
|||
|
prestation.save()
|
|||
|
|
|||
|
|
|||
|
class PersonneManager(models.Manager):
|
|||
|
|
|||
|
def create_personne(self, **kwargs):
|
|||
|
kwargs.pop('_state', None)
|
|||
|
pers = self.create(**kwargs)
|
|||
|
return self.add_formation(pers)
|
|||
|
|
|||
|
def add_formation(self, pers):
|
|||
|
if pers.role.nom == 'Enfant suivi' and not hasattr(pers, 'formation'):
|
|||
|
moins_de_4ans = bool(pers.age and pers.age < 4)
|
|||
|
Formation.objects.create(
|
|||
|
personne=pers, statut='pre_scol' if moins_de_4ans else ''
|
|||
|
)
|
|||
|
if moins_de_4ans:
|
|||
|
try:
|
|||
|
pers.famille.suivi.demande_prioritaire = True
|
|||
|
pers.famille.suivi.save()
|
|||
|
except AttributeError:
|
|||
|
pass
|
|||
|
return pers
|
|||
|
|
|||
|
|
|||
|
class Personne(models.Model):
|
|||
|
""" Classe de base des personnes """
|
|||
|
created_at = models.DateTimeField(auto_now_add=True, editable=False, null=True)
|
|||
|
|
|||
|
nom = models.CharField('Nom', max_length=30)
|
|||
|
prenom = models.CharField('Prénom', max_length=30, blank=True)
|
|||
|
date_naissance = models.DateField('Date de naissance', null=True, blank=True)
|
|||
|
genre = models.CharField('Genre', max_length=1, choices=(('M', 'M'), ('F', 'F')), default='M')
|
|||
|
rue = models.CharField('Rue', max_length=60, blank=True)
|
|||
|
npa = models.CharField('NPA', max_length=4, blank=True)
|
|||
|
localite = models.CharField('Localité', max_length=30, blank=True)
|
|||
|
telephone = models.CharField('Tél.', max_length=60, blank=True)
|
|||
|
email = models.EmailField('Courriel', blank=True)
|
|||
|
pays_origine = CountryField('Nationalité', blank=True)
|
|||
|
remarque = models.TextField(blank=True)
|
|||
|
remarque_privee = models.TextField('Remarque privée', blank=True)
|
|||
|
famille = models.ForeignKey(Famille, on_delete=models.CASCADE, related_name='membres')
|
|||
|
role = models.ForeignKey('Role', on_delete=models.PROTECT)
|
|||
|
profession = models.CharField('Profession', max_length=50, blank=True)
|
|||
|
filiation = models.CharField('Filiation', max_length=80, blank=True)
|
|||
|
decedee = models.BooleanField('Cette personne est décédée', default=False)
|
|||
|
reseaux = models.ManyToManyField(Contact, blank=True)
|
|||
|
allergies = models.TextField('Allergies', blank=True)
|
|||
|
# Champs spécifiques SIFP
|
|||
|
employeur = models.CharField('Adresse empl.', max_length=50, blank=True)
|
|||
|
permis = models.CharField('Permis/séjour', max_length=30, blank=True)
|
|||
|
validite = models.DateField('Date validité', blank=True, null=True)
|
|||
|
animaux = models.BooleanField('Animaux', default=None, null=True)
|
|||
|
|
|||
|
objects = PersonneManager()
|
|||
|
|
|||
|
class Meta:
|
|||
|
verbose_name = 'Personne'
|
|||
|
ordering = ('nom', 'prenom')
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return '{} {}'.format(self.nom, self.prenom)
|
|||
|
|
|||
|
@property
|
|||
|
def nom_prenom(self):
|
|||
|
return str(self)
|
|||
|
|
|||
|
@property
|
|||
|
def adresse(self):
|
|||
|
return format_adresse(self.rue, self.npa, self.localite)
|
|||
|
|
|||
|
@property
|
|||
|
def age(self):
|
|||
|
return self.age_a(date.today())
|
|||
|
|
|||
|
def age_str(self, date_=None, format_='auto'):
|
|||
|
"""Renvoie '2 ans, 4 mois', '5 ans', '8 mois'.
|
|||
|
Param:
|
|||
|
'auto': application du format
|
|||
|
- 'jour' si l'âge < 30 jours
|
|||
|
- 'sem_jour' si l'âge < 77 jours (11 semaines)
|
|||
|
- 'mois_sem' si l'âge < 690 jours (23 mois)
|
|||
|
- 'an_mois': autres cas
|
|||
|
'jour': âge en jours
|
|||
|
'sem_jour': âge en semaines et jours
|
|||
|
'mois_sem': âge en mois et semaines
|
|||
|
"""
|
|||
|
if not self.date_naissance:
|
|||
|
# Pourrait disparaître si date_naissance devient non null
|
|||
|
return ''
|
|||
|
if format_ not in ['auto', 'jour', 'sem_jour', 'mois_sem', 'an_mois']:
|
|||
|
raise ValueError('Paramètre erroné')
|
|||
|
age_jours = self.age_jours(date_)
|
|||
|
|
|||
|
if (age_jours < 30 and format_ == 'auto') or format_ == 'jour':
|
|||
|
age_str = f"{age_jours} jour{'s' if age_jours > 1 else ''}"
|
|||
|
elif (age_jours < 77 and format_ == 'auto') or format_ == 'sem_jour':
|
|||
|
sem, jours = age_jours // 7, age_jours % 7
|
|||
|
age_str = f"{sem} sem."
|
|||
|
age_str += f" {jours} jour{'s' if jours > 1 else ''}" if jours > 0 else ''
|
|||
|
elif (age_jours < 690 and format_ == 'auto') or format_ == 'mois_sem':
|
|||
|
mois, sem = age_jours // 30, int((age_jours % 30)/7)
|
|||
|
age_str = f"{mois} mois"
|
|||
|
age_str += f" {sem} sem." if sem > 0 else ''
|
|||
|
else:
|
|||
|
ans, mois = int(age_jours / 365.25), int((age_jours % 365.25) / 30)
|
|||
|
age_str = f"{ans} an{'s' if ans > 1 else ''}"
|
|||
|
age_str += " %d mois" % mois if mois > 0 else ''
|
|||
|
return age_str
|
|||
|
|
|||
|
def age_a(self, date_):
|
|||
|
if not self.date_naissance:
|
|||
|
# Pourrait disparaître si date_naissance devient non null
|
|||
|
return None
|
|||
|
age = (date_ - self.date_naissance).days / 365.25
|
|||
|
return int(age * 10) / 10 # 1 décimale arrondi vers le bas
|
|||
|
|
|||
|
def age_jours(self, date_=None):
|
|||
|
if date_ is None:
|
|||
|
date_ = date.today()
|
|||
|
return (date_ - self.date_naissance).days
|
|||
|
|
|||
|
def age_mois(self, date_=None):
|
|||
|
"""Âge en mois à la date_"""
|
|||
|
if date_ is None:
|
|||
|
date_ = date.today()
|
|||
|
if not self.date_naissance or date_ < self.date_naissance:
|
|||
|
return None
|
|||
|
return self.age_jours(date_) / (365 / 12)
|
|||
|
|
|||
|
@property
|
|||
|
def localite_display(self):
|
|||
|
return '{} {}'.format(self.npa, self.localite)
|
|||
|
|
|||
|
@property
|
|||
|
def edit_url(self):
|
|||
|
return reverse('personne-edit', args=[self.famille_id, self.pk])
|
|||
|
|
|||
|
def can_edit(self, user):
|
|||
|
return self.famille.can_edit(user)
|
|||
|
|
|||
|
def can_be_deleted(self, user):
|
|||
|
return self.famille.can_be_deleted(user)
|
|||
|
|
|||
|
|
|||
|
class Formation(models.Model):
|
|||
|
|
|||
|
FORMATION_CHOICES = (
|
|||
|
('pre_scol', 'Pré-scolaire'),
|
|||
|
('cycle1', 'Cycle 1'),
|
|||
|
('cycle2', 'Cycle 2'),
|
|||
|
('cycle3', 'Cycle 3'),
|
|||
|
('apprenti', 'Apprentissage'),
|
|||
|
('etudiant', 'Etudiant'),
|
|||
|
('en_emploi', 'En emploi'),
|
|||
|
('sans_emploi', 'Sans emploi'),
|
|||
|
('sans_occupation', 'Sans occupation'),
|
|||
|
)
|
|||
|
|
|||
|
personne = models.OneToOneField(Personne, on_delete=models.CASCADE)
|
|||
|
|
|||
|
statut = models.CharField('Scolarité', max_length=20, choices=FORMATION_CHOICES, blank=True)
|
|||
|
cercle_scolaire = models.ForeignKey(
|
|||
|
CercleScolaire, blank=True, null=True, on_delete=models.SET_NULL,
|
|||
|
related_name='+', verbose_name='Cercle scolaire'
|
|||
|
)
|
|||
|
college = models.CharField('Collège', max_length=50, blank=True)
|
|||
|
classe = models.CharField('Classe', max_length=50, blank=True)
|
|||
|
enseignant = models.CharField('Enseignant', max_length=50, blank=True)
|
|||
|
|
|||
|
creche = models.CharField('Crèche', max_length=50, blank=True)
|
|||
|
creche_resp = models.CharField('Resp.crèche', max_length=50, blank=True)
|
|||
|
|
|||
|
entreprise = models.CharField('Entreprise', max_length=50, blank=True)
|
|||
|
maitre_apprentissage = models.CharField("Maître d'appr.", max_length=50, blank=True)
|
|||
|
|
|||
|
remarque = models.TextField(blank=True)
|
|||
|
|
|||
|
class Meta:
|
|||
|
verbose_name = 'Scolarité'
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return 'Scolarité de {}'.format(self.personne)
|
|||
|
|
|||
|
def can_edit(self, user):
|
|||
|
return self.personne.famille.can_edit(user)
|
|||
|
|
|||
|
def delete(self, **kwargs):
|
|||
|
if self.personne:
|
|||
|
raise IntegrityError
|
|||
|
else:
|
|||
|
super().delete(**kwargs)
|
|||
|
|
|||
|
@property
|
|||
|
def sse(self):
|
|||
|
return "Champ à définir"
|
|||
|
|
|||
|
def pdf_data(self):
|
|||
|
if self.personne.formation.statut == 'pre_scol':
|
|||
|
data = [
|
|||
|
['Crèche: {}'.format(self.creche), 'responsable: {}'.format(self.creche_resp)],
|
|||
|
]
|
|||
|
elif self.personne.formation.statut in ['cycle1', 'cycle2', 'cycle3', 'etudiant']:
|
|||
|
data = [
|
|||
|
['Cercle: {}'.format(self.cercle_scolaire), 'collège: {}'.format(self.college)],
|
|||
|
['Classe: {}'.format(self.classe), 'Enseignant-e: {}'.format(self.enseignant)]
|
|||
|
]
|
|||
|
|
|||
|
elif self.personne.formation.statut == 'apprenti':
|
|||
|
data = [
|
|||
|
['Employeur: {}'.format(self.entreprise), 'resp. apprenti-e: {}'.format(self.maitre_apprentissage)],
|
|||
|
]
|
|||
|
else:
|
|||
|
data = [self.personne.formation.statut]
|
|||
|
return data
|
|||
|
|
|||
|
def info_scol(self):
|
|||
|
if self.statut == 'pre_scol':
|
|||
|
creche = f"Crèche: {self.creche}" if self.creche else ''
|
|||
|
resp = f" - resp.: {self.creche_resp}" if self.creche_resp else ''
|
|||
|
data = ''.join([creche, resp])
|
|||
|
elif self.statut in ['cycle1', 'cycle2', 'cycle3', 'etudiant']:
|
|||
|
college = f"Collège: {self.college}" if self.college else ''
|
|||
|
classe = f" - classe: {self.classe}" if self.classe else ''
|
|||
|
ens = f" - ens.: {self.enseignant}" if self.enseignant else ''
|
|||
|
data = ''.join([college, classe, ens])
|
|||
|
elif self.statut == 'apprenti':
|
|||
|
emp = f"Employeur: {self.entreprise}" if self.entreprise else ''
|
|||
|
resp = f" - resp.: {self.maitre_apprentissage}" if self.maitre_apprentissage else ''
|
|||
|
data = ''.join([emp, resp])
|
|||
|
elif self.statut:
|
|||
|
data = self.get_statut_display() if self.statut else ''
|
|||
|
else:
|
|||
|
data = "Formation: aucune info. saisie"
|
|||
|
return data
|
|||
|
|
|||
|
|
|||
|
class Document(models.Model):
|
|||
|
famille = models.ForeignKey(Famille, related_name='documents', on_delete=models.CASCADE)
|
|||
|
fichier = models.FileField("Nouveau fichier", upload_to='doc')
|
|||
|
titre = models.CharField(max_length=100)
|
|||
|
|
|||
|
class Meta:
|
|||
|
unique_together = ('famille', 'titre')
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return self.titre
|
|||
|
|
|||
|
def delete(self):
|
|||
|
Path(self.fichier.path).unlink(missing_ok=True)
|
|||
|
return super().delete()
|
|||
|
|
|||
|
def can_edit(self, user):
|
|||
|
return self.famille.can_edit(user)
|
|||
|
|
|||
|
def can_be_deleted(self, user):
|
|||
|
return self.famille.can_be_deleted(user)
|
|||
|
|
|||
|
|
|||
|
class Bilan(models.Model):
|
|||
|
famille = models.ForeignKey(Famille, related_name='bilans', on_delete=models.CASCADE)
|
|||
|
date = models.DateField("Date du bilan")
|
|||
|
auteur = models.ForeignKey(
|
|||
|
Utilisateur, related_name='bilans', on_delete=models.PROTECT, null=True,
|
|||
|
)
|
|||
|
objectifs = models.TextField("Objectifs")
|
|||
|
rythme = models.TextField("Rythme et fréquence")
|
|||
|
sig_famille = models.BooleanField("Apposer signature de la famille", default=True)
|
|||
|
sig_interv = models.ManyToManyField(Utilisateur, blank=True, verbose_name="Signature des intervenants")
|
|||
|
fichier = models.FileField("Fichier/image", blank=True, upload_to='bilans')
|
|||
|
|
|||
|
delete_urlname = 'famille-bilan-delete'
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return "Bilan du {} pour la famille {}".format(self.date, self.famille)
|
|||
|
|
|||
|
def get_absolute_url(self):
|
|||
|
return reverse('famille-bilan-view', args=[self.famille_id, self.pk])
|
|||
|
|
|||
|
def get_print_url(self):
|
|||
|
return reverse('print-bilan', args=[self.pk])
|
|||
|
|
|||
|
def edit_url(self):
|
|||
|
return reverse('famille-bilan-edit', args=[self.famille_id, self.pk])
|
|||
|
|
|||
|
def can_edit(self, user):
|
|||
|
return self.famille.can_edit(user) or user in self.famille.suivi.intervenants.all()
|
|||
|
|
|||
|
def title(self):
|
|||
|
return f"Bilan du {format_d_m_Y(self.date)}"
|
|||
|
|
|||
|
|
|||
|
class Rapport(models.Model):
|
|||
|
"""
|
|||
|
Rapport est l'appellation historique, Résumé étant l'appellation moderne (dès février 2023).
|
|||
|
Les champs «techniques» n'ont pas été renommés.
|
|||
|
"""
|
|||
|
famille = models.ForeignKey(Famille, related_name='rapports', on_delete=models.CASCADE)
|
|||
|
date = models.DateField("Date du résumé")
|
|||
|
auteur = models.ForeignKey(Utilisateur, on_delete=models.PROTECT)
|
|||
|
pres_interv = models.ManyToManyField(
|
|||
|
Utilisateur, blank=True, verbose_name="Intervenants cités dans le résumé", related_name='+'
|
|||
|
)
|
|||
|
sig_interv = models.ManyToManyField(
|
|||
|
Utilisateur, blank=True, verbose_name="Signature des intervenants", related_name='+'
|
|||
|
)
|
|||
|
situation = models.TextField("Situation / contexte familial", blank=True)
|
|||
|
observations = models.TextField("Observations, évolution et hypothèses", blank=True)
|
|||
|
projet = models.TextField("Perspectives d’avenir", blank=True)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return "Résumé du {} pour la famille {}".format(format_d_m_Y(self.date), self.famille)
|
|||
|
|
|||
|
def get_absolute_url(self):
|
|||
|
return reverse('famille-rapport-view', args=[self.famille.pk, self.pk])
|
|||
|
|
|||
|
def edit_url(self):
|
|||
|
return reverse('famille-rapport-edit', args=[self.famille.pk, self.pk])
|
|||
|
|
|||
|
def get_print_url(self):
|
|||
|
return reverse('famille-rapport-print', args=[self.famille.pk, self.pk])
|
|||
|
|
|||
|
def can_edit(self, user):
|
|||
|
return self.famille.can_edit(user) or user in self.famille.suivi.intervenants.all()
|
|||
|
|
|||
|
def can_delete(self, user):
|
|||
|
return self.famille.can_edit(user)
|
|||
|
|
|||
|
def title(self):
|
|||
|
return f"Résumé du {format_d_m_Y(self.date)}"
|
|||
|
|
|||
|
def intervenants(self):
|
|||
|
return [i.intervenant for i in self.famille.interventions_actives(self.date)]
|
|||
|
|
|||
|
|
|||
|
class Niveau(models.Model):
|
|||
|
INTERV_CHOICES = [(0, '0'), (1, '1'), (2, '2'), (3, '3')]
|
|||
|
famille = models.ForeignKey(
|
|||
|
Famille, related_name='niveaux', on_delete=models.CASCADE, verbose_name="Famille"
|
|||
|
)
|
|||
|
niveau_interv = models.PositiveSmallIntegerField("Niveau d’intervention", choices=INTERV_CHOICES)
|
|||
|
date_debut = models.DateField('Date début', blank=True, null=True)
|
|||
|
date_fin = models.DateField('Date fin', blank=True, null=True)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return (
|
|||
|
f"{self.famille.nom} - {self.niveau_interv} - du {format_d_m_Y(self.date_debut)} au "
|
|||
|
f"{format_d_m_Y(self.date_fin)}"
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
class LibellePrestation(models.Model):
|
|||
|
code = models.CharField('Code', max_length=6, unique=True)
|
|||
|
nom = models.CharField('Nom', max_length=30)
|
|||
|
actes = models.TextField('Actes à prester', blank=True)
|
|||
|
|
|||
|
class Meta:
|
|||
|
ordering = ('code',)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return self.nom
|
|||
|
|
|||
|
|
|||
|
class Prestation(models.Model):
|
|||
|
famille = models.ForeignKey(Famille, related_name='prestations', null=True, blank=True,
|
|||
|
on_delete=models.SET_NULL, verbose_name="Famille")
|
|||
|
auteur = models.ForeignKey(Utilisateur, on_delete=models.PROTECT, verbose_name='auteur')
|
|||
|
intervenants = models.ManyToManyField(Utilisateur, related_name='prestations')
|
|||
|
date_prestation = models.DateField("date de l’intervention")
|
|||
|
duree = models.DurationField("durée")
|
|||
|
lib_prestation = models.ForeignKey(
|
|||
|
LibellePrestation, on_delete=models.SET_NULL, null=True, default=None,
|
|||
|
related_name='prestations_%(app_label)s'
|
|||
|
)
|
|||
|
# Nombre de familles actives au moment de la prestation, utilisé pour ventiler les heures
|
|||
|
# dans les statistiques.
|
|||
|
familles_actives = models.PositiveSmallIntegerField(blank=True, default=0)
|
|||
|
texte = models.TextField('Contenu', blank=True)
|
|||
|
manque = models.BooleanField('Rendez-vous manqué', default=False)
|
|||
|
fichier = models.FileField('Fichier/image', upload_to='prestations', blank=True)
|
|||
|
|
|||
|
# Nbre de jours maximum après fin d'un mois où il est encore possible de saisir
|
|||
|
# des données du mois précédent.
|
|||
|
DELAI_SAISIE_SUPPL = 14
|
|||
|
|
|||
|
class Meta:
|
|||
|
ordering = ('-date_prestation',)
|
|||
|
permissions = (
|
|||
|
('edit_prest_prev_month', 'Modifier prestations du mois précédent'),
|
|||
|
)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
if self.famille:
|
|||
|
return f'Prestation pour la famille {self.famille} le {self.date_prestation} : {self.duree}'
|
|||
|
return f'Prestation générale le {self.date_prestation} : {self.duree}'
|
|||
|
|
|||
|
def can_edit(self, user):
|
|||
|
return (
|
|||
|
(user == self.auteur or user in self.intervenants.all() or
|
|||
|
user.has_perm('aemo.edit_prest_prev_month')
|
|||
|
) and self.check_date_allowed(user, self.date_prestation)
|
|||
|
)
|
|||
|
|
|||
|
def edit_url(self):
|
|||
|
return reverse('prestation-edit', args=[self.famille.pk if self.famille else 0, self.pk])
|
|||
|
|
|||
|
def save(self, *args, **kwargs):
|
|||
|
if self.famille is None:
|
|||
|
self.familles_actives = Famille.actives(self.date_prestation).count()
|
|||
|
super().save(*args, **kwargs)
|
|||
|
|
|||
|
@classmethod
|
|||
|
def check_date_allowed(cls, user, dt):
|
|||
|
"""Contrôle si la date `dt` est située dans le mois courant + DELAI_SAISIE_SUPPL."""
|
|||
|
today = date.today()
|
|||
|
delai = cls.DELAI_SAISIE_SUPPL
|
|||
|
if user.has_perm('aemo.edit_prest_prev_month'):
|
|||
|
delai += 31
|
|||
|
if today.day <= delai:
|
|||
|
return dt >= (today - timedelta(days=delai + 1)).replace(day=1)
|
|||
|
else:
|
|||
|
return dt.year == today.year and dt.month == today.month
|
|||
|
|
|||
|
@classmethod
|
|||
|
def prestations_historiques(cls, prestations):
|
|||
|
"""
|
|||
|
Renvoie un queryset avec toutes les prestations regroupées par annee/mois
|
|||
|
et le total correspondant.
|
|||
|
"""
|
|||
|
return prestations.annotate(
|
|||
|
annee=ExtractYear('date_prestation'),
|
|||
|
mois=ExtractMonth('date_prestation')
|
|||
|
).values('annee', 'mois').order_by('-annee', '-mois').annotate(total=Sum('duree'))
|
|||
|
|
|||
|
@classmethod
|
|||
|
def temps_total(cls, prestations, tous_utilisateurs=False):
|
|||
|
"""
|
|||
|
Renvoie le temps total des `prestations` (QuerySet) sous forme de chaîne '12:30'.
|
|||
|
Si tous_utilisateurs = True, multiplie le temps de chaque prestation par son
|
|||
|
nombre d'intervenants.
|
|||
|
"""
|
|||
|
if prestations.count() > 0:
|
|||
|
if tous_utilisateurs:
|
|||
|
duree = prestations.annotate(num_util=Count('intervenants')).aggregate(
|
|||
|
total=Sum(F('duree') * F('num_util'), output_field=models.DurationField())
|
|||
|
)['total'] or timedelta()
|
|||
|
else:
|
|||
|
duree = prestations.aggregate(total=Sum('duree'))['total'] or timedelta()
|
|||
|
else:
|
|||
|
duree = timedelta()
|
|||
|
return duree
|
|||
|
|
|||
|
@classmethod
|
|||
|
def temps_total_general(cls, annee, familles=True):
|
|||
|
"""
|
|||
|
Renvoie le temps total des prestations (familiales ou générales selon
|
|||
|
familles) pour l'année civile `annee`.
|
|||
|
"""
|
|||
|
prest_tot = cls.objects.filter(
|
|||
|
famille__isnull=not familles,
|
|||
|
date_prestation__year=annee
|
|||
|
).aggregate(total=Sum('duree'))['total'] or timedelta()
|
|||
|
return prest_tot
|
|||
|
|
|||
|
@classmethod
|
|||
|
def temps_total_general_fam_gen(cls, annee):
|
|||
|
"""
|
|||
|
Renvoie le temps total des prestations familiales ET générales pour l'année civile `annee`.
|
|||
|
"""
|
|||
|
prest_tot = cls.objects.filter(
|
|||
|
date_prestation__year=annee
|
|||
|
).aggregate(total=Sum('duree'))['total'] or timedelta()
|
|||
|
return prest_tot
|
|||
|
|
|||
|
@classmethod
|
|||
|
def temps_totaux_mensuels(cls, annee):
|
|||
|
"""
|
|||
|
Renvoie une liste du total mensuel de toutes les prestations familiales
|
|||
|
(sans prestations générales) pour l'année en cours (de janv. à déc.).
|
|||
|
"""
|
|||
|
data = []
|
|||
|
for month in range(1, 13):
|
|||
|
tot = cls.objects.filter(
|
|||
|
famille__isnull=False,
|
|||
|
date_prestation__year=annee, date_prestation__month=month,
|
|||
|
).aggregate(total=Sum('duree'))['total'] or timedelta()
|
|||
|
data.append(tot)
|
|||
|
return data
|
|||
|
|
|||
|
|
|||
|
class JournalAcces(models.Model):
|
|||
|
"""Journalisation des accès aux familles."""
|
|||
|
famille = models.ForeignKey(Famille, on_delete=models.CASCADE, related_name='+')
|
|||
|
utilisateur = models.ForeignKey(Utilisateur, on_delete=models.CASCADE, related_name='+')
|
|||
|
ordinaire = models.BooleanField(default=True)
|
|||
|
quand = models.DateTimeField(auto_now_add=True)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return f"Accès de «{self.utilisateur}» à la famille «{self.famille}», le {self.quand}"
|
|||
|
|
|||
|
|
|||
|
class Etape:
|
|||
|
"""
|
|||
|
1. Numéro d'ordre pour tri dans la vue
|
|||
|
2. Code
|
|||
|
3. Affichage dans tableau
|
|||
|
4. Nom visible de l'étape
|
|||
|
5. Code de l'étape suivante
|
|||
|
6. Délai jusqu'à l'échéance (en jours)
|
|||
|
7. Code de l'étape précédente
|
|||
|
8. Date obligatoire précédente
|
|||
|
9. Saisie obligatoire ou non (True / False)
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(self, num, code, abrev, nom, suivante, delai, precedente, preced_oblig, oblig):
|
|||
|
self.num = num
|
|||
|
self.code = code
|
|||
|
self.abrev = abrev
|
|||
|
self.nom = nom
|
|||
|
self.suivante = suivante
|
|||
|
self.delai = delai
|
|||
|
self.precedente = precedente
|
|||
|
self.preced_oblig = preced_oblig
|
|||
|
self.oblig = oblig
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return self.nom
|
|||
|
|
|||
|
def __repr__(self):
|
|||
|
return '<Etape - {}/{}>'.format(self.num, self.code)
|
|||
|
|
|||
|
def date(self, suivi):
|
|||
|
return getattr(suivi, 'date_{}'.format(self.code))
|
|||
|
|
|||
|
def delai_depuis(self, suivi, date_base):
|
|||
|
"""Délai de cette étape à partir de la date date_base (en général date précédente étape)."""
|
|||
|
if not date_base:
|
|||
|
return None
|
|||
|
return date_base + timedelta(days=self.delai)
|
|||
|
|
|||
|
def date_nom(self):
|
|||
|
return 'date_{}'.format(self.code)
|
|||
|
|
|||
|
def etape_suivante(self, suivi):
|
|||
|
return Suivi.WORKFLOW[self.suivante] if self.suivante else None
|
|||
|
|
|||
|
def etape_precedente(self):
|
|||
|
return Suivi.WORKFLOW[self.precedente] if self.precedente else None
|
|||
|
|
|||
|
|
|||
|
class EtapeDebutSuivi(Etape):
|
|||
|
def date(self, suivi):
|
|||
|
return suivi.debut_suivi_selon_niveau
|
|||
|
|
|||
|
|
|||
|
class EtapeBilan(Etape):
|
|||
|
delai_standard = 3 * 30 # 3 mois
|
|||
|
delai_niveau3 = 30 # 1 mois
|
|||
|
|
|||
|
def date(self, suivi):
|
|||
|
"""Date du dernier bilan"""
|
|||
|
if self.etape_suivante(suivi).code == 'bilan_suivant':
|
|||
|
return None
|
|||
|
return suivi.date_dernier_bilan()
|
|||
|
|
|||
|
def etape_suivante(self, suivi):
|
|||
|
# Soit bilan suivant, soit rapport
|
|||
|
if suivi.date_dernier_rapport():
|
|||
|
return Suivi.WORKFLOW['resume']
|
|||
|
date_bilan = suivi.date_dernier_bilan()
|
|||
|
prochain_rapport = suivi.date_prochain_rapport()
|
|||
|
if date_bilan and prochain_rapport and (prochain_rapport - date_bilan) < timedelta(days=4 * 30):
|
|||
|
return Suivi.WORKFLOW['resume']
|
|||
|
else:
|
|||
|
return Suivi.WORKFLOW['bilan_suivant']
|
|||
|
|
|||
|
def delai_depuis(self, suivi, date_base):
|
|||
|
"""Délai de cette étape à partir de la date date_base (en général date précédente étape)."""
|
|||
|
if not date_base:
|
|||
|
return None
|
|||
|
base = suivi.date_dernier_bilan() or date_base
|
|||
|
delai = self.delai_niveau3 if suivi.famille.niveau_actuel() == 3 else self.delai_standard
|
|||
|
return base + timedelta(days=delai)
|
|||
|
|
|||
|
|
|||
|
class EtapeResume(Etape):
|
|||
|
def date(self, suivi):
|
|||
|
"""Date du dernier rapport"""
|
|||
|
return suivi.date_dernier_rapport()
|
|||
|
|
|||
|
|
|||
|
class EtapeFin(Etape):
|
|||
|
delai_standard = 365 + 180 # 18 mois
|
|||
|
delai_niveau3 = 270 # 9 mois
|
|||
|
|
|||
|
def delai_depuis(self, suivi, *args):
|
|||
|
if not suivi.date_debut_suivi:
|
|||
|
return None
|
|||
|
debut_suivi = suivi.debut_suivi_selon_niveau
|
|||
|
delai = self.delai_niveau3 if suivi.famille.niveau_actuel() == 3 else self.delai_standard
|
|||
|
return debut_suivi + timedelta(days=delai)
|
|||
|
|
|||
|
|
|||
|
Equipe = namedtuple("Equipe", ['code', 'nom', 'perm'])
|
|||
|
|
|||
|
EQUIPES = [
|
|||
|
Equipe(code='montagnes', nom='Montagnes et V-d-T', perm=None),
|
|||
|
Equipe(code='littoral', nom='Littoral et V-d-R', perm=None),
|
|||
|
# Anciennes équipes, conservées pour anciens dossiers
|
|||
|
Equipe(code='neuch_ville', nom='Neuchâtel-ville (archives)', perm=None),
|
|||
|
Equipe(code='litt_est', nom='Littoral Est (archives)', perm=None),
|
|||
|
Equipe(code='litt_ouest', nom='Littoral Ouest (archives)', perm=None),
|
|||
|
]
|
|||
|
|
|||
|
|
|||
|
class Suivi(models.Model):
|
|||
|
WORKFLOW = OrderedDict([
|
|||
|
('demande', Etape(
|
|||
|
1, 'demande', 'dem', 'Demande déposée', 'debut_evaluation', 0, 'demande', 'demande', True
|
|||
|
)),
|
|||
|
('debut_evaluation', Etape(
|
|||
|
2, 'debut_evaluation', 'deb_eva', "Début de l’évaluation", 'fin_evaluation', 40, 'demande', 'demande', True
|
|||
|
)),
|
|||
|
('fin_evaluation', Etape(
|
|||
|
3, 'fin_evaluation', "fin_eva", "Fin de l’évaluation", 'debut_suivi', 40,
|
|||
|
'debut_evaluation', 'debut_evaluation', True
|
|||
|
)),
|
|||
|
('debut_suivi', EtapeDebutSuivi(
|
|||
|
4, 'debut_suivi', "dsuiv", 'Début du suivi', 'bilan_suivant', 40, 'fin_evaluation', 'fin_evaluation', False
|
|||
|
)),
|
|||
|
('bilan_suivant', EtapeBilan(
|
|||
|
5, 'bilan_suivant', "bil", 'Bilan suivant', 'resume', 90, 'debut_suivi', 'fin_evaluation', False
|
|||
|
)),
|
|||
|
('resume', EtapeResume(
|
|||
|
6, 'resume', 'rés', 'Résumé', 'fin_suivi', 90, 'bilan_suivant', 'fin_evaluation', False
|
|||
|
)),
|
|||
|
('fin_suivi', EtapeFin(
|
|||
|
7, 'fin_suivi', "fsuiv", 'Fin du suivi', 'archivage', 365 + 180, 'resume', 'fin_evaluation', True
|
|||
|
)),
|
|||
|
('archivage', Etape(
|
|||
|
8, 'arch_dossier', 'arch', 'Archivage du dossier', None, 0, 'fin_suivi', 'fin_suivi', False
|
|||
|
)),
|
|||
|
])
|
|||
|
|
|||
|
EQUIPES_CHOICES = [(equ.code, equ.nom) for equ in EQUIPES]
|
|||
|
|
|||
|
famille = models.OneToOneField(Famille, on_delete=models.CASCADE)
|
|||
|
equipe = models.CharField('Équipe', max_length=15, choices=EQUIPES_CHOICES)
|
|||
|
heure_coord = models.BooleanField("Heure de coordination", default=False)
|
|||
|
difficultes = models.TextField("Difficultés", blank=True)
|
|||
|
aides = models.TextField("Aides souhaitées", blank=True)
|
|||
|
competences = models.TextField('Ressources/Compétences', blank=True)
|
|||
|
dates_demande = models.CharField('Dates', max_length=128, blank=True)
|
|||
|
autres_contacts = models.TextField("Autres services contactés", blank=True)
|
|||
|
disponibilites = models.TextField('Disponibilités', blank=True)
|
|||
|
remarque = models.TextField(blank=True)
|
|||
|
remarque_privee = models.TextField('Remarque privée', blank=True)
|
|||
|
service_orienteur = models.CharField(
|
|||
|
"Orienté vers l’AEMO par", max_length=15, choices=choices.SERVICE_ORIENTEUR_CHOICES, blank=True
|
|||
|
)
|
|||
|
service_annonceur = models.CharField('Service annonceur', max_length=60, blank=True)
|
|||
|
motif_demande = ChoiceArrayField(
|
|||
|
models.CharField(max_length=60, choices=choices.MOTIF_DEMANDE_CHOICES),
|
|||
|
verbose_name="Motif de la demande", blank=True, null=True)
|
|||
|
motif_detail = models.TextField('Motif', blank=True)
|
|||
|
|
|||
|
# Référents
|
|||
|
intervenants = models.ManyToManyField(
|
|||
|
Utilisateur, through='Intervenant', related_name='interventions', blank=True
|
|||
|
)
|
|||
|
ope_referent = models.ForeignKey(Contact, blank=True, null=True, related_name='+',
|
|||
|
on_delete=models.SET_NULL, verbose_name='as. OPE')
|
|||
|
ope_referent_2 = models.ForeignKey(
|
|||
|
Contact, blank=True, null=True, related_name='+', on_delete=models.SET_NULL,
|
|||
|
verbose_name='as. OPE 2'
|
|||
|
)
|
|||
|
mandat_ope = ChoiceArrayField(
|
|||
|
models.CharField(max_length=65, choices=choices.MANDATS_OPE_CHOICES, blank=True),
|
|||
|
verbose_name="Mandat OPE", blank=True, null=True,
|
|||
|
)
|
|||
|
sse_referent = models.ForeignKey(Contact, blank=True, null=True, related_name='+',
|
|||
|
on_delete=models.SET_NULL, verbose_name='SSE')
|
|||
|
referent_note = models.TextField('Autres contacts', blank=True)
|
|||
|
collaboration = models.TextField('Collaboration', blank=True)
|
|||
|
ressource = models.TextField('Ressource', blank=True)
|
|||
|
crise = models.TextField('Gestion de crise', blank=True)
|
|||
|
|
|||
|
date_demande = models.DateField("Demande déposée le", blank=True, null=True, default=None)
|
|||
|
date_debut_evaluation = models.DateField("Début de l’évaluation le", blank=True, null=True, default=None)
|
|||
|
date_fin_evaluation = models.DateField("Fin de l’évaluation le", blank=True, null=True, default=None)
|
|||
|
date_debut_suivi = models.DateField("Début du suivi le", blank=True, null=True, default=None)
|
|||
|
date_fin_suivi = models.DateField("Fin du suivi le", blank=True, null=True, default=None)
|
|||
|
|
|||
|
demande_prioritaire = models.BooleanField("Demande prioritaire", default=False)
|
|||
|
demarche = ChoiceArrayField(models.CharField(max_length=60, choices=choices.DEMARCHE_CHOICES, blank=True),
|
|||
|
verbose_name="Démarche", blank=True, null=True)
|
|||
|
|
|||
|
pers_famille_presentes = models.CharField('Membres famille présents', max_length=200, blank=True)
|
|||
|
ref_presents = models.CharField('Intervenants présents', max_length=250, blank=True)
|
|||
|
autres_pers_presentes = models.CharField('Autres pers. présentes', max_length=100, blank=True)
|
|||
|
motif_fin_suivi = models.CharField('Motif de fin de suivi', max_length=20,
|
|||
|
choices=choices.MOTIFS_FIN_SUIVI_CHOICES, blank=True)
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return 'Suivi pour la famille {} '.format(self.famille)
|
|||
|
|
|||
|
@property
|
|||
|
def date_fin_theorique(self):
|
|||
|
if self.date_fin_suivi:
|
|||
|
return self.date_fin_suivi
|
|||
|
if self.date_debut_suivi is None:
|
|||
|
return None
|
|||
|
return self.debut_suivi_selon_niveau + timedelta(days=548) # env. 18 mois
|
|||
|
|
|||
|
@cached_property
|
|||
|
def debut_suivi_selon_niveau(self):
|
|||
|
"""
|
|||
|
Date de début de suivi tenant compte d'éventuel changement de niveau entre 2 et 3.
|
|||
|
"""
|
|||
|
debut = self.date_debut_suivi
|
|||
|
if debut is None:
|
|||
|
return debut
|
|||
|
niv_prec = None
|
|||
|
for niv in sorted(self.famille.niveaux.all(), key=lambda niv: niv.date_debut):
|
|||
|
if (
|
|||
|
(niv_prec in [1, 2] and niv.niveau_interv == 3) or
|
|||
|
(niv_prec == 3 and niv.niveau_interv in [1, 2])
|
|||
|
):
|
|||
|
debut = niv.date_debut
|
|||
|
niv_prec = niv.niveau_interv
|
|||
|
return debut
|
|||
|
|
|||
|
def date_prochain_rapport(self):
|
|||
|
if self.date_debut_suivi is None or self.date_fin_suivi is not None:
|
|||
|
return None
|
|||
|
date_dernier_rapport = self.date_dernier_rapport()
|
|||
|
niveau = self.famille.niveau_actuel()
|
|||
|
if date_dernier_rapport is None:
|
|||
|
# Premier à 9 ou 18 mois, selon niveau
|
|||
|
delai = 30 * 9 if niveau == 3 else 365 + 182
|
|||
|
return self.date_debut_suivi + timedelta(days=delai)
|
|||
|
else:
|
|||
|
# Suivants tous les 9 ou 12 mois, selon niveau
|
|||
|
delai = 30 * 9 if niveau == 3 else 365
|
|||
|
return date_dernier_rapport + timedelta(days=delai)
|
|||
|
|
|||
|
def date_dernier_rapport(self):
|
|||
|
# Using rapports.all() to leverage prefetchs
|
|||
|
debut_suivi = self.debut_suivi_selon_niveau
|
|||
|
return max([
|
|||
|
rapp.date for rapp in self.famille.rapports.all() if rapp.date > debut_suivi
|
|||
|
], default=None)
|
|||
|
|
|||
|
def date_dernier_bilan(self):
|
|||
|
debut_suivi = self.debut_suivi_selon_niveau
|
|||
|
# Using bilans.all() to leverage prefetchs
|
|||
|
return max([
|
|||
|
bilan.date for bilan in self.famille.bilans.all() if bilan.date > debut_suivi
|
|||
|
], default=None)
|
|||
|
|
|||
|
@property
|
|||
|
def ope_referent_display(self):
|
|||
|
return self.ope_referent.nom_prenom if self.ope_referent else '-'
|
|||
|
|
|||
|
@cached_property
|
|||
|
def etape(self):
|
|||
|
"""L’étape *terminée* du suivi."""
|
|||
|
for key, etape in reversed(self.WORKFLOW.items()):
|
|||
|
if key != 'archivage':
|
|||
|
if etape.date(self):
|
|||
|
return etape
|
|||
|
|
|||
|
@cached_property
|
|||
|
def etape_suivante(self):
|
|||
|
return self.etape.etape_suivante(self) if self.etape else None
|
|||
|
|
|||
|
def date_suivante(self):
|
|||
|
etape_date = self.etape.date(self)
|
|||
|
if not self.etape_suivante or not etape_date:
|
|||
|
return None
|
|||
|
return self.etape_suivante.delai_depuis(self, etape_date)
|
|||
|
|
|||
|
def get_mandat_ope_display(self):
|
|||
|
dic = dict(choices.MANDATS_OPE_CHOICES)
|
|||
|
return '; '.join([dic[value] for value in self.mandat_ope]) if self.mandat_ope else '-'
|
|||
|
|
|||
|
def get_motif_demande_display(self):
|
|||
|
dic = dict(choices.MOTIF_DEMANDE_CHOICES)
|
|||
|
return '; '.join([dic[value] for value in self.motif_demande]) if self.motif_demande else ''
|
|||
|
|
|||
|
@property
|
|||
|
def ope_referents(self):
|
|||
|
return [ope for ope in [self.ope_referent, self.ope_referent_2] if ope]
|
|||
|
|
|||
|
|
|||
|
class IntervenantManager(models.Manager):
|
|||
|
def actifs(self, _date=None):
|
|||
|
return self.filter(Q(date_fin__isnull=True) | Q(date_fin__gt=_date or date.today()))
|
|||
|
|
|||
|
|
|||
|
class Intervenant(models.Model):
|
|||
|
"""
|
|||
|
Modèle M2M entre Suivi et Utilisateur (utilisé par through de Suivi.intervenants).
|
|||
|
"""
|
|||
|
suivi = models.ForeignKey(Suivi, on_delete=models.CASCADE)
|
|||
|
intervenant = models.ForeignKey(Utilisateur, on_delete=models.CASCADE)
|
|||
|
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
|||
|
date_debut = models.DateField('Date début', default=timezone.now)
|
|||
|
# date_fin est utilisé pour les interventions s'arrêtant avant la fin du suivi.
|
|||
|
date_fin = models.DateField('Date fin', null=True, blank=True)
|
|||
|
|
|||
|
objects = IntervenantManager()
|
|||
|
|
|||
|
def __str__(self):
|
|||
|
return '{}, {} pour {}'.format(self.intervenant, self.role, self.suivi)
|
|||
|
|
|||
|
def can_edit(self, user):
|
|||
|
return self.suivi.famille.can_edit(user)
|