diff --git a/sigi/apps/eventos/admin_urls.py b/sigi/apps/eventos/admin_urls.py index 859488b..04ee891 100644 --- a/sigi/apps/eventos/admin_urls.py +++ b/sigi/apps/eventos/admin_urls.py @@ -1,4 +1,4 @@ -from django.urls import path, include +from django.urls import path from sigi.apps.eventos import views urlpatterns = [ @@ -7,4 +7,9 @@ urlpatterns = [ "alocacaoequipe/", views.alocacao_equipe, name="eventos_alocacaoequipe" ), path("eventosporuf/", views.eventos_por_uf, name="eventos_eventosporuf"), + path( + "solicitacoesporperiodo/", + views.solicitacoes_por_periodo, + name="eventos_solicitacoesporperiodo", + ), ] diff --git a/sigi/apps/eventos/forms.py b/sigi/apps/eventos/forms.py index f84f16b..3a05bb3 100644 --- a/sigi/apps/eventos/forms.py +++ b/sigi/apps/eventos/forms.py @@ -16,6 +16,7 @@ from sigi.apps.eventos.models import ( ModeloDeclaracao, Evento, TipoEvento, + Solicitacao, ) from sigi.apps.parlamentares.models import Parlamentar @@ -163,6 +164,51 @@ class EventosPorUfForm(forms.Form): ] +class SolicitacoesPorPeriodoForm(forms.Form): + MODO_CHOICES = ( + (True, _("Virtual")), + (False, _("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, + ) + tipos_evento = forms.ModelMultipleChoiceField( + required=False, + label=_("Tipos de evento"), + queryset=TipoEvento.objects.all(), + ) + virtual = forms.MultipleChoiceField( + required=False, + label=_("Modo"), + choices=MODO_CHOICES, + widget=forms.CheckboxSelectMultiple, + ) + status = forms.MultipleChoiceField( + required=False, + label=_("Status"), + choices=Solicitacao.STATUS_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/snippets/solicitacoes_por_periodo_legenda_snippet.html b/sigi/apps/eventos/templates/eventos/snippets/solicitacoes_por_periodo_legenda_snippet.html new file mode 100644 index 0000000..cff8219 --- /dev/null +++ b/sigi/apps/eventos/templates/eventos/snippets/solicitacoes_por_periodo_legenda_snippet.html @@ -0,0 +1,38 @@ +{% load i18n %} + +
+ + + + + + + + + + + + + + + + + +
{% trans 'Período' %} + {% blocktranslate with inicio=data_inicio|date:"SHORT_DATE_FORMAT" fim=data_fim|date:"SHORT_DATE_FORMAT" %} + {{ inicio }} a {{ fim }} + {% endblocktranslate %} +
{% trans 'Status' %} +
    + {% for i, label in status_choices %} +
  • {{ i }}: {{ label }}
  • + {% endfor %} +
+
{% trans 'Oficinas' %} +
    + {% for sigla, nome in legenda_oficinas %} +
  • {{ sigla }}: {{ nome }}
  • + {% endfor %} +
+
+
diff --git a/sigi/apps/eventos/templates/eventos/snippets/solicitacoes_por_periodo_snippet.html b/sigi/apps/eventos/templates/eventos/snippets/solicitacoes_por_periodo_snippet.html new file mode 100644 index 0000000..a382f26 --- /dev/null +++ b/sigi/apps/eventos/templates/eventos/snippets/solicitacoes_por_periodo_snippet.html @@ -0,0 +1,200 @@ +{% load i18n %} +
+
+
+
+ {% trans 'Solicitações' %} +
+ + + + + + + + + + + + + + + + + + + + + {% for sol in solicitacoes.all %} + {% ifchanged sol.casa.municipio.uf.regiao %} + + + + {% endifchanged %} + + + + + + + + + + + + + + {% endfor %} + + + {% for valor in sumario %} + + {% endfor %} + + +
{% trans 'UF' %}{% trans 'Microrregião' %}{% trans 'Casa solicitante' %}{% trans 'Senador' %}{% trans 'Data pedido' %}{% trans 'Oficinas (status)' %}{% trans 'Quantidade' %}{% trans 'Custo total' %}
{% trans 'Solicitada' %}{% trans 'Atendida' %}{% trans 'Não atendida' %}{% trans 'Participantes' %}
+ {{ sol.casa.municipio.uf.get_regiao_display }} +
{{ sol.casa.municipio.uf.sigla }}{{ sol.casa.municipio.microrregiao.nome }}{{ sol.casa.nome }}{{ sol.senador }}{{ sol.data_pedido|date:"SHORT_DATE_FORMAT" }} +
    + {% for item in sol.itemsolicitado_set.all %} +
  • {{ item.tipo_evento.sigla }} ({{ item.status }})
  • + {% endfor %} +
+
{{ sol.qtde_solicitadas }}{{ sol.qtde_atendidas|default:"-" }}{{ sol.qtde_rejeitadas|default:"-" }}{{ sol.participantes|default:"-" }}{{ sol.custo_total|floatformat:2|default:"-" }}
{% trans 'Sumário' %} + {% if forloop.last %} + {{ valor|floatformat:2|default:"-" }} + {% else %} + {{ valor|default:"-" }} + {% endif %} +
+
+
+
+
+
+ +
+
+
+
+ {% trans 'Resumo por Senador' %} +
+ + + + + + + + + + + + + + + + + {% for uf in resumo_uf.itertuples %} + {% ifchanged uf.regiao %} + + + + {% endifchanged %} + + + + + + + + + + {% endfor %} + +
{% trans 'UF' %}{% trans 'Senador' %}{% trans 'Quantidade' %}{% trans 'Custo total' %}
{% trans 'Solicitada' %}{% trans 'Atendida' %}{% trans 'Não atendida' %}{% trans 'Participantes' %}
{{ uf.regiao }}
{{ uf.uf }}{{ uf.senador }}{{ uf.qtde_solicitadas }}{{ uf.qtde_atendidas|default:"-" }}{{ uf.qtde_rejeitadas|default:"-" }}{{ uf.participantes|default:"-" }}{{ uf.custo_total|floatformat:2|default:"-" }}
+
+
+
+
+
+ +
+
+
+
+ {% trans 'Resumo por Região' %} +
+ + + + + + + + + + + + + + + + {% for regiao in resumo_regiao.itertuples %} + + + + + + + + + {% endfor %} + +
{% trans 'Região' %}{% trans 'Quantidade' %}{% trans 'Custo total' %}
{% trans 'Solicitada' %}{% trans 'Atendida' %}{% trans 'Não atendida' %}{% trans 'Participantes' %}
{{ regiao.regiao }}{{ regiao.qtde_solicitadas }}{{ regiao.qtde_atendidas|default:"-" }}{{ regiao.qtde_rejeitadas|default:"-" }}{{ regiao.participantes|default:"-" }}{{ regiao.custo_total|floatformat:2|default:"-" }}
+
+
+
+
+
+ +
+
+
+
+ {% trans 'Resumo por tipo de evento' %} +
+ + + + + + + + + + + + + + + + + {% for tipo in resumo_tipo_evento.itertuples %} + + + + + + + + + + {% endfor %} + +
{% trans 'Sigla' %}{% trans 'Nome' %}{% trans 'Quantidade' %}{% trans 'Custo total' %}
{% trans 'Solicitada' %}{% trans 'Atendida' %}{% trans 'Não atendida' %}{% trans 'Participantes' %}
{{ tipo.sigla }}{{ tipo.nome }}{{ tipo.qtde_solicitadas }}{{ tipo.qtde_atendidas|default:"-" }}{{ tipo.qtde_rejeitadas|default:"-" }}{{ tipo.participantes|default:"-" }}{{ tipo.custo_total|floatformat:2|default:"-" }}
+
+
+
+
+
\ No newline at end of file diff --git a/sigi/apps/eventos/templates/eventos/solicitacoes_por_periodo.html b/sigi/apps/eventos/templates/eventos/solicitacoes_por_periodo.html new file mode 100644 index 0000000..c32f27b --- /dev/null +++ b/sigi/apps/eventos/templates/eventos/solicitacoes_por_periodo.html @@ -0,0 +1,112 @@ +{% 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 'Solicitações de evento por período' %}
+{% endblock %} + +{% block breadcrumbs %}{% endblock %} + +{% block content %} + {% if solicitacoes is None %} +
+
+
+
+
+ {{ form }} +
+
+ + {% if not solicitacoes is None %} +
+ + print + +
    +
  • +
  • +
+
+ {% endif %} +
+
+
+
+
+ {% else %} +
+
+
+
+ {% trans 'Legenda' %} + {% include "eventos/snippets/solicitacoes_por_periodo_legenda_snippet.html" %} +
+ +
+
+
+ {% include "eventos/snippets/solicitacoes_por_periodo_snippet.html" with mode="html" %} +
+ {% for field in form %}{{ field.as_hidden }}{% endfor %} +
+ + print + + +
+
+ {% endif %} +{% endblock %} + +{% block footer %} + {{ block.super }} + {{ form.media }} + +{% endblock %} diff --git a/sigi/apps/eventos/templates/eventos/solicitacoes_por_periodo_pdf.html b/sigi/apps/eventos/templates/eventos/solicitacoes_por_periodo_pdf.html new file mode 100644 index 0000000..c906c87 --- /dev/null +++ b/sigi/apps/eventos/templates/eventos/solicitacoes_por_periodo_pdf.html @@ -0,0 +1,54 @@ +{% 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; + } + ul.report-list>li { + list-style: inside square; + white-space: nowrap; + } + div.legenda { + margin-bottom: 12px; + } + div.legenda th::after { + content: ":"; + } + div.legenda tr:nth-child(even) { + background-color: white; +} +{% endblock %} + + +{% block main_content %} + {% include "eventos/snippets/solicitacoes_por_periodo_legenda_snippet.html" %} + {% include "eventos/snippets/solicitacoes_por_periodo_snippet.html" %} +{% endblock %} diff --git a/sigi/apps/eventos/views.py b/sigi/apps/eventos/views.py index 040b838..c18a0eb 100644 --- a/sigi/apps/eventos/views.py +++ b/sigi/apps/eventos/views.py @@ -5,49 +5,40 @@ import locale import pandas as pd from functools import reduce from itertools import groupby -from rest_framework import mixins, generics +from rest_framework import generics from typing import OrderedDict -from django import forms 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.db.models import Count, Sum, Q, F, OuterRef, Subquery from django.http import HttpResponse -from django.shortcuts import redirect, render, get_object_or_404 -from django.template import Template, Context +from django.shortcuts import render from django.utils import timezone -from django.utils.text import slugify from django.utils.translation import ( to_locale, get_language, ngettext, gettext as _, ) -from django.urls import reverse from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import ListView -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.models import ( + TipoEvento, + Evento, + Equipe, + Solicitacao, + ItemSolicitado, +) from sigi.apps.eventos.forms import ( - SelecionaModeloForm, - ConviteForm, EventosPorUfForm, - CasaForm, - FuncionarioForm, - ParlamentarForm, + SolicitacoesPorPeriodoForm, ) from sigi.apps.eventos.serializers import ( EventoSerializer, EventoListSerializer, ) -from sigi.apps.parlamentares.models import Parlamentar -from sigi.apps.servidores.models import Servidor @login_required @@ -154,7 +145,6 @@ def calendario(request): if pdf: context["title"] = _("Calendário de eventos") context["pdf"] = True - # return render(request, "eventos/calendario_pdf.html", context) return WeasyTemplateResponse( filename=f"calendario_{ano_pesquisa:04}{mes_pesquisa:02}.pdf", request=request, @@ -620,6 +610,232 @@ def eventos_por_uf(request): return render(request, "eventos/eventos_por_uf.html", context=context) +@login_required +@staff_member_required +def solicitacoes_por_periodo(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] + ), + "tipos_evento": TipoEvento.objects.all(), + "virtual": [m[0] for m in SolicitacoesPorPeriodoForm.MODO_CHOICES], + "status": [s[0] for s in Solicitacao.STATUS_CHOICES], + } + if "data_inicio" in request.GET or "data_fim" in request.GET: + form = SolicitacoesPorPeriodoForm(request.GET) + else: + form = SolicitacoesPorPeriodoForm(initial=initials) + if not form.is_valid(): + return render( + request, + "eventos/solicitacoes_por_periodo.html", + context={"form": form}, + ) + data_inicio = form.cleaned_data.get("data_inicio") + data_fim = form.cleaned_data.get("data_fim") + tipos_evento = form.cleaned_data.get( + "tipos_evento", initials["tipos_evento"] + ) + virtual = form.cleaned_data.get("virtual", initials["virtual"]) + status = form.cleaned_data.get("status", initials["status"]) + + sq_equipe = ( + Equipe.objects.order_by() + .annotate( + tot=Sum( + F("qtde_diarias") * F("valor_diaria") + F("total_passagens") + ) + ) + .values("tot") + ) + sq_equipe.query.group_by = [] + solicitacoes = Solicitacao.objects.filter( + data_pedido__range=(data_inicio, data_fim), + itemsolicitado__tipo_evento__in=tipos_evento, + itemsolicitado__virtual__in=virtual, + status__in=status, + ) + legenda_oficinas = ( + solicitacoes.order_by("itemsolicitado__tipo_evento__sigla") + .values_list( + "itemsolicitado__tipo_evento__sigla", + "itemsolicitado__tipo_evento__nome", + ) + .distinct() + ) + solicitacoes = ( + solicitacoes.order_by( + "casa__municipio__uf__regiao", + "casa__municipio__uf", + "casa__nome", + "data_pedido", + ) + .annotate( + qtde_solicitadas=Count("itemsolicitado__id"), + qtde_atendidas=Count( + "itemsolicitado__id", + filter=Q( + itemsolicitado__status=ItemSolicitado.STATUS_AUTORIZADO + ), + ), + qtde_rejeitadas=Count( + "itemsolicitado__id", + filter=Q( + itemsolicitado__status=ItemSolicitado.STATUS_REJEITADO + ), + ), + participantes=Sum("itemsolicitado__evento__total_participantes"), + custo_total=Subquery( + sq_equipe.filter( + evento__itemsolicitado__solicitacao=OuterRef("pk") + )[:1] + ), + ) + .select_related( + "casa", + "casa__municipio", + "casa__municipio__uf", + "casa__municipio__microrregiao", + ) + .prefetch_related("itemsolicitado_set") + ) + sumario = solicitacoes.aggregate( + Sum("qtde_solicitadas"), + Sum("qtde_atendidas"), + Sum("qtde_rejeitadas"), + Sum("participantes"), + Sum("custo_total"), + ).values() + resumo_uf = ( + pd.DataFrame( + solicitacoes.values( + "casa__municipio__uf__regiao", + "casa__municipio__uf__sigla", + "senador", + "qtde_solicitadas", + "qtde_atendidas", + "qtde_rejeitadas", + "participantes", + "custo_total", + ) + ) + .rename( + columns={ + "casa__municipio__uf__regiao": "regiao", + "casa__municipio__uf__sigla": "uf", + } + ) + .fillna(0) + .replace({"regiao": dict(UnidadeFederativa.REGIAO_CHOICES)}) + .groupby(["regiao", "uf", "senador"], as_index=False) + .sum() + ) + resumo_uf["participantes"] = resumo_uf["participantes"].astype("int") + resumo_regiao = resumo_uf.groupby(["regiao"], as_index=False)[ + [ + "qtde_solicitadas", + "qtde_atendidas", + "qtde_rejeitadas", + "participantes", + "custo_total", + ] + ].sum() + resumo_uf.replace([0], [None], inplace=True) + resumo_regiao.replace([0], [None], inplace=True) + resumo_tipo_evento = ( + pd.DataFrame( + ItemSolicitado.objects.filter(solicitacao__in=solicitacoes) + .order_by("tipo_evento__sigla", "tipo_evento__nome") + .values("tipo_evento__sigla", "tipo_evento__nome") + .annotate( + qtde_solicitadas=Count("id"), + qtde_atendidas=Count( + "id", filter=Q(status=ItemSolicitado.STATUS_AUTORIZADO) + ), + qtde_rejeitadas=Count( + "id", filter=Q(status=ItemSolicitado.STATUS_REJEITADO) + ), + participantes=Sum("evento__total_participantes"), + custo_total=Subquery( + sq_equipe.filter(evento__itemsolicitado=OuterRef("pk"))[:1] + ), + ) + ) + .rename( + columns={ + "tipo_evento__sigla": "sigla", + "tipo_evento__nome": "nome", + } + ) + .groupby(["sigla", "nome"], as_index=False) + .sum() + .fillna(0) + ) + resumo_tipo_evento["participantes"] = resumo_tipo_evento[ + "participantes" + ].astype("int") + resumo_tipo_evento.replace([0], [None], inplace=True) + # Imprimir + context = { + "form": form, + "data_inicio": data_inicio, + "data_fim": data_fim, + "status_choices": ItemSolicitado.STATUS_CHOICES, + "legenda_oficinas": legenda_oficinas, + "tipos_evento": tipos_evento, + "virtual": [ + m[1] + for m in SolicitacoesPorPeriodoForm.MODO_CHOICES + if m[0] in virtual + ], + "solicitacoes": solicitacoes, + "sumario": sumario, + "resumo_uf": resumo_uf, + "resumo_regiao": resumo_regiao, + "resumo_tipo_evento": resumo_tipo_evento, + } + if formato == "pdf": + context["title"] = _("Solicitações por período") + context["pdf"] = True + return WeasyTemplateResponse( + filename=f"solicitacoes_por_periodo-{data_inicio}-{data_fim}.pdf", + request=request, + template="eventos/solicitacoes_por_periodo_pdf.html", + context=context, + content_type="application/pdf", + ) + elif formato == "csv": + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = ( + f'attachment; filename="solicitacoes_por_periodo-{data_inicio}-{data_fim}.csv"' + ) + fieldnames = [ + "id", + "casa__nome", + "casa__municipio__microrregiao__nome", + "casa__municipio__uf__sigla", + "casa__municipio__uf__regiao", + "senador", + "data_pedido", + "qtde_solicitadas", + "qtde_atendidas", + "qtde_rejeitadas", + "participantes", + "custo_total", + ] + writer = csv.DictWriter(response, fieldnames) + writer.writeheader() + writer.writerows(solicitacoes.values(*fieldnames)) + return response + return render( + request, "eventos/solicitacoes_por_periodo.html", context=context + ) + + class ApiEventoAbstract: queryset = ( Evento.objects.filter(publicar=True) diff --git a/sigi/menu_conf.yaml b/sigi/menu_conf.yaml index e1c0c62..33bcd40 100644 --- a/sigi/menu_conf.yaml +++ b/sigi/menu_conf.yaml @@ -31,6 +31,8 @@ main_menu: children: - title: Eventos por UF view_name: eventos_eventosporuf + - title: Solicitações de eventos por período + view_name: eventos_solicitacoesporperiodo - title: Calendário de eventos view_name: eventos_calendario - title: Alocação de equipe eventos