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: {: {(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']]