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' %}
+
+
+
+
+ {% 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' %} |
+
+
+
+ {% for sol in solicitacoes.all %}
+ {% ifchanged sol.casa.municipio.uf.regiao %}
+
+
+ {{ sol.casa.municipio.uf.get_regiao_display }}
+ |
+
+ {% endifchanged %}
+
+ {{ 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:"-" }} |
+
+ {% endfor %}
+
+ {% trans 'Sumário' %} |
+ {% for valor in sumario %}
+
+ {% if forloop.last %}
+ {{ valor|floatformat:2|default:"-" }}
+ {% else %}
+ {{ valor|default:"-" }}
+ {% endif %}
+ |
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans 'Resumo por Senador' %}
+
+
+
+
+ {% trans 'UF' %} |
+ {% trans 'Senador' %} |
+ {% trans 'Quantidade' %} |
+ {% trans 'Custo total' %} |
+
+
+ {% trans 'Solicitada' %} |
+ {% trans 'Atendida' %} |
+ {% trans 'Não atendida' %} |
+ {% trans 'Participantes' %} |
+
+
+
+ {% for uf in resumo_uf.itertuples %}
+ {% ifchanged uf.regiao %}
+
+ {{ uf.regiao }} |
+
+ {% endifchanged %}
+
+ {{ uf.uf }} |
+ {{ uf.senador }} |
+ {{ uf.qtde_solicitadas }} |
+ {{ uf.qtde_atendidas|default:"-" }} |
+ {{ uf.qtde_rejeitadas|default:"-" }} |
+ {{ uf.participantes|default:"-" }} |
+ {{ uf.custo_total|floatformat:2|default:"-" }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans 'Resumo por Região' %}
+
+
+
+
+ {% trans 'Região' %} |
+ {% trans 'Quantidade' %} |
+ {% trans 'Custo total' %} |
+
+
+ {% trans 'Solicitada' %} |
+ {% trans 'Atendida' %} |
+ {% trans 'Não atendida' %} |
+ {% trans 'Participantes' %} |
+
+
+
+ {% for regiao in resumo_regiao.itertuples %}
+
+ {{ regiao.regiao }} |
+ {{ regiao.qtde_solicitadas }} |
+ {{ regiao.qtde_atendidas|default:"-" }} |
+ {{ regiao.qtde_rejeitadas|default:"-" }} |
+ {{ regiao.participantes|default:"-" }} |
+ {{ regiao.custo_total|floatformat:2|default:"-" }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans 'Resumo por tipo de evento' %}
+
+
+
+
+ {% trans 'Sigla' %} |
+ {% trans 'Nome' %} |
+ {% trans 'Quantidade' %} |
+ {% trans 'Custo total' %} |
+
+
+ {% trans 'Solicitada' %} |
+ {% trans 'Atendida' %} |
+ {% trans 'Não atendida' %} |
+ {% trans 'Participantes' %} |
+
+
+
+ {% for tipo in resumo_tipo_evento.itertuples %}
+
+ {{ tipo.sigla }} |
+ {{ tipo.nome }} |
+ {{ tipo.qtde_solicitadas }} |
+ {{ tipo.qtde_atendidas|default:"-" }} |
+ {{ tipo.qtde_rejeitadas|default:"-" }} |
+ {{ tipo.participantes|default:"-" }} |
+ {{ tipo.custo_total|floatformat:2|default:"-" }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
\ 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 %}
+
+ {% else %}
+
+
+
+
+ {% trans 'Legenda' %}
+ {% include "eventos/snippets/solicitacoes_por_periodo_legenda_snippet.html" %}
+
+
+
+
+
+ {% include "eventos/snippets/solicitacoes_por_periodo_snippet.html" with mode="html" %}
+
+ {% 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