diff --git a/sapl/materia/onlyoffice_materia_views.py b/sapl/materia/onlyoffice_materia_views.py index 2a1257781..f0f0b637a 100644 --- a/sapl/materia/onlyoffice_materia_views.py +++ b/sapl/materia/onlyoffice_materia_views.py @@ -526,3 +526,162 @@ def docacessorio_onlyoffice_editor(request, pk): } return render(request, 'onlyoffice/onlyoffice_editor.html', context) + + +# ============================================================ +# Views para Geração de PDF para Assinatura +# ============================================================ + +@login_required +@require_http_methods(["GET"]) +def materia_gerar_pdf_assinatura(request, pk): + """ + Gera o PDF do documento da matéria para assinatura digital. + Usa a API de conversão do OnlyOffice para converter DOCX para PDF. + """ + import requests as http_requests + import xml.etree.ElementTree as ET + + materia = get_object_or_404(MateriaLegislativa, pk=pk) + + # Verifica se a matéria tem número de protocolo + if not materia.numero_protocolo: + messages.error(request, 'Esta matéria ainda não possui número de protocolo.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + # Verifica se existe documento + if not materia.texto_original: + messages.error(request, 'Esta matéria não possui documento de texto original.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + # Verifica a extensão do arquivo + file_path = materia.texto_original.path + file_name = materia.texto_original.name.lower() + + # Se já é PDF, retorna diretamente + if file_name.endswith('.pdf'): + try: + with open(file_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 arquivo PDF: {e}") + messages.error(request, 'Erro ao ler o arquivo PDF.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + # URL do documento para o OnlyOffice (dentro da rede Docker) + download_url = request.build_absolute_uri( + reverse('sapl.materia:materia_onlyoffice_download', kwargs={'pk': pk}) + ) + + # Substituir pelo nome do container na rede Docker + host = request.get_host() + download_url = download_url.replace(f'http://{host}', 'http://sapl-dev:8000') + download_url = download_url.replace(f'https://{host}', 'http://sapl-dev:8000') + + # URL da API de conversão do OnlyOffice (dentro da rede Docker) + conversion_url = 'http://onlyoffice:80/ConvertService.ashx' + + # Configuração da conversão + conversion_data = { + "async": False, + "filetype": "docx", + "key": generate_file_key("materia_pdf", materia.pk, request.user.pk), + "outputtype": "pdf", + "title": f"Materia_{materia.tipo}_{materia.numero}_{materia.ano}.pdf", + "url": download_url, + } + + # Adiciona JWT se estiver habilitado + if settings.ONLYOFFICE_JWT_ENABLED and settings.ONLYOFFICE_JWT_SECRET: + import jwt + token = jwt.encode(conversion_data, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256') + conversion_data['token'] = token + + try: + # Solicita a conversão + headers = {'Content-Type': 'application/json'} + + # Adiciona header de autorização JWT se habilitado + if settings.ONLYOFFICE_JWT_ENABLED and settings.ONLYOFFICE_JWT_SECRET: + 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: + logger.error(f"Erro na conversão OnlyOffice: status={conversion_response.status_code}") + messages.error(request, 'Erro ao converter o documento para PDF.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + # A API do OnlyOffice retorna XML + try: + root = ET.fromstring(conversion_response.text) + except ET.ParseError as e: + logger.error(f"Erro ao parsear resposta XML do OnlyOffice: {e}") + logger.error(f"Resposta recebida: {conversion_response.text[:500]}") + messages.error(request, 'Erro ao processar resposta do serviço de conversão.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + # Verifica se houve erro + error_elem = root.find('Error') + if error_elem is not None: + error_code = error_elem.text + logger.error(f"Erro na conversão OnlyOffice: error={error_code}") + messages.error(request, f'Erro na conversão do documento: código {error_code}') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + # Obtém a URL do PDF convertido + file_url_elem = root.find('FileUrl') + if file_url_elem is None or not file_url_elem.text: + logger.error("URL do PDF não retornada pelo OnlyOffice") + logger.error(f"Resposta XML: {conversion_response.text}") + messages.error(request, 'Erro ao obter o documento PDF convertido.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + pdf_url = file_url_elem.text + + # Substitui 'onlyoffice' pelo endereço correto na rede Docker + # A URL retornada usa 'onlyoffice' como host + if 'onlyoffice/' in pdf_url and not pdf_url.startswith('http://onlyoffice:'): + pdf_url = pdf_url.replace('http://onlyoffice/', 'http://onlyoffice:80/') + + logger.info(f"URL do PDF para download: {pdf_url}") + + # Baixa o PDF convertido + pdf_response = http_requests.get(pdf_url, timeout=60) + + if pdf_response.status_code != 200: + logger.error(f"Erro ao baixar PDF convertido: status={pdf_response.status_code}") + messages.error(request, 'Erro ao baixar o documento PDF convertido.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + + # Retorna o PDF + filename = f"Materia_{materia.tipo}_{materia.numero}_{materia.ano}.pdf" + # Remove caracteres especiais do nome do arquivo + 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("Timeout na conversão OnlyOffice") + messages.error(request, 'Tempo limite excedido ao converter o documento.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + except http_requests.exceptions.ConnectionError: + logger.error("Erro de conexão com OnlyOffice") + messages.error(request, 'Não foi possível conectar ao serviço de conversão de documentos.') + return redirect('sapl.materia:materialegislativa_detail', pk=pk) + except Exception as e: + 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) diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index f7ff5a892..10c8b7e3a 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -38,7 +38,8 @@ from sapl.materia.onlyoffice_materia_views import ( materia_onlyoffice_editor, materia_onlyoffice_config, materia_onlyoffice_download, materia_onlyoffice_callback, docacessorio_onlyoffice_editor, docacessorio_onlyoffice_config, - docacessorio_onlyoffice_download, docacessorio_onlyoffice_callback + docacessorio_onlyoffice_download, docacessorio_onlyoffice_callback, + materia_gerar_pdf_assinatura ) from sapl.norma.views import NormaPesquisaSimplesView from sapl.protocoloadm.views import ( @@ -145,6 +146,8 @@ urlpatterns_materia = [ name='materia_onlyoffice_download'), url(r'^materia/(?P\d+)/onlyoffice/callback$', materia_onlyoffice_callback, name='materia_onlyoffice_callback'), + url(r'^materia/(?P\d+)/pdf-assinatura$', materia_gerar_pdf_assinatura, + name='materia_pdf_assinatura'), # OnlyOffice endpoints para Documento Acessório url(r'^materia/documentoacessorio/(?P\d+)/onlyoffice/editor$', docacessorio_onlyoffice_editor, diff --git a/sapl/templates/materia/materialegislativa_detail.html b/sapl/templates/materia/materialegislativa_detail.html index 10e5043c5..903f86fca 100644 --- a/sapl/templates/materia/materialegislativa_detail.html +++ b/sapl/templates/materia/materialegislativa_detail.html @@ -15,6 +15,13 @@ {% endif %} + {% if object.numero_protocolo and object.texto_original %} + + {% endif %} {% endblock sub_actions %} {% block editions %}