From 425686ce0d9313ec39eba2a16f596756ae61499a Mon Sep 17 00:00:00 2001 From: KemuelAlves Date: Thu, 14 May 2026 09:16:16 -0300 Subject: [PATCH] =?UTF-8?q?Feat=20(Assinatura=20digital):=20Como=20cliente?= =?UTF-8?q?,=20quero=20poder=20imprimir=20diversos=20documentos=20de=20uma?= =?UTF-8?q?=20=C3=BAnica=20vez=20[AB#1325]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sapl/materia/onlyoffice_materia_views.py | 207 ++++++++++++++++++ sapl/materia/urls.py | 10 +- sapl/templates/materia/assinatura_modal.html | 166 +++++++++++++- .../materia/materialegislativa_detail.html | 10 +- .../materia/materialegislativa_filter.html | 114 +++++++++- 5 files changed, 491 insertions(+), 16 deletions(-) diff --git a/sapl/materia/onlyoffice_materia_views.py b/sapl/materia/onlyoffice_materia_views.py index e7ebb663d..3da122b51 100644 --- a/sapl/materia/onlyoffice_materia_views.py +++ b/sapl/materia/onlyoffice_materia_views.py @@ -821,3 +821,210 @@ def materia_gerar_pdf_assinatura(request, pk): logger.error(f"Erro inesperado na geração de PDF: {e}") messages.error(request, 'Erro inesperado ao gerar o PDF.') return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + +# ============================================================ +# Prévia de PDF para Documento Acessório +# ============================================================ + +# ============================================================ +# Prévia de PDF para Documento Acessório +# ============================================================ + +@login_required +@require_http_methods(["GET"]) +def materia_gerar_pdf_previa(request, pk): + """ + Gera o PDF da matéria para prévia antes da assinatura. + Diferente do materia_gerar_pdf_assinatura, não exige numero_protocolo. + Se já for PDF, retorna direto. Caso seja DOCX, converte via OnlyOffice. + """ + import requests as http_requests + import xml.etree.ElementTree as ET + + materia = get_object_or_404(MateriaLegislativa, pk=pk) + + if not materia.texto_original: + return HttpResponse('Matéria não possui documento de texto original.', status=404) + + file_name = materia.texto_original.name.lower() + + if file_name.endswith('.pdf'): + try: + with open(materia.texto_original.path, 'rb') as f: + content = f.read() + response = HttpResponse(content, content_type='application/pdf') + response['Content-Disposition'] = ( + f'inline; filename="Materia_{materia.tipo}_{materia.numero}_{materia.ano}.pdf"' + ) + return response + except Exception as e: + logger.error(f"Erro ao ler PDF da materia {pk}: {e}") + return HttpResponse('Erro ao ler o arquivo PDF.', status=500) + + download_url = build_onlyoffice_url( + request, + reverse('sapl.materia:materia_onlyoffice_download', kwargs={'pk': pk}) + ) + + conversion_url = f'{settings.ONLYOFFICE_URL}/ConvertService.ashx' + + conversion_data = { + "async": False, + "filetype": "docx", + "key": generate_file_key("materia_previa", pk, request.user.pk), + "outputtype": "pdf", + "title": f"Materia_{materia.tipo}_{materia.numero}_{materia.ano}.pdf", + "url": download_url, + } + + if getattr(settings, 'ONLYOFFICE_JWT_ENABLED', False) and getattr(settings, 'ONLYOFFICE_JWT_SECRET', None): + import jwt + token = jwt.encode(conversion_data, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + conversion_data['token'] = token + + try: + headers = {'Content-Type': 'application/json'} + + if getattr(settings, 'ONLYOFFICE_JWT_ENABLED', False) and getattr(settings, 'ONLYOFFICE_JWT_SECRET', None): + import jwt + header_token = jwt.encode({"payload": conversion_data}, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + headers['Authorization'] = f'Bearer {header_token}' + + conversion_response = http_requests.post( + conversion_url, + json=conversion_data, + headers=headers, + timeout=60 + ) + + if conversion_response.status_code != 200: + return HttpResponse('Erro ao converter o documento.', status=500) + + try: + root = ET.fromstring(conversion_response.text) + except ET.ParseError as e: + logger.error(f"Erro ao parsear resposta XML (materia previa): {e}") + return HttpResponse('Erro ao processar resposta do serviço.', status=500) + + error_elem = root.find('Error') + if error_elem is not None: + return HttpResponse(f'Erro na conversão: {error_elem.text}', status=500) + + file_url_elem = root.find('FileUrl') + if file_url_elem is None or not file_url_elem.text: + return HttpResponse('URL do PDF não retornada.', status=500) + + pdf_response = http_requests.get(file_url_elem.text, timeout=60) + if pdf_response.status_code != 200: + return HttpResponse('Erro ao baixar PDF convertido.', status=500) + + filename = f"Materia_{materia.tipo}_{materia.numero}_{materia.ano}.pdf" + filename = filename.replace(' ', '_').replace('/', '-') + response = HttpResponse(pdf_response.content, content_type='application/pdf') + response['Content-Disposition'] = f'inline; filename="{filename}"' + return response + + except http_requests.exceptions.Timeout: + logger.error(f"Timeout na conversão OnlyOffice (materia previa pk={pk})") + return HttpResponse('Tempo limite excedido ao converter o documento.', status=504) + except http_requests.exceptions.ConnectionError: + logger.error(f"Erro de conexão com OnlyOffice (materia previa pk={pk})") + return HttpResponse('Não foi possível conectar ao serviço de conversão.', status=502) + except Exception as e: + logger.error(f"Erro inesperado na previa PDF (materia pk={pk}): {e}") + return HttpResponse('Erro inesperado ao gerar o PDF.', status=500) + + +@login_required +@require_http_methods(["GET"]) +def docacessorio_gerar_pdf_previa(request, pk): + """ + Gera o PDF do documento acessório para prévia antes da assinatura. + Se já for PDF, retorna direto. Caso seja DOCX, converte via OnlyOffice. + """ + import requests as http_requests + import xml.etree.ElementTree as ET + + docacessorio = get_object_or_404(DocumentoAcessorio, pk=pk) + + if not docacessorio.arquivo: + return HttpResponse('Documento sem arquivo.', status=404) + + file_name = docacessorio.arquivo.name.lower() + + if file_name.endswith('.pdf'): + try: + with open(docacessorio.arquivo.path, 'rb') as f: + content = f.read() + response = HttpResponse(content, content_type='application/pdf') + response['Content-Disposition'] = f'inline; filename="DocAcessorio_{pk}.pdf"' + return response + except Exception as e: + logger.error(f"Erro ao ler arquivo PDF do doc acessorio: {e}") + return HttpResponse('Erro ao ler o arquivo PDF.', status=500) + + download_url = build_onlyoffice_url( + request, + reverse('sapl.materia:docacessorio_onlyoffice_download', kwargs={'pk': pk}) + ) + + conversion_url = f'{settings.ONLYOFFICE_URL}/ConvertService.ashx' + + conversion_data = { + "async": False, + "filetype": "docx", + "key": generate_file_key("docacessorio_pdf", pk, request.user.pk), + "outputtype": "pdf", + "title": f"DocAcessorio_{pk}.pdf", + "url": download_url, + } + + if getattr(settings, 'ONLYOFFICE_JWT_ENABLED', False) and getattr(settings, 'ONLYOFFICE_JWT_SECRET', None): + import jwt + token = jwt.encode(conversion_data, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + conversion_data['token'] = token + + try: + headers = {'Content-Type': 'application/json'} + + if getattr(settings, 'ONLYOFFICE_JWT_ENABLED', False) and getattr(settings, 'ONLYOFFICE_JWT_SECRET', None): + import jwt + header_token = jwt.encode({"payload": conversion_data}, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + headers['Authorization'] = f'Bearer {header_token}' + + conversion_response = http_requests.post( + conversion_url, + json=conversion_data, + headers=headers, + timeout=60 + ) + + if conversion_response.status_code != 200: + return HttpResponse('Erro ao converter o documento.', status=500) + + try: + root = ET.fromstring(conversion_response.text) + except ET.ParseError as e: + logger.error(f"Erro ao parsear resposta XML: {e}") + return HttpResponse('Erro ao processar resposta do serviço.', status=500) + + error_elem = root.find('Error') + if error_elem is not None: + return HttpResponse(f'Erro na conversão: {error_elem.text}', status=500) + + file_url_elem = root.find('FileUrl') + if file_url_elem is None or not file_url_elem.text: + return HttpResponse('URL do PDF não retornada.', status=500) + + pdf_response = http_requests.get(file_url_elem.text, timeout=60) + if pdf_response.status_code != 200: + return HttpResponse('Erro ao baixar PDF convertido.', status=500) + + response = HttpResponse(pdf_response.content, content_type='application/pdf') + response['Content-Disposition'] = f'inline; filename="DocAcessorio_{pk}.pdf"' + return response + + except Exception as e: + logger.error(f"Erro inesperado na previa PDF (docacessorio): {e}") + return HttpResponse('Erro inesperado ao gerar o PDF.', status=500) diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index a3e67b8f2..007a7b7b6 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -47,7 +47,7 @@ from sapl.materia.onlyoffice_materia_views import ( docacessorio_onlyoffice_editor, docacessorio_onlyoffice_config, docacessorio_onlyoffice_download, docacessorio_onlyoffice_callback, docacessorio_check_doc, docacessorio_forcesave, - materia_gerar_pdf_assinatura + materia_gerar_pdf_assinatura, materia_gerar_pdf_previa, docacessorio_gerar_pdf_previa ) from sapl.materia.views_assinatura import ( materia_assinar_a1, materia_assinar_a3_preparar, materia_assinar_a3_finalizar, @@ -180,6 +180,10 @@ urlpatterns_materia = [ url(r'^materia/(?P\d+)/pdf-assinatura$', materia_gerar_pdf_assinatura, name='materia_pdf_assinatura'), + # Prévia de PDF da Matéria antes da assinatura (sem restrição de protocolo) + url(r'^materia/(?P\d+)/pdf-previa$', materia_gerar_pdf_previa, + name='materia_pdf_previa'), + # Assinatura Digital de Matéria Legislativa url(r'^materia/(?P\d+)/assinar/a1/$', materia_assinar_a1, name='materia_assinar_a1'), @@ -216,6 +220,10 @@ urlpatterns_materia = [ url(r'^materia/documentoacessorio/(?P\d+)/forcesave$', docacessorio_forcesave, name='docacessorio_forcesave'), + # Prévia de PDF do Documento Acessório antes da assinatura + url(r'^materia/documentoacessorio/(?P\d+)/pdf-previa$', docacessorio_gerar_pdf_previa, + name='docacessorio_pdf_previa'), + # Assinatura Digital de Documento Acessório url(r'^materia/documentoacessorio/(?P\d+)/assinar/a1/$', docacessorio_assinar_a1, name='docacessorio_assinar_a1'), diff --git a/sapl/templates/materia/assinatura_modal.html b/sapl/templates/materia/assinatura_modal.html index 25c15ba62..d7c27d438 100644 --- a/sapl/templates/materia/assinatura_modal.html +++ b/sapl/templates/materia/assinatura_modal.html @@ -1,5 +1,52 @@ {% load i18n %} + + +