From 317e60e5c75460dcf0844fec8232fe91e6d2edc8 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 15 Aug 2025 15:52:27 -0300 Subject: [PATCH] =?UTF-8?q?Implementa=20Relat=C3=B3rio=20de=20Vota=C3=A7?= =?UTF-8?q?=C3=B5es=20Nominais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/relatorios/forms.py | 67 +++++++++- sapl/relatorios/urls.py | 7 +- sapl/relatorios/views.py | 78 ++++++++++- sapl/sessao/models.py | 15 ++- .../materia/materialegislativa_filter.html | 59 ++++++++- .../RelatorioVotacoesNominais_filter.html | 79 ++++++++++++ .../relatorios/relatorio_votacao_nominal.html | 49 +++++++ .../templates/relatorios/relatorios_list.html | 7 + sapl/utils.py | 122 ++++++++++++++++++ 9 files changed, 465 insertions(+), 18 deletions(-) create mode 100644 sapl/templates/relatorios/RelatorioVotacoesNominais_filter.html create mode 100644 sapl/templates/relatorios/relatorio_votacao_nominal.html diff --git a/sapl/relatorios/forms.py b/sapl/relatorios/forms.py index 59eb2d741..a7f42e8e5 100644 --- a/sapl/relatorios/forms.py +++ b/sapl/relatorios/forms.py @@ -3,6 +3,7 @@ from crispy_forms.bootstrap import (FormActions) from crispy_forms.layout import (HTML, Button, Fieldset, Layout, Submit) from django import forms +from django.forms import ModelChoiceField from django.utils.translation import ugettext_lazy as _ from sapl.audiencia.models import AudienciaPublica @@ -10,10 +11,10 @@ from sapl.base.models import Autor from sapl.comissoes.models import Reuniao from sapl.crispy_layout_mixin import SaplFormHelper, to_row, form_actions from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa, MateriaEmTramitacao, UnidadeTramitacao, \ - StatusTramitacao + StatusTramitacao, TipoMateriaLegislativa from sapl.norma.models import NormaJuridica from sapl.protocoloadm.models import DocumentoAdministrativo -from sapl.sessao.models import SessaoPlenaria +from sapl.sessao.models import SessaoPlenaria, VotoParlamentar from sapl.utils import FilterOverridesMetaMixin, choice_anos_com_normas, qs_override_django_filter, \ choice_anos_com_materias, choice_tipos_normas, autor_label, autor_modal @@ -68,6 +69,68 @@ class RelatorioDocumentosAcessoriosFilterSet(django_filters.FilterSet): ) +class RelatorioVotacoesNominaisFilterSet(django_filters.FilterSet): + + @property + def qs(self): + parent = super(RelatorioVotacoesNominaisFilterSet, self).qs + return parent.distinct().order_by('-votacao_id', 'parlamentar') + + class Meta(FilterOverridesMetaMixin): + model = VotoParlamentar + fields = ['data_hora'] + + def __init__(self, *args, **kwargs): + super( + RelatorioVotacoesNominaisFilterSet, self + ).__init__(*args, **kwargs) + + self.filters['data_hora'].label = 'Período (Data Inicial - Data Final)' + + tipo_materia = '''
' + + numero = '''
+
''' + + ano = '''
+
' + + row0= HTML('
' + tipo_materia + numero + ano + '
') + + row1 = to_row([('data_hora', 12)]) + + buttons = FormActions( + *[ + HTML(''' +
+ + +
+ ''') + ], + Submit('pesquisar', _('Pesquisar'), css_class='float-right', + onclick='return true;'), + css_class='form-group row justify-content-between', + ) + + self.form.helper = SaplFormHelper() + self.form.helper.form_method = 'GET' + self.form.helper.layout = Layout( + Fieldset(_('Pesquisa'), + row0, row1, + buttons) + ) + + class RelatorioAtasFilterSet(django_filters.FilterSet): class Meta(FilterOverridesMetaMixin): model = SessaoPlenaria diff --git a/sapl/relatorios/urls.py b/sapl/relatorios/urls.py index b27bfc55a..5244ea3d6 100644 --- a/sapl/relatorios/urls.py +++ b/sapl/relatorios/urls.py @@ -11,7 +11,7 @@ from .views import (relatorio_capa_processo, RelatorioMateriasTramitacaoView, RelatorioMateriaAnoAssuntoView, RelatorioHistoricoTramitacaoView, RelatorioDataFimPrazoTramitacaoView, RelatorioPresencaSessaoView, RelatorioAtasView, RelatorioReuniaoView, RelatorioAudienciaView, RelatorioHistoricoTramitacaoAdmView, - RelatorioDocumentosAcessoriosView, RelatorioNormasPorAutorView) + RelatorioDocumentosAcessoriosView, RelatorioNormasPorAutorView, RelatorioVotacoesNominaisView) from ..base.views import EstatisticasAcessoNormas app_name = AppConfig.name @@ -95,6 +95,9 @@ urlpatterns = [ url(r'^sistema/relatorios/documentos_acessorios$', RelatorioDocumentosAcessoriosView.as_view(), name='relatorio_documentos_acessorios'), + url(r'^sistema/relatorios/votacoes_nominais$', + RelatorioVotacoesNominaisView.as_view(), + name='relatorio_votacoes_nominais'), url(r'^sistema/relatorios/normas-por-autor$', RelatorioNormasPorAutorView.as_view(), name='normas_por_autor'), -] \ No newline at end of file +] diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 99a299abb..4c5eaab42 100755 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -31,7 +31,8 @@ from sapl.relatorios.forms import RelatorioNormasPorAutorFilterSet, RelatorioHis RelatorioNormasVigenciaFilterSet, RelatorioNormasMesFilterSet, RelatorioMateriasPorAutorFilterSet, \ RelatorioMateriasPorAnoAutorTipoFilterSet, RelatorioMateriasTramitacaoFilterSet, RelatorioAudienciaFilterSet, \ RelatorioReuniaoFilterSet, RelatorioDataFimPrazoTramitacaoFilterSet, RelatorioHistoricoTramitacaoFilterSet, \ - RelatorioPresencaSessaoFilterSet, RelatorioAtasFilterSet, RelatorioDocumentosAcessoriosFilterSet + RelatorioPresencaSessaoFilterSet, RelatorioAtasFilterSet, RelatorioDocumentosAcessoriosFilterSet, \ + RelatorioVotacoesNominaisFilterSet from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, IntegranteMesa, JustificativaAusencia, Orador, OradorExpediente, @@ -50,7 +51,7 @@ from sapl.sessao.views import (get_identificacao_basica, get_mesa_diretora, from sapl.settings import MEDIA_URL from sapl.settings import STATIC_ROOT from sapl.utils import LISTA_DE_UFS, TrocaTag, filiacao_data, create_barcode, show_results_filter_set, \ - num_materias_por_tipo, parlamentares_ativos + num_materias_por_tipo, parlamentares_ativos, VotacoesMultiFormatOutputMixin from .templates import (pdf_capa_processo_gerar, pdf_documento_administrativo_gerar, pdf_espelho_gerar, pdf_etiqueta_protocolo_gerar, pdf_materia_gerar, @@ -1560,6 +1561,10 @@ def relatorio_documento_acessorio(obj, request, context): return cria_relatorio(request, context, 'relatorios/relatorio_documento_acessorio.html') +def relatorio_votacao_nominal(obj, request, context): + return cria_relatorio(request, context, 'relatorios/relatorio_votacao_nominal.html') + + def relatorio_normas_por_autor(obj, request, context): return cria_relatorio(request, context, 'relatorios/relatorio_normas_por_autor.html') @@ -1880,6 +1885,75 @@ class RelatorioDocumentosAcessoriosView(RelatorioMixin, FilterView): return context +class RelatorioVotacoesNominaisView(RelatorioMixin, VotacoesMultiFormatOutputMixin, FilterView): + model = VotoParlamentar + filterset_class = RelatorioVotacoesNominaisFilterSet + template_name = 'relatorios/RelatorioVotacoesNominais_filter.html' + relatorio = relatorio_votacao_nominal + + fields_base_report = [ + 'votacao_id', 'votacao', 'parlamentar__nome_parlamentar', 'voto' + ] + + fields_report = { + 'csv': fields_base_report, + 'xlsx': fields_base_report, + 'json': fields_base_report, + } + + def get_context_data(self, **kwargs): + context = super( + RelatorioVotacoesNominaisView, self + ).get_context_data(**kwargs) + + context['title'] = _('Votações Nominais') + + if not self.filterset.form.is_valid(): + return context + + query_dict = self.request.GET.copy() + context['filter_url'] = ('&' + query_dict.urlencode()) if len(query_dict) > 0 else '' + context['show_results'] = show_results_filter_set(query_dict) + + data_inicial = self.request.GET['data_hora_0'] + data_final = self.request.GET['data_hora_1'] + if not data_inicial: + data_inicial = "Data Inicial não definida" + if not data_final: + data_final = "Data Final não definida" + context['periodo'] = ( + data_inicial + ' - ' + data_final + ) + + if self.request.GET['tipo_materia'] or self.request.GET['numero'] or self.request.GET['ano']: + object_list = context['object_list'] + if self.request.GET['tipo_materia']: + tipo_id = self.request.GET['tipo_materia'] + context['tipo_materia'] = TipoMateriaLegislativa.objects.get(id=tipo_id) + object_list = object_list.filter( + Q(ordem__materia__tipo_id=tipo_id) | + Q(expediente__materia__tipo_id=tipo_id)) + if self.request.GET['numero']: + numero = self.request.GET['numero'] + context['numero'] = numero + object_list = object_list.filter( + Q(ordem__materia__numero=numero) | + Q(expediente__materia__numero=numero)) + if self.request.GET['ano']: + ano = self.request.GET['ano'] + context['ano'] = ano + object_list = object_list.filter( + Q(ordem__materia__ano=ano) | + Q(expediente__materia__ano=ano)) + context['object_list'] = object_list + + + if not 'format' in query_dict: + context['qtde_votacoes'] = context['object_list'].distinct('votacao_id').count() + + return context + + class RelatorioAtasView(RelatorioMixin, FilterView): model = SessaoPlenaria filterset_class = RelatorioAtasFilterSet diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 55fe581a1..d20ba930c 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -641,11 +641,14 @@ class RegistroVotacao(models.Model): ordering = ('id',) def __str__(self): - return _('Ordem: %(ordem)s - Votação: %(votacao)s - ' - 'Matéria: %(materia)s') % { - 'ordem': self.ordem, - 'votacao': self.tipo_resultado_votacao, - 'materia': self.materia} + if self.ordem: + return _('Ordem: %(ordem)s - Votação: %(votacao)s') % { + 'ordem': self.ordem, + 'votacao': self.tipo_resultado_votacao} + else: + return _('Expediente: %(expediente)s - Votação: %(votacao)s') % { + 'expediente': self.expediente, + 'votacao': self.tipo_resultado_votacao} def clean(self): """Exatamente um dos campos ordem ou expediente deve estar preenchido. @@ -696,7 +699,7 @@ class VotoParlamentar(models.Model): # RegistroVotacaoParlamentar class Meta: verbose_name = _('Registro de Votação de Parlamentar') verbose_name_plural = _('Registros de Votações de Parlamentares') - ordering = ('id',) + ordering = ('parlamentar',) def __str__(self): return _('Votação: %(votacao)s - Parlamentar: %(parlamentar)s') % { diff --git a/sapl/templates/materia/materialegislativa_filter.html b/sapl/templates/materia/materialegislativa_filter.html index 0c5064806..cddba92e4 100644 --- a/sapl/templates/materia/materialegislativa_filter.html +++ b/sapl/templates/materia/materialegislativa_filter.html @@ -108,19 +108,47 @@ Resultado:  {{m|resultado_votacao}}
{% endif %} {% if m.registrovotacao_set.exists %} +
+
Data Votação: +
+
{% for rv in m.registrovotacao_set.all %} {% if rv.ordem %} - - {{ rv.ordem.sessao_plenaria.data_inicio }} - + {{ rv.ordem.sessao_plenaria.data_inicio }} + {% if rv.ordem.get_tipo_votacao_display == 'Nominal' %} + - + Votação Nominal >>> + + + {% endif %} {% elif rv.expediente %} - - {{ rv.expediente.sessao_plenaria.data_inicio }} - + {{ rv.expediente.sessao_plenaria.data_inicio }} + {% if rv.expediente.get_tipo_votacao_display == 'Nominal' %} + - + Votação Nominal >>> + + + {% endif %} {% endif %}
{% endfor %} +
+
{% endif %} {% if m.tramitacao_set.first.data_tramitacao %} Data da última Tramitação:  {{m.tramitacao_set.first.data_tramitacao}}
@@ -215,6 +243,14 @@ {% block extra_js %} {% endblock extra_js %} diff --git a/sapl/templates/relatorios/RelatorioVotacoesNominais_filter.html b/sapl/templates/relatorios/RelatorioVotacoesNominais_filter.html new file mode 100644 index 000000000..36561fa7e --- /dev/null +++ b/sapl/templates/relatorios/RelatorioVotacoesNominais_filter.html @@ -0,0 +1,79 @@ +{% extends "crud/list.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block base_content %} + {% if not show_results %} + {% crispy filter.form %} + {% else %} +
+ {% with 'sapl.relatorios:relatorio_votacoes_nominais' as url_reverse %} + {% include "crud/format_options.html" %} + {% endwith %} +
+ +
+ {% trans 'Fazer uma nova pesquisa' %} +
+

+ PARÂMETROS DE PESQUISA
+ {% if tipo_materia %} +  Tipo de Matéria: {{ tipo_materia }}
+ {% endif %} + {% if numero %} +  Número: {{ numero }}
+ {% endif %} + {% if ano %} +  Ano: {{ ano }}
+ {% endif %} +  Período: {{ periodo }}

+ + {% if object_list %} + + {% if qtde_votacoes > 1 %} +

Foram encontradas {{qtde_votacoes}} votações.


+ {% elif qtde_votacoes == 1 %} +

Foi encontrada {{qtde_votacoes}} votação.


+ {% endif %} + + + + + + + + + {% for voto in object_list %} + {% ifchanged voto.votacao_id %} + {% if not forloop.first %} + + {% endif %} + +
Dados da Votação / MatériaParlamentar - Voto
+ {% if voto.ordem %} + {{ voto.ordem.materia }} - {{ voto.ordem.materia.ementa }}
+ Momento da Votação: Ordem do Dia - + {{ voto.ordem.sessao_plenaria }}
+ Data da Votação: {{ voto.data_hora|date:"d/m/Y" }}
+ Resultado: {{ voto.ordem.resultado }} + {% else %} + {{ voto.expediente.materia }} - {{ voto.expediente.materia.ementa }}
+ Momento da Votação: Expediente - + {{ voto.expediente.sessao_plenaria }}
+ Data da Votação: {{ voto.data_hora|date:"d/m/Y" }}
+ Resultado: {{ voto.expediente.resultado }} + + {% endif %} +
+ {% endifchanged %} + + {{ voto.parlamentar }} - {{ voto.voto }} +
+ {% endfor %} +
+ {% else %} +

Nenhuma votação encontrada com esses parâmetros.



+ {% endif %} + {% endif %} + {% include "paginacao.html" %} +{% endblock base_content %} diff --git a/sapl/templates/relatorios/relatorio_votacao_nominal.html b/sapl/templates/relatorios/relatorio_votacao_nominal.html new file mode 100644 index 000000000..cb0605b20 --- /dev/null +++ b/sapl/templates/relatorios/relatorio_votacao_nominal.html @@ -0,0 +1,49 @@ +{% extends "relatorios/base_relatorio.html" %} +{% load i18n %} +{% load common_tags %} +{% load static %} + +{% block content %} +

Votações Nominais

+ + PARÂMETROS DE PESQUISA:
+ {% if tipo_materia %} + Tipo de matéria: {{ tipo_materia }}
+ {% endif %} + Período: {{ periodo }}
+
+ + {% if object_list %} + + {% if qtde_votacoes > 1 %} +

Foram encontradas {{qtde_votacoes}} votações.

+ {% elif qtde_votacoes == 1 %} +

Foi encontrada {{qtde_votacoes}} votação.

+ {% endif %} + + {% for voto in object_list %} + {% ifchanged voto.votacao_id %} + {% if not forloop.first %}{% endif %} +
+ {% if voto.ordem %} + Matéria: {{ voto.ordem.materia }} - {{ voto.ordem.materia.ementa }}
+ Momento da Votação: Ordem do Dia - {{ voto.ordem.sessao_plenaria }}
+ Data da Votação: {{ voto.data_hora|date:"d/m/Y" }}
+ Resultado: {{ voto.ordem.resultado }}

+ {% else %} + Matéria: {{ voto.expediente.materia }} - {{ voto.expediente.materia.ementa }}
+ Momento da Votação: Expediente - {{ voto.expediente.sessao_plenaria }}
+ Data da Votação: {{ voto.data_hora|date:"d/m/Y" }}
+ Resultado: {{ voto.expediente.resultado }}

+ {% endif %} +
+ + {% else %} +

Nenhuma votação encontrada com esses parâmetros.



+ {% endif %} + +{% endblock content %} diff --git a/sapl/templates/relatorios/relatorios_list.html b/sapl/templates/relatorios/relatorios_list.html index 38346fa21..eec0843d0 100644 --- a/sapl/templates/relatorios/relatorios_list.html +++ b/sapl/templates/relatorios/relatorios_list.html @@ -127,6 +127,13 @@ Listagem e totalização de normas por autor, com filtros para tipo e período. {% endif %} + {% url 'sapl.relatorios:relatorio_votacoes_nominais' as relatorio_votacoes_nominais %} + {% if request|is_report_visible:relatorio_votacoes_nominais %} + + Votações Nominais + Votações Nominais em Expedientes e Ordens do Dia por data. + + {% endif %} diff --git a/sapl/utils.py b/sapl/utils.py index cddeb1f49..6c7213b0e 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -1707,3 +1707,125 @@ class PautaMultiFormatOutputMixin(MultiFormatOutputMixin): output.close() return response + + +class VotacoesMultiFormatOutputMixin(MultiFormatOutputMixin): + + def render_to_csv(self, context): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = f'attachment; filename="sapl_{self.request.resolver_match.url_name}.csv"' + response['Cache-Control'] = 'no-cache' + response['Pragma'] = 'no-cache' + response['Expires'] = 0 + writer = csv.writer(response, delimiter=";", + quoting=csv.QUOTE_NONNUMERIC) + + object_list = context['object_list'] + + data = [[list(self._headers(self.fields_report['csv']))], ] + for obj in object_list: + wr = list(self._write_row(obj, self.fields_report['csv'])) + if wr[0] != data[-1][0][0]: + data.append([wr]) + else: + data[-1].append(wr) + + for mri, multirows in enumerate(data): + if len(multirows) == 1: + writer.writerow(multirows[0]) + else: + for v in multirows: + writer.writerow(v) + + return response + + def render_to_xlsx(self, context): + + object_list = context['object_list'] + + data = [[list(self._headers(self.fields_report['xlsx']))], ] + row = 0 + for obj in object_list: + wr = list(self._write_row(obj, self.fields_report['xlsx'])) + if wr[0] != data[-1][0][0]: + data.append([wr]) + else: + data[-1].append(wr) + + output = io.BytesIO() + wb = Workbook(output, {'in_memory': True}) + + ws = wb.add_worksheet() + + for mri, multirows in enumerate(data): + if len(multirows) == 1: + for rc, cell in enumerate(multirows[0]): + ws.write(row, rc, cell) + row += 1 + else: + for v in multirows: + for rc, cell in enumerate(v): + try: + ws.write(row, rc, cell) + except TypeError: + ws.write(row, rc, str(cell)) + row += 1 + ws.autofit() + wb.close() + + output.seek(0) + + response = HttpResponse(output.read( + ), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + response['Content-Disposition'] = f'attachment; filename="sapl_{self.request.resolver_match.url_name}.xlsx"' + response['Cache-Control'] = 'no-cache' + response['Pragma'] = 'no-cache' + response['Expires'] = 0 + + output.close() + + return response + + def render_to_json(self, context): + + object_list = context['object_list'] + + data = [] + for obj in object_list: + wr = list(self._write_row(obj, self.fields_report['json'])) + + wr[1] = str(wr[1]) + if not data: + data.append([wr]) + continue + + if wr[0] != data[-1][0][0]: + data.append([wr]) + else: + data[-1].append(wr) + + fields_report = list(map(lambda i, j: (i, j), self.fields_report['json'], self._headers(self.fields_report['json']))) + fields_report[2] = ('parlamentar_voto', 'Voto por Parlamentar') + fields_report_data = [] + for f in fields_report: + fields_report_data.append(f[0]) + for mri, multirows in enumerate(data): + parlamentar_voto = [] + for ri, cols in enumerate(multirows): + parlamentar_voto.append([cols[2], cols[3]]) + data[mri] = dict( + map(lambda i, j: (i, j), fields_report_data, [multirows[0][0], multirows[0][1], parlamentar_voto])) + + json_metadata = { + 'headers': dict(fields_report[:-1]), + 'results': data + } + + response = JsonResponse(json_metadata) + response['Content-Disposition'] = f'attachment; filename="sapl_{self.request.resolver_match.url_name}.json"' + response['Cache-Control'] = 'no-cache' + response['Pragma'] = 'no-cache' + response['Expires'] = 0 + + return response +