From 83a7263a75d2a268ed2cc8296950c1a0b9509026 Mon Sep 17 00:00:00 2001 From: rangelbruno Date: Sat, 21 Feb 2026 15:51:49 -0300 Subject: [PATCH] =?UTF-8?q?Feat(Materia):=20Adicionar=20bot=C3=A3o=20Baixa?= =?UTF-8?q?r=20Todos=20os=20Documentos=20(ZIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permite baixar ZIP com o PDF da matéria (assinado ou original) junto com todos os documentos acessórios. Botão aparece na página de detalhe e na pesquisa de matérias quando há documentos acessórios cadastrados. --- sapl/materia/urls.py | 4 +- sapl/materia/views.py | 87 +++++++++++++++++++ .../js/materia_pesquisa_download_pdfs.js | 13 +++ .../materia/materialegislativa_detail.html | 7 ++ .../materia/materialegislativa_filter.html | 5 +- 5 files changed, 113 insertions(+), 3 deletions(-) diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index e51f196f3..ffa14d199 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_docacessorios, get_pdf_docacessorios, get_zip_completo, configEtiquetaMateriaLegislativaCrud, PesquisarStatusTramitacaoView, HistoricoProposicaoView) from sapl.materia.onlyoffice_views import (onlyoffice_config, onlyoffice_download, @@ -147,6 +147,8 @@ urlpatterns_materia = [ name='compress_docacessorios'), url(r'^materia/docacessorio/pdf/(?P\d+)$', get_pdf_docacessorios, name='merge_docacessorios'), + url(r'^materia/zip-completo/(?P\d+)$', get_zip_completo, + name='zip_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 4f95e87e8..bc319916c 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -3256,6 +3256,93 @@ def get_zip_docacessorios(request, pk): return response +def create_zip_completo(materia): + """ + Creates in memory zip file with materia PDF + all documentos acessórios + """ + logger = logging.getLogger(__name__) + + files_to_zip = [] + + # PDF da matéria: prefere pdf_assinado, fallback texto_original + if materia.pdf_assinado: + files_to_zip.append(os.path.join(MEDIA_ROOT, str(materia.pdf_assinado))) + elif materia.texto_original: + files_to_zip.append(os.path.join(MEDIA_ROOT, str(materia.texto_original))) + + # Documentos acessórios: prefere pdf_assinado, fallback arquivo + for doc in materia.documentoacessorio_set.all(): + if doc.pdf_assinado: + files_to_zip.append(os.path.join(MEDIA_ROOT, str(doc.pdf_assinado))) + elif doc.arquivo: + files_to_zip.append(os.path.join(MEDIA_ROOT, str(doc.arquivo))) + + if not files_to_zip: + return None, None + + logger.info( + "Gerando zip completo da matéria com {} arquivos: {}".format( + len(files_to_zip), files_to_zip)) + + _zipfile = BytesIO() + added = 0 + + try: + with zipfile.ZipFile(_zipfile, 'w', zipfile.ZIP_DEFLATED) as zipf: + for f in files_to_zip: + if os.path.exists(f): + zipf.write(f, f.split(os.sep)[-1]) + added += 1 + else: + logger.warning("Arquivo não encontrado: {}".format(f)) + except Exception as e: + logger.error(e) + raise e + + if not added: + raise FileNotFoundError( + "Nenhum arquivo encontrado no disco para a matéria.") + + external_name = "mat_{}_{}_completo.zip".format( + materia.numero, materia.ano) + return external_name, _zipfile.getvalue() + + +def get_zip_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) + data = None + try: + external_name, data = create_zip_completo(materia) + logger.info( + "user= {}. Gerou o zip completo da matéria {}".format(username, pk)) + except FileNotFoundError: + logger.error("user= {}.Arquivos não encontrados para matéria {}".format(username, pk)) + msg = _('Não há arquivos 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 zip 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})) + + if not data: + msg = _('Não há nenhum documento cadastrado para esta matéria.') + messages.add_message(request, messages.ERROR, msg) + return redirect(reverse('sapl.materia:materialegislativa_detail', + kwargs={'pk': pk})) + + response = HttpResponse(data, content_type='application/zip') + 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 14692a7c2..a65bc74a3 100644 --- a/sapl/static/js/materia_pesquisa_download_pdfs.js +++ b/sapl/static/js/materia_pesquisa_download_pdfs.js @@ -166,6 +166,19 @@ // insere logo após o strong.parentNode.insertBefore(document.createTextNode(' '), strong.nextSibling); strong.parentNode.insertBefore(btn, strong.nextSibling); + + // Botão "Baixar Todos" (ZIP com matéria + acessórios) + const materiaPk = link.getAttribute('data-materia-pk'); + if (materiaPk) { + 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.href = '/materia/zip-completo/' + materiaPk; + zipBtn.innerHTML = ''; + strong.parentNode.insertBefore(document.createTextNode(' '), btn.nextSibling); + strong.parentNode.insertBefore(zipBtn, btn.nextSibling.nextSibling); + } }); } diff --git a/sapl/templates/materia/materialegislativa_detail.html b/sapl/templates/materia/materialegislativa_detail.html index 3e52f7452..082e5923d 100644 --- a/sapl/templates/materia/materialegislativa_detail.html +++ b/sapl/templates/materia/materialegislativa_detail.html @@ -28,6 +28,13 @@ {% endif %} {% endif %} + {% if object.documentoacessorio_set.all.exists %} + + {% endif %} {% endblock sub_actions %} {% block editions %} diff --git a/sapl/templates/materia/materialegislativa_filter.html b/sapl/templates/materia/materialegislativa_filter.html index 923434194..885ab5d38 100644 --- a/sapl/templates/materia/materialegislativa_filter.html +++ b/sapl/templates/materia/materialegislativa_filter.html @@ -209,8 +209,9 @@ {% endif %} {% if m.texto_original %} - + Texto Original