aemo_fr/aemo/views_stats.py

910 lines
39 KiB
Python
Raw Normal View History

2024-06-03 16:49:01 +02:00
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 lordre.")
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 daccueil',
'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 dannonce',
'orient': 'Service annonceur',
'fin_preeval': 'Abandon avant évaluation',
'fin_posteval': 'Abandon après évaluation',
'fin_postacc': 'Fin de laccompagnement',
'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 dintervention']
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']]