diff --git a/sigi/apps/eventos/admin_urls.py b/sigi/apps/eventos/admin_urls.py index 6496b3b..859488b 100644 --- a/sigi/apps/eventos/admin_urls.py +++ b/sigi/apps/eventos/admin_urls.py @@ -6,4 +6,5 @@ urlpatterns = [ path( "alocacaoequipe/", views.alocacao_equipe, name="eventos_alocacaoequipe" ), + path("eventosporuf/", views.eventos_por_uf, name="eventos_eventosporuf"), ] diff --git a/sigi/apps/eventos/forms.py b/sigi/apps/eventos/forms.py index c0f22ab..f84f16b 100644 --- a/sigi/apps/eventos/forms.py +++ b/sigi/apps/eventos/forms.py @@ -5,10 +5,18 @@ from django.core.files.base import File from django.db.models.base import Model from django.forms.utils import ErrorList from django.utils.translation import gettext as _ -from material.admin.widgets import MaterialAdminTextareaWidget +from material.admin.widgets import ( + MaterialAdminTextareaWidget, + MaterialAdminDateWidget, +) from sigi.apps.casas.models import Funcionario, Orgao from sigi.apps.espacos.models import Espaco, Reserva -from sigi.apps.eventos.models import Convite, ModeloDeclaracao, Evento +from sigi.apps.eventos.models import ( + Convite, + ModeloDeclaracao, + Evento, + TipoEvento, +) from sigi.apps.parlamentares.models import Parlamentar @@ -115,6 +123,46 @@ class SelecionaModeloForm(forms.Form): ) +class EventosPorUfForm(forms.Form): + MODO_CHOICES = ( + ("V", _("Virtual")), + ("P", _("Presencial")), + ) + data_inicio = forms.DateField( + required=True, + label=_("data de início"), + widget=MaterialAdminDateWidget, + ) + data_fim = forms.DateField( + required=True, + label=_("data de término"), + widget=MaterialAdminDateWidget, + ) + categoria = forms.MultipleChoiceField( + required=False, + label=_("Categoria"), + choices=TipoEvento.CATEGORIA_CHOICES, + widget=forms.CheckboxSelectMultiple, + ) + virtual = forms.MultipleChoiceField( + required=False, + label=_("Modo"), + choices=MODO_CHOICES, + widget=forms.CheckboxSelectMultiple, + ) + + class Media: + css = {"all": ["css/change_form.css"]} + js = [ + "admin/js/vendor/select2/select2.full.js", + "admin/js/change_form.js", + "admin/js/vendor/select2/i18n/pt-BR.js", + "material/admin/js/widgets/TimeInput.js", + "admin/js/core.js", + "/admin/jsi18n/", + ] + + class ConviteForm(forms.ModelForm): class Meta: model = Convite diff --git a/sigi/apps/eventos/templates/eventos/eventos_por_uf.html b/sigi/apps/eventos/templates/eventos/eventos_por_uf.html new file mode 100644 index 0000000..9482cef --- /dev/null +++ b/sigi/apps/eventos/templates/eventos/eventos_por_uf.html @@ -0,0 +1,93 @@ +{% extends "admin/base_site.html" %} +{% load static i18n %} + +{% block extrastyle %} + {{ block.super }} + +{% endblock %} + +{% block extrahead %} + {{ block.super }} +{% endblock %} + +{% block coltype %}colMS{% endblock %} + +{% block content_title %} +
{% trans 'Eventos por Unidade da Federação' %}
+ {% if data_inicio %} +
+ {% blocktranslate with inicio=data_inicio|date:"SHORT_DATE_FORMAT" fim=data_fim|date:"SHORT_DATE_FORMAT" %} + Período: {{ inicio }} a {{ fim }} + {% endblocktranslate %} +
+ {% endif %} +{% endblock %} + +{% block breadcrumbs %}{% endblock %} + +{% block content %} +
+
+
+
+
+ {{ form }} +
+
+ + {% if not pivo_uf is None %} +
+ + print + +
    +
  • +
  • +
+
+ {% endif %} +
+
+
+
+
+ {% if not pivo_uf is None %} + {% include "eventos/snippets/eventos_por_uf_snippet.html" with mode="html" %} + {% endif %} +{% endblock %} + +{% block footer %} + {{ block.super }} + {{ form.media }} + +{% endblock %} diff --git a/sigi/apps/eventos/templates/eventos/eventos_por_uf_pdf.html b/sigi/apps/eventos/templates/eventos/eventos_por_uf_pdf.html new file mode 100644 index 0000000..4207ad2 --- /dev/null +++ b/sigi/apps/eventos/templates/eventos/eventos_por_uf_pdf.html @@ -0,0 +1,48 @@ +{% extends "pdf/base_report.html" %} +{% load static %} +{% load i18n %} + +{% block page_size %}A4 landscape{% endblock page_size %} +{% block extra_style %} + {{ block.super }} + a { + color: black; + text-decoration: none; + } + h4 { + padding: 24px 0 24px 0; + line-height: 1.5em; + } + .row { + margin-bottom: 12px; + } + .card-title { + font-size: 1.2em; + margin: 20px 4px; + padding-left: 1.5rem; + border-left: 5px solid #ee6e73; + } + .sep_regiao { + text-align: center; + text-transform: uppercase; + } + .center { + text-align: center; + } + .numero { + text-align: right; + } +{% endblock %} + + +{% block main_content %} +

+ {% blocktranslate with inicio=data_inicio|date:"SHORT_DATE_FORMAT" fim=data_fim|date:"SHORT_DATE_FORMAT" %} + Período: {{ inicio }} a {{ fim }} + {% endblocktranslate %} +
+ {% trans 'Categoria(s)' %}: {{ categorias|join:", " }}
+ {% trans 'Modo(s)' %}: {{ virtual|join:", " }}
+

+ {% include "eventos/snippets/eventos_por_uf_snippet.html" %} +{% endblock %} diff --git a/sigi/apps/eventos/templates/eventos/snippets/eventos_por_uf_snippet.html b/sigi/apps/eventos/templates/eventos/snippets/eventos_por_uf_snippet.html new file mode 100644 index 0000000..dd31d9f --- /dev/null +++ b/sigi/apps/eventos/templates/eventos/snippets/eventos_por_uf_snippet.html @@ -0,0 +1,105 @@ +{% load i18n %} +
+
+
+
+ {% trans 'Resumo por Região' %} +
+ + + + + {% for label, items in cabecalho_regiao %} + + {% endfor %} + + + {% for top, items in cabecalho_regiao %} + {% for label in items %} + + {% endfor %} + {% endfor %} + + + + {% for datarow in pivo_regiao.itertuples %} + + {% for datacol in datarow %} + {% if forloop.first %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + + + {% for total in total_regiao %} + + {% endfor %} + + +
{% trans 'Região' %}{{ label|capfirst }}
{{ label }}
{{ datacol }}{{ datacol|default:"-" }}
{% trans 'Sumário' %}{{ total }}
+
+
+
+
+
+ +
+
+
+
+ {% trans 'Resumo por Unidade da Federação' %} +
+ + + + + {% for label, items in cabecalho_uf %} + + {% endfor %} + + + {% for top, items in cabecalho_uf %} + {% for label in items %} + + {% endfor %} + {% endfor %} + + + + {% for datarow in pivo_uf.itertuples %} + {% ifchanged datarow.Index.0 %} + + {% with l1=cabecalho_uf.0.1|length l2=cabecalho_uf.1.1|length l3=cabecalho_uf.2.1|length l4=cabecalho_uf.3.1|length %} + + {% endwith %} + + + {% endifchanged %} + + {% for datacol in datarow %} + {% if forloop.first %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + + + {% for total in total_uf %} + + {% endfor %} + + +
{% trans 'Unidade Federativa' %}{{ label|capfirst }}
{{ label }}
{{ datarow.Index.0 }}
{{ datacol.1 }}{{ datacol|default:"-" }}
{% trans 'Sumário' %}{{ total }}
+
+
+
+
+
+ diff --git a/sigi/apps/eventos/views.py b/sigi/apps/eventos/views.py index c43e733..73375a0 100644 --- a/sigi/apps/eventos/views.py +++ b/sigi/apps/eventos/views.py @@ -1,7 +1,10 @@ import calendar import csv +import datetime import locale +import pandas as pd from functools import reduce +from itertools import groupby from rest_framework import mixins, generics from typing import OrderedDict from django import forms @@ -9,6 +12,7 @@ from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator +from django.db.models import Count, Sum, Q from django.http import HttpResponse from django.shortcuts import redirect, render, get_object_or_404 from django.template import Template, Context @@ -27,11 +31,13 @@ from django_weasyprint.utils import django_url_fetcher from django_weasyprint.views import WeasyTemplateResponse from weasyprint import HTML from sigi.apps.casas.models import Funcionario, Orgao +from sigi.apps.contatos.models import UnidadeFederativa from sigi.apps.convenios.models import Projeto from sigi.apps.eventos.models import TipoEvento, Evento from sigi.apps.eventos.forms import ( SelecionaModeloForm, ConviteForm, + EventosPorUfForm, CasaForm, FuncionarioForm, ParlamentarForm, @@ -345,7 +351,7 @@ def alocacao_equipe(request): context["title"] = _("Alocação de equipe") context["pdf"] = True return WeasyTemplateResponse( - # filename="alocacao_equipe.pdf", + filename="alocacao_equipe.pdf", request=request, template="eventos/alocacao_equipe_pdf.html", context=context, @@ -364,6 +370,253 @@ def alocacao_equipe(request): return render(request, "eventos/alocacao_equipe.html", context) +@login_required +@staff_member_required +def eventos_por_uf(request): + formato = request.GET.get("fmt", "html") + initials = { + "data_inicio": datetime.date.today().replace(day=1), + "data_fim": datetime.date.today().replace( + day=calendar.monthrange( + datetime.date.today().year, datetime.date.today().month + )[1] + ), + "categoria": [c[0] for c in TipoEvento.CATEGORIA_CHOICES], + "virtual": [m[0] for m in EventosPorUfForm.MODO_CHOICES], + } + if "data_inicio" in request.GET or "data_fim" in request.GET: + form = EventosPorUfForm(request.GET) + else: + form = EventosPorUfForm(initial=initials) + if not form.is_valid(): + return render( + request, "eventos/eventos_por_uf.html", context={"form": form} + ) + data_inicio = form.cleaned_data.get("data_inicio") + data_fim = form.cleaned_data.get("data_fim") + categorias = form.cleaned_data.get("categoria", initials["categoria"]) + virtual = form.cleaned_data.get("virtual", initials["virtual"]) + annotates = dict() + aggfuncs = dict() + if "P" in virtual: + annotates["eventos_presenciais"] = Count( + "municipio__orgao__evento__id", + distinct=True, + filter=Q(municipio__orgao__evento__virtual=False), + ) + annotates["participantes_presenciais"] = Sum( + "municipio__orgao__evento__total_participantes", + filter=Q(municipio__orgao__evento__virtual=False), + ) + aggfuncs["nº eventos presenciais"] = sum + aggfuncs["participantes presenciais"] = sum + if "V" in virtual: + annotates["eventos_virtuais"] = Count( + "municipio__orgao__evento__id", + distinct=True, + filter=Q(municipio__orgao__evento__virtual=True), + ) + annotates["participantes_virtuais"] = Sum( + "municipio__orgao__evento__total_participantes", + filter=Q(municipio__orgao__evento__virtual=True), + ) + aggfuncs["nº eventos virtuais"] = sum + aggfuncs["participantes virtuais"] = sum + eventos = ( + UnidadeFederativa.objects.filter( + municipio__orgao__evento__status=Evento.STATUS_REALIZADO, + municipio__orgao__evento__data_inicio__range=( + data_inicio, + data_fim, + ), + municipio__orgao__evento__tipo_evento__categoria__in=categorias, + ) + .order_by("regiao", "nome") + .values( + "regiao", + "nome", + "municipio__orgao__evento__tipo_evento__categoria", + ) + .annotate(**annotates) + ) + df = pd.DataFrame(eventos) + if df.empty: + messages.add_message( + request, + messages.ERROR, + _("Nenhum evento foi realizado no período solicitado"), + ) + return render( + request, "eventos/eventos_por_uf.html", context={"form": form} + ) + # Renomeia colunas + df.rename( + columns={ + "municipio__orgao__evento__tipo_evento__categoria": "categoria", + "eventos_presenciais": "nº eventos presenciais", + "eventos_virtuais": "nº eventos virtuais", + "participantes_presenciais": "participantes presenciais", + "participantes_virtuais": "participantes virtuais", + }, + inplace=True, + ) + # Troca a sigla pelo nome da região + for sigla, nome in UnidadeFederativa.REGIAO_CHOICES: + df["regiao"].replace(sigla, nome, inplace=True) + # Troca o código pelo nome da categoria de eventos + for cod, nome in TipoEvento.CATEGORIA_CHOICES: + df["categoria"].replace(cod, nome, inplace=True) + # Cria tabela pivot das UFs + pivo_uf = df.pivot_table( + index=["regiao", "nome"], + columns="categoria", + aggfunc=aggfuncs, + fill_value=0, + ) + if len(categorias) > 1: + # calcula os totais de eventos e de participantes para as UFs + ix_eventos_presenciais = [ + i for i in pivo_uf.columns if i[0] == "nº eventos presenciais" + ] + ix_eventos_virtuais = [ + i for i in pivo_uf.columns if i[0] == "nº eventos virtuais" + ] + ix_participantes_presenciais = [ + i for i in pivo_uf.columns if i[0] == "participantes presenciais" + ] + ix_participantes_virtuais = [ + i for i in pivo_uf.columns if i[0] == "participantes virtuais" + ] + if ix_eventos_presenciais: + pivo_uf[("nº eventos presenciais", "total")] = pivo_uf[ + ix_eventos_presenciais + ].sum(axis=1) + ix_eventos_presenciais.append(("nº eventos presenciais", "total")) + if ix_eventos_virtuais: + pivo_uf[("nº eventos virtuais", "total")] = pivo_uf[ + ix_eventos_virtuais + ].sum(axis=1) + ix_eventos_virtuais.append(("nº eventos virtuais", "total")) + if ix_participantes_presenciais: + pivo_uf[("participantes presenciais", "total")] = pivo_uf[ + ix_participantes_presenciais + ].sum(axis=1) + ix_participantes_presenciais.append( + ("participantes presenciais", "total") + ) + if ix_participantes_virtuais: + pivo_uf[("participantes virtuais", "total")] = pivo_uf[ + ix_participantes_virtuais + ].sum(axis=1) + ix_participantes_virtuais.append( + ("participantes virtuais", "total") + ) + pivo_uf = pivo_uf[ + ix_eventos_presenciais + + ix_eventos_virtuais + + ix_participantes_presenciais + + ix_participantes_virtuais + ] + # Cria tabela pivot das regiões + pivo_regiao = df.pivot_table( + index="regiao", + columns="categoria", + aggfunc=aggfuncs, + fill_value=0, + ) + # Calcula os totais de eventos e participantes para as regiões + if len(categorias) > 1: + ix_eventos_presenciais = [ + i for i in pivo_regiao.columns if i[0] == "nº eventos presenciais" + ] + ix_eventos_virtuais = [ + i for i in pivo_regiao.columns if i[0] == "nº eventos virtuais" + ] + ix_participantes_presenciais = [ + i + for i in pivo_regiao.columns + if i[0] == "participantes presenciais" + ] + ix_participantes_virtuais = [ + i for i in pivo_regiao.columns if i[0] == "participantes virtuais" + ] + if ix_eventos_presenciais: + pivo_regiao[("nº eventos presenciais", "total")] = pivo_regiao[ + ix_eventos_presenciais + ].sum(axis=1) + ix_eventos_presenciais.append(("nº eventos presenciais", "total")) + if ix_eventos_virtuais: + pivo_regiao[("nº eventos virtuais", "total")] = pivo_regiao[ + ix_eventos_virtuais + ].sum(axis=1) + ix_eventos_virtuais.append(("nº eventos virtuais", "total")) + if ix_participantes_presenciais: + pivo_regiao[("participantes presenciais", "total")] = pivo_regiao[ + ix_participantes_presenciais + ].sum(axis=1) + ix_participantes_presenciais.append( + ("participantes presenciais", "total") + ) + if ix_participantes_virtuais: + pivo_regiao[("participantes virtuais", "total")] = pivo_regiao[ + ix_participantes_virtuais + ].sum(axis=1) + ix_participantes_virtuais.append( + ("participantes virtuais", "total") + ) + pivo_regiao = pivo_regiao[ + ix_eventos_presenciais + + ix_eventos_virtuais + + ix_participantes_presenciais + + ix_participantes_virtuais + ] + # Cabeçalhos para impressão + cabecalho_uf = [ + (k, [i[1] for i in v]) + for k, v in groupby(pivo_uf.columns, lambda x: x[0]) + ] + cabecalho_regiao = [ + (k, [i[1] for i in v]) + for k, v in groupby(pivo_regiao.columns, lambda x: x[0]) + ] + # Imprimir + context = { + "form": form, + "data_inicio": data_inicio, + "data_fim": data_fim, + "categorias": [ + c[1] for c in TipoEvento.CATEGORIA_CHOICES if c[0] in categorias + ], + "virtual": [ + m[1] for m in EventosPorUfForm.MODO_CHOICES if m[0] in virtual + ], + "pivo_uf": pivo_uf, + "pivo_regiao": pivo_regiao, + "cabecalho_uf": cabecalho_uf, + "cabecalho_regiao": cabecalho_regiao, + "total_uf": pivo_uf.sum(), + "total_regiao": pivo_regiao.sum(), + } + if formato == "pdf": + context["title"] = _("Eventos por Unidade da Federação") + context["pdf"] = True + return WeasyTemplateResponse( + filename=f"eventos_por_uf-{data_inicio}-{data_fim}.pdf", + request=request, + template="eventos/eventos_por_uf_pdf.html", + context=context, + content_type="application/pdf", + ) + elif formato == "csv": + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = ( + f'attachment; filename="eventos_por_uf-{data_inicio}-{data_fim}.csv"' + ) + pivo_uf.to_csv(response) + return response + return render(request, "eventos/eventos_por_uf.html", context=context) + + class ApiEventoAbstract: queryset = ( Evento.objects.filter(publicar=True) diff --git a/sigi/menu_conf.yaml b/sigi/menu_conf.yaml index 0e47259..e1c0c62 100644 --- a/sigi/menu_conf.yaml +++ b/sigi/menu_conf.yaml @@ -26,6 +26,23 @@ admin_menu: - title: Configurações view_name: admin:utils_config_changelist main_menu: + - title: Relatórios + icon: print + children: + - title: Eventos por UF + view_name: eventos_eventosporuf + - title: Calendário de eventos + view_name: eventos_calendario + - title: Alocação de equipe eventos + view_name: eventos_alocacaoequipe + separator: [after,] + - title: Reservas de espaços + view_name: espacos_agenda + - title: Uso dos espaços + view_name: espacos_usoespaco + separator: [after,] + - title: Lista de gerentes + view_name: casas_gerentes - title: Municípios icon: location_city children: @@ -71,8 +88,6 @@ main_menu: view_name: - title: Organizar relacionamentos view_name: - - title: Lista de gerentes - view_name: casas_gerentes - title: Convênios icon: assignment children: @@ -101,10 +116,6 @@ main_menu: - title: Reservas view_name: admin:espacos_reserva_changelist querystr: status__exact=A - - title: Agenda de reservas - view_name: espacos_agenda - - title: Uso dos espaços - view_name: espacos_usoespaco - title: Eventos icon: school children: @@ -128,10 +139,6 @@ main_menu: - title: Visitas Interlegis view_name: admin:eventos_evento_changelist querystr: tipo_evento__categoria__exact=V - - title: Calendário mensal - view_name: eventos_calendario - - title: Alocação de equipe - view_name: eventos_alocacaoequipe - title: Servidores icon: account_circle children: