From ba4e7704a18d939a719a7d8dd750b6303db1cd6f Mon Sep 17 00:00:00 2001 From: rangelbruno Date: Sat, 21 Feb 2026 16:00:50 -0300 Subject: [PATCH] =?UTF-8?q?Feat(Materia):=20Adicionar=20download=20PDF=20u?= =?UTF-8?q?nificado=20e=20melhorar=20layout=20dos=20bot=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona opção de baixar matéria + acessórios mesclados em um único PDF, além do ZIP já existente. Reorganiza botões da página de detalhe com flex-wrap responsivo e cores distintas para cada ação. --- sapl/materia/urls.py | 4 +- sapl/materia/views.py | 77 +++++++++++++++++++ .../js/materia_pesquisa_download_pdfs.js | 19 ++++- .../materia/materialegislativa_detail.html | 43 +++++------ 4 files changed, 116 insertions(+), 27 deletions(-) diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index ffa14d199..397028b26 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -29,7 +29,7 @@ from sapl.materia.views import (AcompanhamentoConfirmarView, RetornarProposicao, MateriaPesquisaSimplesView, DespachoInicialMultiCreateView, - get_zip_docacessorios, get_pdf_docacessorios, get_zip_completo, + get_zip_docacessorios, get_pdf_docacessorios, get_zip_completo, get_pdf_completo, configEtiquetaMateriaLegislativaCrud, PesquisarStatusTramitacaoView, HistoricoProposicaoView) from sapl.materia.onlyoffice_views import (onlyoffice_config, onlyoffice_download, @@ -149,6 +149,8 @@ urlpatterns_materia = [ name='merge_docacessorios'), url(r'^materia/zip-completo/(?P\d+)$', get_zip_completo, name='zip_completo_materia'), + url(r'^materia/pdf-completo/(?P\d+)$', get_pdf_completo, + name='pdf_completo_materia'), # OnlyOffice endpoints para Matéria Legislativa url(r'^materia/(?P\d+)/onlyoffice/editor$', materia_onlyoffice_editor, diff --git a/sapl/materia/views.py b/sapl/materia/views.py index bc319916c..385161499 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -3343,6 +3343,83 @@ def get_zip_completo(request, pk): return response +def create_pdf_completo(materia): + """ + Creates a unified in memory PDF with materia PDF + all documentos acessórios + """ + logger = logging.getLogger(__name__) + + pdf_files = [] + + # PDF da matéria: prefere pdf_assinado, fallback texto_original + if materia.pdf_assinado: + pdf_files.append(os.path.join(MEDIA_ROOT, str(materia.pdf_assinado))) + elif materia.texto_original: + f = os.path.join(MEDIA_ROOT, str(materia.texto_original)) + if f.lower().endswith('pdf'): + pdf_files.append(f) + + # Documentos acessórios: prefere pdf_assinado, fallback arquivo (somente PDF) + for doc in materia.documentoacessorio_set.all(): + if doc.pdf_assinado: + pdf_files.append(os.path.join(MEDIA_ROOT, str(doc.pdf_assinado))) + elif doc.arquivo: + f = os.path.join(MEDIA_ROOT, str(doc.arquivo)) + if f.lower().endswith('pdf'): + pdf_files.append(f) + + # Filtra apenas arquivos que existem no disco + pdf_files = [f for f in pdf_files if os.path.exists(f)] + + if not pdf_files: + raise FileNotFoundError( + "Nenhum arquivo PDF encontrado para a matéria.") + + logger.info( + "Gerando PDF completo da matéria com {} arquivos: {}".format( + len(pdf_files), pdf_files)) + + merger = PdfFileMerger(strict=False) + for f in pdf_files: + merger.append(fileobj=f) + + data = BytesIO() + merger.write(data) + merger.close() + + external_name = "mat_{}_{}_completo.pdf".format( + materia.numero, materia.ano) + return external_name, data.getvalue() + + +def get_pdf_completo(request, pk): + logger = logging.getLogger(__name__) + username = 'Usuário anônimo' if request.user.is_anonymous else request.user.username + materia = get_object_or_404(MateriaLegislativa, pk=pk) + try: + external_name, data = create_pdf_completo(materia) + logger.info( + "user= {}. Gerou o PDF completo da matéria {}".format(username, pk)) + except FileNotFoundError: + logger.error("user= {}.Arquivos PDF não encontrados para matéria {}".format(username, pk)) + msg = _('Não há arquivos PDF disponíveis para download nesta matéria.') + messages.add_message(request, messages.ERROR, msg) + return redirect(reverse('sapl.materia:materialegislativa_detail', + kwargs={'pk': pk})) + except Exception as e: + logger.error("user={}. Erro ao criar PDF completo da matéria {}: {}" + .format(username, pk, str(e))) + msg = _('Um erro inesperado ocorreu. Entre em contato com o suporte do SGVP.') + messages.add_message(request, messages.ERROR, msg) + return redirect(reverse('sapl.materia:materialegislativa_detail', + kwargs={'pk': pk})) + + response = HttpResponse(data, content_type='application/pdf') + response['Content-Disposition'] = ('attachment; filename="%s"' + % external_name) + return response + + def create_pdf_docacessorios(materia): """ Creates a unified in memory PDF file diff --git a/sapl/static/js/materia_pesquisa_download_pdfs.js b/sapl/static/js/materia_pesquisa_download_pdfs.js index a65bc74a3..389c7065f 100644 --- a/sapl/static/js/materia_pesquisa_download_pdfs.js +++ b/sapl/static/js/materia_pesquisa_download_pdfs.js @@ -167,17 +167,28 @@ strong.parentNode.insertBefore(document.createTextNode(' '), strong.nextSibling); strong.parentNode.insertBefore(btn, strong.nextSibling); - // Botão "Baixar Todos" (ZIP com matéria + acessórios) + // Botões "Baixar Todos" quando há documentos acessórios const materiaPk = link.getAttribute('data-materia-pk'); if (materiaPk) { + // Botão PDF unificado + const pdfBtn = document.createElement('a'); + pdfBtn.setAttribute('data-sapl-download-all-btn', '1'); + pdfBtn.className = 'btn btn-sm btn-info ml-1'; + pdfBtn.title = 'Baixar todos em PDF único'; + pdfBtn.href = '/materia/pdf-completo/' + materiaPk; + pdfBtn.innerHTML = ''; + strong.parentNode.insertBefore(document.createTextNode(' '), btn.nextSibling); + strong.parentNode.insertBefore(pdfBtn, btn.nextSibling.nextSibling); + + // Botão ZIP const zipBtn = document.createElement('a'); zipBtn.setAttribute('data-sapl-download-all-btn', '1'); zipBtn.className = 'btn btn-sm btn-info ml-1'; - zipBtn.title = 'Baixar todos os documentos (ZIP)'; + zipBtn.title = 'Baixar todos em ZIP'; zipBtn.href = '/materia/zip-completo/' + materiaPk; zipBtn.innerHTML = ''; - strong.parentNode.insertBefore(document.createTextNode(' '), btn.nextSibling); - strong.parentNode.insertBefore(zipBtn, btn.nextSibling.nextSibling); + strong.parentNode.insertBefore(document.createTextNode(' '), pdfBtn.nextSibling); + strong.parentNode.insertBefore(zipBtn, pdfBtn.nextSibling.nextSibling); } }); } diff --git a/sapl/templates/materia/materialegislativa_detail.html b/sapl/templates/materia/materialegislativa_detail.html index 082e5923d..92ca306d7 100644 --- a/sapl/templates/materia/materialegislativa_detail.html +++ b/sapl/templates/materia/materialegislativa_detail.html @@ -3,38 +3,37 @@ {% block sub_actions %} {{ block.super }} - {% if object.em_tramitacao and mail_service_configured and google_recaptcha_configured %} - - {% endif %} - {% if is_autor and not object.numero_protocolo or can_edit_materia %} -
- + - {% endif %} - {% if object.numero_protocolo and object.texto_original %} -
+ {% endif %} + {% if object.numero_protocolo and object.texto_original %} {% if object.pdf_assinado %} - + {% trans "Ver PDF Assinado" %} {% elif is_autor %} - {% endif %} -
- {% endif %} - {% if object.documentoacessorio_set.all.exists %} - - {% endif %} + + {% trans "Todos em ZIP" %} + + {% endif %} +
{% endblock sub_actions %} {% block editions %}