910 lines
39 KiB
Python
910 lines
39 KiB
Python
import calendar
|
||
from dataclasses import dataclass
|
||
from datetime import date, timedelta
|
||
from operator import attrgetter
|
||
|
||
from django import forms
|
||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||
from django.db.models import (
|
||
Case, Count, DurationField, ExpressionWrapper, F, IntegerField, OuterRef, Q,
|
||
Subquery, Sum, When
|
||
)
|
||
from django.db.models.functions import Coalesce, TruncMonth
|
||
from django.utils.dates import MONTHS
|
||
from django.views.generic import TemplateView
|
||
|
||
from .forms import DateYearForm
|
||
from .models import Intervenant, LibellePrestation, Niveau, Personne, Prestation, Suivi, Utilisateur
|
||
from .utils import format_d_m_Y, format_duree
|
||
from common.choices import (
|
||
MOTIF_DEMANDE_CHOICES, MOTIFS_FIN_SUIVI_CHOICES, PROVENANCE_DESTINATION_CHOICES,
|
||
SERVICE_ORIENTEUR_CHOICES,
|
||
)
|
||
from .export import ExportStatistique
|
||
|
||
|
||
class DateLimitForm(forms.Form):
|
||
YEAR_CHOICES = tuple(
|
||
(str(y), str(y))
|
||
for y in range(2020, date.today().year + (1 if date.today().month < 12 else 2))
|
||
)
|
||
start_month = forms.ChoiceField(choices=[(str(m), MONTHS[m]) for m in range(1, 13)])
|
||
start_year = forms.ChoiceField(choices=YEAR_CHOICES)
|
||
end_month = forms.ChoiceField(choices=[(str(m), MONTHS[m]) for m in range(1, 13)])
|
||
end_year = forms.ChoiceField(choices=YEAR_CHOICES)
|
||
|
||
def __init__(self, data, **kwargs):
|
||
if not data:
|
||
today = date.today()
|
||
data = {
|
||
'start_year': today.year, 'start_month': today.month,
|
||
'end_year': today.year if today.month < 12 else today.year + 1,
|
||
'end_month': (today.month + 1) if today.month < 12 else 1,
|
||
}
|
||
super().__init__(data, **kwargs)
|
||
|
||
def clean(self):
|
||
cleaned_data = super().clean()
|
||
if not self.errors and self.start > self.end:
|
||
raise forms.ValidationError("Les dates ne sont pas dans l’ordre.")
|
||
return cleaned_data
|
||
|
||
@property
|
||
def start(self):
|
||
return date(int(self.cleaned_data['start_year']), int(self.cleaned_data['start_month']), 1)
|
||
|
||
@property
|
||
def end(self):
|
||
return date(
|
||
int(self.cleaned_data['end_year']),
|
||
int(self.cleaned_data['end_month']),
|
||
calendar.monthrange(int(self.cleaned_data['end_year']), int(self.cleaned_data['end_month']))[1]
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class Month:
|
||
year: int
|
||
month: int
|
||
|
||
def __str__(self):
|
||
return f'{self.month:0>2}.{self.year}'
|
||
|
||
def __lt__(self, other):
|
||
return (self.year, self.month) < (other.year, other.month)
|
||
|
||
def __hash__(self):
|
||
return hash((self.year, self.month))
|
||
|
||
def next(self):
|
||
if self.month == 12:
|
||
return Month(self.year + 1, 1)
|
||
else:
|
||
return Month(self.year, self.month + 1)
|
||
|
||
@classmethod
|
||
def from_date(cls, dt):
|
||
return Month(dt.year, dt.month)
|
||
|
||
def is_future(self):
|
||
return date(self.year, self.month, 1) > date.today()
|
||
|
||
|
||
class StatsMixin:
|
||
permission_required = 'aemo.export_stats'
|
||
|
||
def get_months(self):
|
||
"""Return a list of tuples [(year, month), ...] from date_start to date_end."""
|
||
months = [Month(self.date_start.year, self.date_start.month)]
|
||
while True:
|
||
next_m = months[-1].next()
|
||
if next_m > Month(self.date_end.year, self.date_end.month):
|
||
break
|
||
months.append(next_m)
|
||
return months
|
||
|
||
def month_limits(self, month):
|
||
"""From (2020, 4), return (date(2020, 4, 1), date(2020, 5, 1))."""
|
||
next_m = month.next()
|
||
return (
|
||
date(month.year, month.month, 1),
|
||
date(next_m.year, next_m.month, 1)
|
||
)
|
||
|
||
@staticmethod
|
||
def init_counters(names, months, default=0, total=0):
|
||
"""
|
||
Create stat counters:
|
||
{<counter_name>: {(2020, 3): 0, (2020, 4): 0, …, 'total': 0}, …}
|
||
"""
|
||
counters = {}
|
||
for count_name in names:
|
||
counters[count_name] = {}
|
||
for month in months:
|
||
counters[count_name][month] = '-' if month.is_future() else default
|
||
counters[count_name]['total'] = total
|
||
return counters
|
||
|
||
def get_stats(self, months):
|
||
# Here subclasses produce stats to be merged in view context
|
||
return {}
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
get_params = self.request.GET.copy()
|
||
self.export_flag = get_params.pop('export', None)
|
||
date_form = DateLimitForm(get_params)
|
||
context['date_form'] = date_form
|
||
if not date_form.is_valid():
|
||
return context
|
||
self.date_start = date_form.start
|
||
self.date_end = date_form.end
|
||
months = self.get_months()
|
||
context.update({
|
||
'date_form': date_form,
|
||
'months': months,
|
||
})
|
||
context.update(self.get_stats(months))
|
||
return context
|
||
|
||
def export_as_openxml(self, context):
|
||
export = ExportStatistique(col_widths=[50])
|
||
export.fill_data(self.export_lines(context))
|
||
return export.get_http_response(self.__class__.__name__.replace('View', '') + '.xlsx')
|
||
|
||
def render_to_response(self, context, **response_kwargs):
|
||
if self.export_flag:
|
||
return self.export_as_openxml(context)
|
||
else:
|
||
return super().render_to_response(context, **response_kwargs)
|
||
|
||
|
||
class StatistiquesView(StatsMixin, PermissionRequiredMixin, TemplateView):
|
||
template_name = 'statistiques/statistiques.html'
|
||
labels = {
|
||
'familles_total': 'Total familles (évaluation et/ou accomp.)',
|
||
'enfants_total': 'Total enfants suivis (évaluation et/ou accomp.)',
|
||
'familles_evaluees': 'Familles évaluées',
|
||
'enfants_evalues': 'Enfants évalués',
|
||
'enfants_evalues_non_suivis': 'Enfants non suivis de familles évaluées',
|
||
'familles_eval_sans_suivi': 'Familles évaluées sans aboutir à un suivi',
|
||
'familles_suivies': 'Familles suivies',
|
||
'enfants_suivis': 'Enfants suivis',
|
||
'enfants_suivis_non_suivis': 'Enfants non suivis de familles suivies',
|
||
'familles_accueil': 'dont Familles d’accueil',
|
||
'familles_connues': 'dont Familles déjà suivies',
|
||
'prioritaires': 'Demandes prioritaires',
|
||
'rdv_manques': 'Rendez-vous manqués',
|
||
'duree_attente': 'Durée moyenne entre demande et début de suivi',
|
||
}
|
||
|
||
def suivi_stats(self, model, months):
|
||
suivis_base = model.objects.annotate(
|
||
date_fin=Coalesce('date_fin_suivi', date.today()),
|
||
date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
|
||
).filter(
|
||
date_debut__lte=self.date_end,
|
||
date_fin__gte=self.date_start
|
||
).exclude(motif_fin_suivi='erreur')
|
||
|
||
# Annotations pour Count("Enfant suivi"), Count("Enfant non-suivi"), duree_attente
|
||
suivis = suivis_base.annotate(enf_suivis=Count(Case(
|
||
When(famille__membres__role__nom="Enfant suivi", then=1),
|
||
output_field=IntegerField(),
|
||
)), enf_nonsuivis=Count(Case(
|
||
When(famille__membres__role__nom="Enfant non-suivi", then=1),
|
||
output_field=IntegerField(),
|
||
)), duree_attente=F('date_debut_suivi') - F('date_demande'),
|
||
).select_related('famille')
|
||
|
||
count_keys = [
|
||
'familles_total', 'enfants_total',
|
||
'familles_evaluees', 'enfants_evalues', 'enfants_evalues_non_suivis',
|
||
'familles_eval_sans_suivi', 'familles_suivies', 'enfants_suivis',
|
||
'enfants_suivis_non_suivis', 'familles_accueil', 'familles_connues',
|
||
'prioritaires', 'rdv_manques',
|
||
]
|
||
if not getattr(model, 'demande_prioritaire', False):
|
||
count_keys.remove('prioritaires')
|
||
self.counters = self.init_counters(count_keys, months)
|
||
self.counters['duree_attente'] = {'familles': 0, 'total': timedelta(), 'moyenne': timedelta()}
|
||
count_keys.append('duree_attente')
|
||
for suivi in suivis:
|
||
self.update_counters(suivi, months)
|
||
if self.counters['duree_attente']['familles'] > 0:
|
||
self.counters['duree_attente']['moyenne'] = (
|
||
self.counters['duree_attente']['total'] / self.counters['duree_attente']['familles']
|
||
)
|
||
|
||
# Rendez-vous manqués
|
||
rdv_manques = suivis_base.annotate(
|
||
month=TruncMonth('famille__prestations__date_prestation'),
|
||
).values('month').annotate(
|
||
rdv_manques=Count(
|
||
'famille__prestations',
|
||
filter=Q(**{'famille__prestations__manque': True})
|
||
)
|
||
)
|
||
for line in rdv_manques:
|
||
if not line['month']:
|
||
continue
|
||
month = Month.from_date(line['month'])
|
||
if month not in self.counters['rdv_manques']:
|
||
continue
|
||
self.counters['rdv_manques'][month] = line['rdv_manques']
|
||
self.counters['rdv_manques']['total'] += line['rdv_manques']
|
||
data = {key: self.counters[key] for key in count_keys}
|
||
data['total_familles'] = len(suivis)
|
||
return data
|
||
|
||
def update_counters(self, suivi, months):
|
||
def suivi_entre(date_deb, date_fin):
|
||
return (
|
||
suivi.date_debut_suivi and suivi.date_debut_suivi < date_fin and
|
||
(not suivi.date_fin_suivi or suivi.date_fin_suivi > date_deb)
|
||
)
|
||
|
||
def evaluation_entre(date_deb, date_fin):
|
||
# Dès la demande éventuelle, on considère la famille en cours d'évaluation
|
||
# pour les stats, même sans date de debut d'évaluation
|
||
debut_eval = suivi.date_demande or suivi.date_debut_evaluation
|
||
fin_eval = suivi.date_fin_evaluation or suivi.date_fin_suivi
|
||
return (
|
||
debut_eval and debut_eval < date_fin
|
||
and (not fin_eval or fin_eval > date_deb)
|
||
)
|
||
|
||
# Pour chaque mois:
|
||
for month in months:
|
||
if month.is_future():
|
||
continue
|
||
month_start, month_end = self.month_limits(month)
|
||
month_evalue = evaluation_entre(month_start, month_end)
|
||
month_suivi = suivi_entre(month_start, month_end)
|
||
if month_evalue:
|
||
self.counters['familles_evaluees'][month] += 1
|
||
self.counters['enfants_evalues'][month] += suivi.enf_suivis
|
||
self.counters['enfants_evalues_non_suivis'][month] += suivi.enf_nonsuivis
|
||
if suivi.famille.accueil:
|
||
self.counters['familles_accueil'][month] += 1
|
||
if suivi.famille.connue:
|
||
self.counters['familles_connues'][month] += 1
|
||
if month_suivi:
|
||
self.counters['familles_suivies'][month] += 1
|
||
self.counters['enfants_suivis'][month] += suivi.enf_suivis
|
||
self.counters['enfants_suivis_non_suivis'][month] += suivi.enf_nonsuivis
|
||
if suivi.famille.accueil:
|
||
self.counters['familles_accueil'][month] += 1
|
||
if suivi.famille.connue:
|
||
self.counters['familles_connues'][month] += 1
|
||
if month_evalue or month_suivi:
|
||
if getattr(suivi, 'demande_prioritaire', False):
|
||
self.counters['prioritaires'][month] += 1
|
||
self.counters['prioritaires']['total'] += 1
|
||
self.counters['familles_total'][month] += 1
|
||
self.counters['enfants_total'][month] += suivi.enf_suivis
|
||
|
||
if not suivi.date_debut_suivi and suivi.motif_fin_suivi and (
|
||
suivi.date_fin_suivi >= month_start and suivi.date_fin_suivi < month_end
|
||
):
|
||
self.counters['familles_eval_sans_suivi'][month] += 1
|
||
self.counters['familles_eval_sans_suivi']['total'] += 1
|
||
|
||
# Au total:
|
||
suivi_evalue = evaluation_entre(self.date_start, self.date_end)
|
||
suivi_acc = suivi_entre(self.date_start, self.date_end)
|
||
if suivi_evalue:
|
||
self.counters['familles_evaluees']['total'] += 1
|
||
self.counters['enfants_evalues']['total'] += suivi.enf_suivis
|
||
self.counters['enfants_evalues_non_suivis']['total'] += suivi.enf_nonsuivis
|
||
if suivi.famille.accueil:
|
||
self.counters['familles_accueil']['total'] += 1
|
||
if suivi.famille.connue:
|
||
self.counters['familles_connues']['total'] += 1
|
||
if suivi_acc:
|
||
self.counters['familles_suivies']['total'] += 1
|
||
self.counters['enfants_suivis']['total'] += suivi.enf_suivis
|
||
self.counters['enfants_suivis_non_suivis']['total'] += suivi.enf_nonsuivis
|
||
if suivi.famille.accueil:
|
||
self.counters['familles_accueil']['total'] += 1
|
||
if suivi.famille.connue:
|
||
self.counters['familles_connues']['total'] += 1
|
||
if suivi_evalue or suivi_acc:
|
||
self.counters['familles_total']['total'] += 1
|
||
self.counters['enfants_total']['total'] += suivi.enf_suivis
|
||
if suivi.duree_attente is not None and (
|
||
suivi.date_debut_suivi >= self.date_start and suivi.date_debut_suivi <= self.date_end
|
||
):
|
||
self.counters['duree_attente']['familles'] += 1
|
||
self.counters['duree_attente']['total'] += suivi.duree_attente
|
||
|
||
def get_stats(self, months):
|
||
return {
|
||
'familles': self.suivi_stats(Suivi, months),
|
||
}
|
||
|
||
def export_lines(self, context):
|
||
months = context['months']
|
||
|
||
def stats_for(values):
|
||
for key, vals in values.items():
|
||
if key in ['duree_attente', 'total_familles']:
|
||
continue
|
||
yield [self.labels[key]] + [vals[month] for month in months] + [vals['total']]
|
||
yield (
|
||
[self.labels['duree_attente'] + ' (jours)'] +
|
||
(len(months)) * [''] +
|
||
[values['duree_attente']['moyenne'].days]
|
||
)
|
||
|
||
yield ['BOLD', 'Familles AEMO'] + [str(month) for month in months] + ['Total']
|
||
yield from stats_for(context['familles'])
|
||
|
||
|
||
class StatistiquesParIntervView(StatsMixin, PermissionRequiredMixin, TemplateView):
|
||
"""This view is currently unused (#344). It might be deleted in the future."""
|
||
template_name = 'statistiques/stats-interv.html'
|
||
|
||
def interv_stats(self, model, months):
|
||
intervs = model.objects.annotate(
|
||
date_fin=Coalesce('suivi__date_fin_suivi', date.today()),
|
||
date_debut=Coalesce('suivi__date_debut_evaluation', 'suivi__date_debut_suivi')
|
||
).filter(
|
||
date_debut__lte=self.date_end,
|
||
date_fin__gte=self.date_start
|
||
).exclude(
|
||
suivi__motif_fin_suivi='erreur'
|
||
).annotate(
|
||
enf_suivis=Count(Case(
|
||
When(suivi__famille__membres__role__nom="Enfant suivi", then=1),
|
||
output_field=IntegerField(),
|
||
))
|
||
).select_related('suivi', 'intervenant')
|
||
|
||
counters = {}
|
||
for interv in intervs:
|
||
if interv.intervenant not in counters:
|
||
counters[interv.intervenant] = self.init_counters(['num_familles', 'num_enfants'], months)
|
||
counters[interv.intervenant]['num_familles']['total'] += 1
|
||
counters[interv.intervenant]['num_enfants']['total'] += interv.enf_suivis
|
||
for month in months:
|
||
month_start, month_end = self.month_limits(month)
|
||
if interv.date_debut <= month_end and interv.date_fin >= month_start:
|
||
counters[interv.intervenant]['num_familles'][month] += 1
|
||
counters[interv.intervenant]['num_enfants'][month] += interv.enf_suivis
|
||
return {key: counters[key] for key in sorted(counters.keys(), key=attrgetter('nom'))}
|
||
|
||
def get_stats(self, months):
|
||
return {
|
||
'intervs': self.interv_stats(Intervenant, months),
|
||
}
|
||
|
||
def export_lines(self, context):
|
||
months = context['months']
|
||
|
||
def stats_for(values):
|
||
for key, vals in values.items():
|
||
yield [key.nom_prenom, 'Familles'] + [vals['num_familles'][month] for month in months]
|
||
yield [key.nom_prenom, 'Enfants'] + [vals['num_enfants'][month] for month in months]
|
||
|
||
yield ['BOLD', 'AEMO', ''] + [str(month) for month in months]
|
||
yield from stats_for(context['intervs'])
|
||
|
||
|
||
class StatistiquesParLocaliteView(StatsMixin, PermissionRequiredMixin, TemplateView):
|
||
template_name = 'statistiques/stats-localite.html'
|
||
|
||
def localite_query(self, model):
|
||
return model.objects.annotate(
|
||
date_fin=Coalesce('date_fin_suivi', date.today()),
|
||
date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
|
||
).filter(
|
||
date_debut__lte=self.date_end,
|
||
date_fin__gte=self.date_start
|
||
).exclude(
|
||
motif_fin_suivi='erreur'
|
||
).annotate(
|
||
enf_suivis=Count(Case(
|
||
When(famille__membres__role__nom="Enfant suivi", then=1),
|
||
output_field=IntegerField(),
|
||
))
|
||
).values('date_debut', 'date_fin', 'famille__npa', 'famille__localite', 'enf_suivis')
|
||
|
||
def localite_stats(self, model, months):
|
||
suivis = self.localite_query(model)
|
||
counters = self.init_counters(['totals'], months)
|
||
for suivi in suivis:
|
||
loc_key = f"{suivi['famille__npa']} {suivi['famille__localite']}"
|
||
if loc_key not in counters:
|
||
counters.update(self.init_counters([loc_key], months))
|
||
counters[loc_key]['total'] += suivi['enf_suivis']
|
||
counters['totals']['total'] += suivi['enf_suivis']
|
||
for month in months:
|
||
month_start, month_end = self.month_limits(month)
|
||
if suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start:
|
||
counters[loc_key][month] += suivi['enf_suivis']
|
||
counters['totals'][month] += suivi['enf_suivis']
|
||
return {localite: counters[localite] for localite in sorted(counters.keys())}
|
||
|
||
def get_stats(self, months):
|
||
return {
|
||
'localites': self.localite_stats(Suivi, months),
|
||
}
|
||
|
||
def export_lines(self, context):
|
||
months = context['months']
|
||
|
||
def stats_for(values):
|
||
for key, vals in values.items():
|
||
yield ['Totaux' if key == 'totals' else key] + [vals[month] for month in months] + [vals['total']]
|
||
|
||
yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
|
||
yield from stats_for(context['localites'])
|
||
|
||
|
||
class StatistiquesParRegionView(StatsMixin, PermissionRequiredMixin, TemplateView):
|
||
template_name = 'statistiques/stats-region.html'
|
||
|
||
def region_query(self, model, region_key):
|
||
return model.objects.annotate(
|
||
date_fin=Coalesce('date_fin_suivi', date.today()),
|
||
date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
|
||
).filter(
|
||
date_debut__lte=self.date_end,
|
||
date_fin__gte=self.date_start
|
||
).exclude(
|
||
motif_fin_suivi='erreur'
|
||
).annotate(
|
||
enf_suivis=Count(Case(
|
||
When(famille__membres__role__nom="Enfant suivi", then=1),
|
||
output_field=IntegerField(),
|
||
))
|
||
).values('date_debut', 'date_fin', region_key, 'enf_suivis')
|
||
|
||
def region_stats(self, model, months, region_key):
|
||
suivis = self.region_query(model, region_key)
|
||
counters = self.init_counters(['totals'], months)
|
||
for suivi in suivis:
|
||
loc_key = suivi[region_key] or '?'
|
||
if loc_key not in counters:
|
||
counters.update(self.init_counters([loc_key], months))
|
||
counters[loc_key]['total'] += suivi['enf_suivis']
|
||
counters['totals']['total'] += suivi['enf_suivis']
|
||
for month in months:
|
||
month_start, month_end = self.month_limits(month)
|
||
if suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start:
|
||
counters[loc_key][month] += suivi['enf_suivis']
|
||
counters['totals'][month] += suivi['enf_suivis']
|
||
return {region: counters[region] for region in sorted(counters.keys())}
|
||
|
||
def get_stats(self, months):
|
||
return {
|
||
'regions': self.region_stats(Suivi, months, 'famille__region__nom'),
|
||
}
|
||
|
||
def export_lines(self, context):
|
||
months = context['months']
|
||
|
||
def stats_for(values):
|
||
for key, vals in values.items():
|
||
yield (
|
||
['Totaux' if key == 'totals' else key] +
|
||
[vals[month] for month in months] +
|
||
[vals['total']]
|
||
)
|
||
|
||
yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
|
||
yield from stats_for(context['regions'])
|
||
|
||
|
||
class StatistiquesParAgeView(StatsMixin, PermissionRequiredMixin, TemplateView):
|
||
template_name = 'statistiques/stats-age.html'
|
||
|
||
def age_stats(self, model, months):
|
||
enfants_suivis = Personne.objects.filter(role__nom='Enfant suivi')
|
||
enfants_suivis = enfants_suivis.exclude(
|
||
famille__suivi__motif_fin_suivi='erreur'
|
||
).annotate(
|
||
date_fin=Coalesce(
|
||
'famille__suivi__date_fin_suivi', date.today()
|
||
),
|
||
date_debut=Coalesce(
|
||
'famille__suivi__date_demande',
|
||
'famille__suivi__date_debut_evaluation', 'famille__suivi__date_debut_suivi'
|
||
)
|
||
)
|
||
enfants_suivis = enfants_suivis.filter(
|
||
date_debut__lte=self.date_end,
|
||
date_fin__gte=self.date_start
|
||
)
|
||
|
||
counters = {}
|
||
means = {'total': [], **{m: [] for m in months}}
|
||
for enfant in enfants_suivis:
|
||
age = enfant.age_a(enfant.date_debut + ((enfant.date_fin - enfant.date_debut) / 2))
|
||
if age is None:
|
||
continue
|
||
age = age_real = int(age)
|
||
if age > 18:
|
||
age = 18
|
||
if age not in counters:
|
||
counters.update(self.init_counters([age], months))
|
||
counters[age]['total'] += 1
|
||
means['total'].append(age_real)
|
||
for month in months:
|
||
month_start, month_end = self.month_limits(month)
|
||
if enfant.date_debut <= month_end and enfant.date_fin >= month_start:
|
||
counters[age][month] += 1
|
||
means[month].append(age_real)
|
||
stats = {str(age): counters[age] for age in sorted(counters.keys())}
|
||
if '18' in stats:
|
||
stats['18 et plus'] = stats['18']
|
||
del stats['18']
|
||
# Calcul des moyennes à 1 décimale
|
||
means['total'] = int(sum(means['total']) / max(len(means['total']), 1) * 10) / 10
|
||
for month in months:
|
||
if month.is_future():
|
||
means[month] = '-'
|
||
else:
|
||
means[month] = int(sum(means[month]) / max(len(means[month]), 1) * 10) / 10
|
||
return stats, means
|
||
|
||
def get_stats(self, months):
|
||
stats, means = self.age_stats(Suivi, months)
|
||
return {
|
||
'ages': stats,
|
||
'means': means,
|
||
}
|
||
|
||
def export_lines(self, context):
|
||
months = context['months']
|
||
|
||
def stats_for(values):
|
||
for key, vals in values.items():
|
||
yield ['Totaux' if key == 'totals' else key] + [vals[month] for month in months] + [vals['total']]
|
||
|
||
yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
|
||
yield from stats_for(context['ages'])
|
||
yield ['Âge moyen'] + [context['means'][month] for month in months] + [context['means']['total']]
|
||
|
||
|
||
class StatistiquesParDureeView(StatsMixin, PermissionRequiredMixin, TemplateView):
|
||
template_name = 'statistiques/stats-duree.html'
|
||
|
||
slices = {
|
||
Suivi: [
|
||
{'label': '0 à 3 mois', 'start': 0, 'stop': 120},
|
||
{'label': '4 à 6 mois', 'start': 121, 'stop': 210},
|
||
{'label': '7 à 9 mois', 'start': 211, 'stop': 300},
|
||
{'label': '10 à 12 mois', 'start': 301, 'stop': 394},
|
||
{'label': '13 à 15 mois', 'start': 395, 'stop': 484},
|
||
{'label': '16 à 18 mois', 'start': 485, 'stop': 574},
|
||
{'label': '19 à 24 mois', 'start': 575, 'stop': 760},
|
||
{'label': '25 à 36 mois', 'start': 761, 'stop': 1125},
|
||
{'label': '37 à 48 mois', 'start': 1126, 'stop': 1490},
|
||
{'label': '49 mois et plus', 'start': 1491, 'stop': 100000},
|
||
],
|
||
}
|
||
|
||
def duree_stats(self, model, months):
|
||
suivis = model.objects.filter(
|
||
date_debut_suivi__isnull=False,
|
||
date_fin_suivi__lte=self.date_end,
|
||
date_fin_suivi__gte=self.date_start
|
||
).exclude(
|
||
motif_fin_suivi='erreur'
|
||
).annotate(
|
||
duree_suivi=F('date_fin_suivi') - F('date_debut_suivi')
|
||
)
|
||
counters = {sl['label']: 0 for sl in self.slices[model]}
|
||
for suivi in suivis:
|
||
duree_days = suivi.duree_suivi.days
|
||
for sl in self.slices[model]:
|
||
if sl['stop'] > duree_days > sl['start']:
|
||
counters[sl['label']] += 1
|
||
break
|
||
return counters
|
||
|
||
def get_stats(self, months):
|
||
return {
|
||
'slices': self.slices[Suivi],
|
||
'durees': self.duree_stats(Suivi, months),
|
||
}
|
||
|
||
def export_lines(self, context):
|
||
def stats_for(values, slices):
|
||
for _slice in slices:
|
||
yield [_slice['label'], values[_slice['label']]]
|
||
|
||
yield ['BOLD', f'AEMO, du {format_d_m_Y(self.date_start)} au {format_d_m_Y(self.date_end)}']
|
||
yield ['BOLD', 'Durée', 'Nombre de suivis']
|
||
yield from stats_for(context['durees'], self.slices[Suivi])
|
||
|
||
|
||
class StatistiquesMotifsView(StatsMixin, PermissionRequiredMixin, TemplateView):
|
||
template_name = 'statistiques/stats-motifs.html'
|
||
labels = {
|
||
'ann': 'Motif d’annonce',
|
||
'orient': 'Service annonceur',
|
||
'fin_preeval': 'Abandon avant évaluation',
|
||
'fin_posteval': 'Abandon après évaluation',
|
||
'fin_postacc': 'Fin de l’accompagnement',
|
||
'fin_total': 'Total',
|
||
'prov': 'Provenance',
|
||
'dest': 'Destination',
|
||
}
|
||
|
||
def motifs_stats(self, model, months):
|
||
suivis = model.objects.annotate(
|
||
date_fin=Coalesce('date_fin_suivi', date.today()),
|
||
date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
|
||
).filter(
|
||
date_debut__lte=self.date_end,
|
||
date_fin__gte=self.date_start
|
||
).exclude(
|
||
motif_fin_suivi='erreur'
|
||
).values(
|
||
'famille_id', 'date_debut', 'date_fin', 'date_debut_evaluation', 'date_debut_suivi',
|
||
'motif_demande', 'motif_fin_suivi',
|
||
'service_orienteur', 'famille__provenance', 'famille__destination',
|
||
)
|
||
|
||
# Initiate all counters to 0 for each month.
|
||
counters = {
|
||
'dem': {}, 'orient': {}, 'prov': {}, 'dest': {},
|
||
'fin': {'preeval': {}, 'posteval': {}, 'postacc': {}, 'total': {}}
|
||
}
|
||
for key, choices in (
|
||
('dem', MOTIF_DEMANDE_CHOICES), ('fin', MOTIFS_FIN_SUIVI_CHOICES),
|
||
('orient', SERVICE_ORIENTEUR_CHOICES), ('prov', PROVENANCE_DESTINATION_CHOICES),
|
||
('dest', PROVENANCE_DESTINATION_CHOICES)):
|
||
for ch in choices:
|
||
if key == 'fin':
|
||
if ch[0] == 'erreur':
|
||
continue
|
||
for subkey in counters['fin'].keys():
|
||
counters[key][subkey].update(self.init_counters([ch[0]], months))
|
||
else:
|
||
counters[key].update(self.init_counters([ch[0]], months))
|
||
counters['orient'].update(self.init_counters(['undefined'], months))
|
||
|
||
def suivi_in_month(suivi, month):
|
||
month_start, month_end = self.month_limits(month)
|
||
return suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start
|
||
|
||
for suivi in suivis:
|
||
# Stats motif demande
|
||
for motif in (suivi['motif_demande'] or []):
|
||
counters['dem'][motif]['total'] += 1
|
||
for month in months:
|
||
if suivi_in_month(suivi, month):
|
||
for motif in (suivi['motif_demande'] or []):
|
||
counters['dem'][motif][month] += 1
|
||
# Stats service annonceur
|
||
counters['orient'][suivi['service_orienteur'] or 'undefined']['total'] += 1
|
||
for month in months:
|
||
if suivi_in_month(suivi, month):
|
||
counters['orient'][suivi['service_orienteur'] or 'undefined'][month] += 1
|
||
# Stats motif fin de suivi
|
||
if suivi['motif_fin_suivi'] and suivi['motif_fin_suivi'] != 'erreur':
|
||
counters['fin']['total'][suivi['motif_fin_suivi']]['total'] += 1
|
||
if not suivi['date_debut_evaluation'] and not suivi['date_debut_suivi']:
|
||
counters['fin']['preeval'][suivi['motif_fin_suivi']]['total'] += 1
|
||
elif not suivi['date_debut_suivi']:
|
||
counters['fin']['posteval'][suivi['motif_fin_suivi']]['total'] += 1
|
||
else:
|
||
counters['fin']['postacc'][suivi['motif_fin_suivi']]['total'] += 1
|
||
for month in months:
|
||
if suivi_in_month(suivi, month):
|
||
counters['fin']['total'][suivi['motif_fin_suivi']][month] += 1
|
||
if not suivi['date_debut_evaluation'] and not suivi['date_debut_suivi']:
|
||
counters['fin']['preeval'][suivi['motif_fin_suivi']][month] += 1
|
||
elif not suivi['date_debut_suivi']:
|
||
counters['fin']['posteval'][suivi['motif_fin_suivi']][month] += 1
|
||
else:
|
||
counters['fin']['postacc'][suivi['motif_fin_suivi']][month] += 1
|
||
# Stats provenance
|
||
if suivi['famille__provenance']:
|
||
counters['prov'][suivi['famille__provenance']]['total'] += 1
|
||
for month in months:
|
||
if suivi_in_month(suivi, month):
|
||
counters['prov'][suivi['famille__provenance']][month] += 1
|
||
# Stats destination
|
||
if suivi['famille__destination']:
|
||
counters['dest'][suivi['famille__destination']]['total'] += 1
|
||
for month in months:
|
||
if suivi_in_month(suivi, month):
|
||
counters['dest'][suivi['famille__destination']][month] += 1
|
||
return counters
|
||
|
||
def get_stats(self, months):
|
||
motif_ann_dict = dict(MOTIF_DEMANDE_CHOICES)
|
||
annonceur_dict = dict(SERVICE_ORIENTEUR_CHOICES)
|
||
motif_fin_dict = dict(MOTIFS_FIN_SUIVI_CHOICES)
|
||
provdest_dict = dict(PROVENANCE_DESTINATION_CHOICES)
|
||
stats = self.motifs_stats(Suivi, months)
|
||
return {
|
||
'data': {
|
||
'ann': {motif_ann_dict[key]: data for key, data in stats['dem'].items()},
|
||
'orient': {annonceur_dict.get(key, 'Non défini'): data for key, data in stats['orient'].items()},
|
||
'fin_preeval': {motif_fin_dict[key]: data for key, data in stats['fin']['preeval'].items()},
|
||
'fin_posteval': {motif_fin_dict[key]: data for key, data in stats['fin']['posteval'].items()},
|
||
'fin_postacc': {motif_fin_dict[key]: data for key, data in stats['fin']['postacc'].items()},
|
||
'fin_total': {motif_fin_dict[key]: data for key, data in stats['fin']['total'].items()},
|
||
'prov': {provdest_dict[key]: data for key, data in stats['prov'].items()},
|
||
'dest': {provdest_dict[key]: data for key, data in stats['dest'].items()},
|
||
}
|
||
}
|
||
|
||
def export_lines(self, context):
|
||
months = context['months']
|
||
|
||
def stats_for(values):
|
||
for key1, vals in values.items():
|
||
yield ['BOLD', self.labels[key1]]
|
||
for key2, subvals in vals.items():
|
||
yield [key2] + [subvals[month] for month in months] + [subvals['total']]
|
||
|
||
yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
|
||
yield from stats_for(context['data'])
|
||
|
||
|
||
class StatistiquesPrestationView(PermissionRequiredMixin, TemplateView):
|
||
permission_required = 'aemo.export_stats'
|
||
template_name = 'statistiques/stats-prestations.html'
|
||
|
||
def _sum_list(self, liste):
|
||
tot = timedelta()
|
||
for data in liste:
|
||
if data != '-':
|
||
tot += data
|
||
return tot
|
||
|
||
@staticmethod
|
||
def temps_totaux_mensuels_fam_gen(prest_model, annee):
|
||
"""
|
||
Renvoie un dictionnaire avec les totaux mensuels de toutes les prestations familiales
|
||
et générales pour l'année en cours (de janv. à déc.).
|
||
"""
|
||
query = prest_model.objects.filter(
|
||
date_prestation__year=annee
|
||
).annotate(
|
||
month=TruncMonth('date_prestation'),
|
||
duree_tot=ExpressionWrapper(Count('intervenants') * F('duree'), output_field=DurationField())
|
||
).values('month', 'lib_prestation__nom', 'duree_tot')
|
||
# La somme est calculée en Python, car Django ne sait pas faire la somme de duree_tot.
|
||
months = [Month(annee, num_month) for num_month in range(1, 13)]
|
||
data = {month: {'total': '-' if month.is_future() else timedelta(0)} for month in months}
|
||
for result in query:
|
||
month = Month.from_date(result['month'])
|
||
if result['lib_prestation__nom'] not in data[month]:
|
||
data[month][result['lib_prestation__nom']] = '-' if month.is_future() else timedelta(0)
|
||
data[month][result['lib_prestation__nom']] += result['duree_tot']
|
||
data[month]['total'] += result['duree_tot']
|
||
return data
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
get_params = self.request.GET.copy()
|
||
self.export_flag = get_params.pop('export', None)
|
||
date_form = DateYearForm(get_params)
|
||
context['date_form'] = date_form
|
||
if not date_form.is_valid():
|
||
return context
|
||
if date_form.is_valid():
|
||
annee = int(date_form.cleaned_data['year'])
|
||
counters = {}
|
||
tot_dus_mensuels = [timedelta()] * 12
|
||
tot_ecarts_mensuels = [timedelta()] * 12
|
||
|
||
intervenants = Utilisateur.objects.annotate(
|
||
num_prest=Count('prestations', filter=Q(prestations__date_prestation__year=annee))
|
||
).filter(num_prest__gt=0).order_by('nom', 'prenom')
|
||
for interv in intervenants:
|
||
h_prestees = interv.totaux_mensuels('aemo', annee)
|
||
h_prestees = [('-' if Month(annee, idx).is_future() else h) for idx, h in enumerate(h_prestees, start=1)]
|
||
|
||
tot_prestees = self._sum_list(h_prestees)
|
||
counters[interv] = {
|
||
'heures_prestees': h_prestees,
|
||
'tot_prestees': tot_prestees,
|
||
}
|
||
|
||
tot_prest_mensuels = self.temps_totaux_mensuels_fam_gen(Prestation, annee)
|
||
|
||
tot_dus_mensuels.append(self._sum_list(tot_dus_mensuels))
|
||
tot_ecarts_mensuels.append(self._sum_list(tot_ecarts_mensuels))
|
||
|
||
totaux_par_prest = {}
|
||
for month, data in tot_prest_mensuels.items():
|
||
for label, duration in data.items():
|
||
if label not in totaux_par_prest:
|
||
totaux_par_prest[label] = timedelta(0)
|
||
if duration != '-':
|
||
totaux_par_prest[label] += duration
|
||
|
||
context.update({
|
||
'annee': annee,
|
||
'titre': 'Prestations AEMO',
|
||
'intervenants': counters,
|
||
'months': [date(annee, m, 1) for m in range(1, 13)],
|
||
'libelles_prest': LibellePrestation.objects.all().order_by('code'),
|
||
'totaux_prest_mensuels': tot_prest_mensuels,
|
||
'totaux_par_prest': totaux_par_prest,
|
||
'total_gen': totaux_par_prest['total'],
|
||
})
|
||
return context
|
||
|
||
def render_to_response(self, context, **response_kwargs):
|
||
if self.export_flag:
|
||
export = ExportStatistique(col_widths=[30])
|
||
export.fill_data(self.export_lines(context))
|
||
return export.get_http_response(self.__class__.__name__.replace('View', '') + '.xlsx')
|
||
else:
|
||
return super().render_to_response(context, **response_kwargs)
|
||
|
||
def export_lines(self, context):
|
||
months = context['months']
|
||
yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
|
||
for user, vals in context['intervenants'].items():
|
||
yield ([user.nom_prenom] + [format_duree(val) for val in vals['heures_prestees']]
|
||
+ [format_duree(vals['tot_prestees'])])
|
||
yield ['BOLD', 'Par type d’intervention']
|
||
for prest in context['libelles_prest']:
|
||
yield ([prest.nom] + [
|
||
format_duree(context['totaux_prest_mensuels'][month].get(prest.nom,
|
||
'-' if month.is_future() else timedelta(0)))
|
||
for month in [Month.from_date(m) for m in months]
|
||
] + [format_duree(context['totaux_par_prest'].get(prest.nom, timedelta(0)))])
|
||
|
||
|
||
class StatistiquesNiveauxView(StatsMixin, PermissionRequiredMixin, TemplateView):
|
||
permission_required = 'aemo.export_stats'
|
||
template_name = 'statistiques/stats-niveaux.html'
|
||
|
||
def get_stats(self, months):
|
||
prest_list = ['aemo04', 'aemo05', 'aemo06', 'aemo07']
|
||
prest_query = Prestation.objects.filter(
|
||
famille__isnull=False,
|
||
date_prestation__range=(self.date_start, self.date_end),
|
||
lib_prestation__code__in=prest_list,
|
||
).annotate(
|
||
month=TruncMonth('date_prestation'),
|
||
niveau_interv=Subquery(
|
||
Niveau.objects.filter(
|
||
Q(famille=OuterRef('famille_id')) &
|
||
Q(date_debut__lt=OuterRef('date_prestation')) & (
|
||
Q(date_fin__isnull=True) | Q(date_fin__gt=OuterRef('date_prestation'))
|
||
)
|
||
).order_by('-date_debut').values('niveau_interv')[:1]
|
||
),
|
||
).values(
|
||
'month', 'niveau_interv', 'lib_prestation__code',
|
||
).annotate(
|
||
sum_hours=Sum('duree'),
|
||
)
|
||
niveaux = {
|
||
niv: {
|
||
p: {
|
||
**{m: timedelta(0) for m in months}, 'total': timedelta(0)
|
||
} for p in prest_list
|
||
} for niv in [0, 1, 2, 3]
|
||
}
|
||
for line in prest_query:
|
||
if line['niveau_interv'] is None:
|
||
continue
|
||
niveaux[line['niveau_interv']][line['lib_prestation__code']][Month.from_date(line['month'])] = line['sum_hours']
|
||
niveaux[line['niveau_interv']][line['lib_prestation__code']]['total'] += line['sum_hours']
|
||
return {
|
||
'stats': niveaux,
|
||
'prest_map': LibellePrestation.objects.in_bulk(prest_list, field_name='code'),
|
||
}
|
||
|
||
def export_lines(self, context):
|
||
months = context['months']
|
||
yield ['BOLD', 'Ressources par niveau'] + [str(month) for month in months] + ['Total']
|
||
for niv, prests in context['stats'].items():
|
||
yield ['BOLD', niv]
|
||
for prest, numbers in prests.items():
|
||
yield [context['prest_map'][prest].nom] + [numbers[m] for m in months] + [numbers['total']]
|