Browse Source

Feat (Assinatura): Como cliente, quero poder assinar documentos acessórios em lote

pull/3858/head
KemuelAlves 4 months ago
parent
commit
f4b239dc32
  1. 7
      sapl/materia/urls.py
  2. 23
      sapl/materia/views.py
  3. 474
      sapl/materia/views_assinatura.py
  4. 394
      sapl/templates/materia/documentoacessorio_list.html

7
sapl/materia/urls.py

@ -56,7 +56,8 @@ from sapl.materia.views_assinatura import (
detectar_aplicacao_a3, materia_assinar_lote, detectar_aplicacao_a3, materia_assinar_lote,
docacessorio_assinar_a1, docacessorio_pdf_assinado, docacessorio_assinar_a1, docacessorio_pdf_assinado,
docacessorio_verificar_assinatura, docacessorio_remover_assinatura, docacessorio_verificar_assinatura, docacessorio_remover_assinatura,
materia_verificar_documento, docacessorio_verificar_documento materia_verificar_documento, docacessorio_verificar_documento,
docacessorio_assinar_lote
) )
from sapl.norma.views import NormaPesquisaSimplesView from sapl.norma.views import NormaPesquisaSimplesView
from sapl.protocoloadm.views import ( from sapl.protocoloadm.views import (
@ -237,6 +238,10 @@ urlpatterns_materia = [
url(r'^materia/documentoacessorio/(?P<pk>\d+)/remover-assinatura/$', docacessorio_remover_assinatura, url(r'^materia/documentoacessorio/(?P<pk>\d+)/remover-assinatura/$', docacessorio_remover_assinatura,
name='docacessorio_remover_assinatura'), name='docacessorio_remover_assinatura'),
# Assinatura em Lote de Documentos Acessórios
url(r'^materia/documentoacessorio/assinar-em-lote/$', docacessorio_assinar_lote,
name='docacessorio_assinar_lote'),
# Verificação pública de autenticidade de Documento Acessório (sem login) # Verificação pública de autenticidade de Documento Acessório (sem login)
url(r'^materia/documentoacessorio/(?P<pk>\d+)/verificar/$', docacessorio_verificar_documento, url(r'^materia/documentoacessorio/(?P<pk>\d+)/verificar/$', docacessorio_verificar_documento,
name='docacessorio_verificar_documento'), name='docacessorio_verificar_documento'),

23
sapl/materia/views.py

@ -2019,6 +2019,29 @@ class DocumentoAcessorioCrud(MasterDetailCrud):
u.has_perm('materia.add_documentoacessorio') u.has_perm('materia.add_documentoacessorio')
) )
context['tipos_documento'] = TipoDocumento.objects.all() context['tipos_documento'] = TipoDocumento.objects.all()
# Documentos acessórios pendentes de assinatura para o lote
pode_assinar_lote = u.is_authenticated and (
u.is_superuser or
u.has_perm('materia.change_documentoacessorio')
)
if not pode_assinar_lote:
pode_assinar_lote = u.is_authenticated and OperadorAutor.objects.filter(user=u).exists()
if pode_assinar_lote:
materia_pk = self.kwargs.get('pk') or self.kwargs.get('root_pk')
qs_pendentes = DocumentoAcessorio.objects.filter(
materia__pk=materia_pk,
pdf_assinado='',
).order_by('data', 'nome')
docs_lote = [
{'id': d.pk, 'descricao': f'{d.nome} ({d.tipo}) — {d.data}'}
for d in qs_pendentes
]
context['docs_pendentes_lote'] = docs_lote
else:
context['docs_pendentes_lote'] = []
return context return context
def hook_arquivo(self, obj, default, url): def hook_arquivo(self, obj, default, url):

474
sapl/materia/views_assinatura.py

@ -2037,3 +2037,477 @@ def materia_assinar_lote(request):
'erros': erro_count, 'erros': erro_count,
'resultados': resultados, 'resultados': resultados,
}) })
# =============================================================================
# Assinatura em Lote de Documentos Acessórios
# =============================================================================
@login_required
@csrf_exempt
@require_http_methods(["POST"])
def docacessorio_assinar_lote(request):
"""
Assina em lote documentos acessórios pendentes com certificado A1.
POST multipart:
- certificado: arquivo .pfx / .p12
- senha: senha do certificado
- ids: JSON array com os PKs dos documentos acessórios ex: "[1,2,3]"
Retorna JSON:
{
"total": 3, "sucesso": 2, "erros": 1,
"resultados": [
{"pk": 1, "success": true, "descricao": "Despacho - PL 1/2025"},
{"pk": 2, "success": false, "descricao": "...", "error": "..."}
]
}
"""
_tem_perm_django = request.user.has_perm('materia.change_documentoacessorio')
try:
_autor_lote = OperadorAutor.objects.get(user=request.user).autor
except OperadorAutor.DoesNotExist:
_autor_lote = None
if not (_tem_perm_django or _autor_lote or request.user.is_superuser):
return JsonResponse(
{'success': False, 'error': 'Sem permissao para assinar documentos acessorios.'},
status=403
)
# -- IDs dos documentos --
ids_raw = request.POST.get('ids', '')
ids_multi = request.POST.getlist('ids[]')
if ids_multi:
pks = [int(i) for i in ids_multi if str(i).isdigit()]
elif ids_raw:
try:
parsed = json.loads(ids_raw)
pks = [int(i) for i in parsed if str(i).isdigit() or isinstance(i, int)]
except (json.JSONDecodeError, ValueError):
return JsonResponse(
{'success': False, 'error': 'Parametro "ids" invalido. Envie um array JSON.'},
status=400
)
else:
return JsonResponse({'success': False, 'error': 'Nenhum documento selecionado.'}, status=400)
if not pks:
return JsonResponse({'success': False, 'error': 'Lista de IDs vazia.'}, status=400)
if len(pks) > 200:
return JsonResponse(
{'success': False, 'error': 'Limite maximo de 200 documentos por lote.'},
status=400
)
# -- Certificado --
certificado_file = request.FILES.get('certificado')
senha = request.POST.get('senha', '')
if not certificado_file:
return JsonResponse({'success': False, 'error': 'Certificado nao informado.'}, status=400)
if not senha:
return JsonResponse({'success': False, 'error': 'Senha do certificado nao informada.'}, status=400)
cert_bytes = certificado_file.read()
import tempfile as tmp_module
with tmp_module.NamedTemporaryFile(delete=False, suffix='.pfx') as tmp_cert:
tmp_cert.write(cert_bytes)
tmp_cert_path = tmp_cert.name
try:
from pyhanko.sign import signers
signer = signers.SimpleSigner.load_pkcs12(
pfx_file=tmp_cert_path,
passphrase=senha.encode('utf-8')
)
except Exception as cert_error:
logger.error(f"[lote-doc] Erro ao carregar certificado: {cert_error}")
err_msg = str(cert_error)
if 'password' in err_msg.lower() or 'mac' in err_msg.lower():
detail = 'Senha incorreta ou arquivo invalido.'
elif 'decode' in err_msg.lower() or 'parse' in err_msg.lower():
detail = 'Arquivo nao e um certificado valido (.pfx/.p12).'
else:
detail = f'Detalhes: {err_msg}'
return JsonResponse({'success': False, 'error': f'Erro ao carregar certificado: {detail}'}, status=400)
finally:
if os.path.exists(tmp_cert_path):
os.unlink(tmp_cert_path)
cert_info = signer.signing_cert
error_response = _validar_certificado(cert_info)
if error_response:
data = json.loads(error_response.content)
return JsonResponse({'success': False, 'error': data.get('error', 'Certificado invalido.')}, status=400)
# -- Assinatura por documento --
docs = DocumentoAcessorio.objects.filter(pk__in=pks).select_related('materia', 'tipo', 'materia__tipo')
docs_map = {d.pk: d for d in docs}
resultados = []
sucesso_count = 0
erro_count = 0
for pk in pks:
doc = docs_map.get(pk)
if not doc:
resultados.append({'pk': pk, 'success': False, 'descricao': f'ID {pk}', 'error': 'Documento nao encontrado.'})
erro_count += 1
continue
descricao = f'{doc.nome} - {doc.materia.tipo.sigla} {doc.materia.numero}/{doc.materia.ano}'
if doc.pdf_assinado:
resultados.append({'pk': pk, 'success': False, 'descricao': descricao, 'error': 'Ja possui PDF assinado. Ignorado.'})
erro_count += 1
continue
assinaturas_existentes = _normalizar_assinatura_info(doc.assinatura_info)
if any(a.get('signed_by') == request.user.username for a in assinaturas_existentes):
resultados.append({'pk': pk, 'success': False, 'descricao': descricao, 'error': 'Voce ja assinou este documento.'})
erro_count += 1
continue
pdf_bytes, error = _gerar_pdf_do_docacessorio(doc, request)
if error:
resultados.append({'pk': pk, 'success': False, 'descricao': descricao, 'error': error})
erro_count += 1
continue
temp_stamped_path = None
try:
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from PyPDF4 import PdfFileReader, PdfFileWriter as PyPDF4Writer
nome_assinante, cargo, tipo_cert = _obter_info_assinante(request, cert_info)
data_assinatura = timezone.localtime(timezone.now())
data_formatada = data_assinatura.strftime('%d/%m/%Y %H:%M:%S')
data_simples = data_assinatura.strftime('%d/%m/%Y %H:%M')
codigo = _gerar_codigo_autenticacao(pdf_bytes)
url_verificacao = _construir_url_verificacao(request, 'docacessorio', pk, codigo)
nova_assinatura_info = {
'nome_assinante': nome_assinante,
'cargo': cargo,
'data_assinatura': data_simples,
}
original_pdf = PdfFileReader(io.BytesIO(pdf_bytes))
last_page = original_pdf.getPage(original_pdf.getNumPages() - 1)
page_box = last_page.mediaBox
page_width = float(page_box.getWidth())
page_height = float(page_box.getHeight())
auth_page_bytes = _gerar_pagina_autenticacao(
[nova_assinatura_info], codigo, url_verificacao,
page_width, page_height
)
auth_page_pdf = PdfFileReader(io.BytesIO(auth_page_bytes))
output_pdf = PyPDF4Writer()
for page_num in range(original_pdf.getNumPages()):
output_pdf.addPage(original_pdf.getPage(page_num))
output_pdf.addPage(auth_page_pdf.getPage(0))
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_f:
temp_stamped_path = tmp_f.name
output_pdf.write(tmp_f)
with open(temp_stamped_path, 'rb') as f:
stamped_bytes = f.read()
signed_buffer = io.BytesIO()
with io.BytesIO(stamped_bytes) as inf:
w = IncrementalPdfFileWriter(inf)
meta = signers.PdfSignatureMetadata(
field_name='AssinaturaDigital',
location='Camara Municipal',
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001',
name=nome_assinante
)
signers.sign_pdf(w, meta, signer=signer, output=signed_buffer)
signed_buffer.seek(0)
signed_pdf_content = signed_buffer.read()
filename = f"docacessorio_{doc.pk}_assinado_{int(timezone.now().timestamp())}.pdf"
doc.pdf_assinado.save(filename, ContentFile(signed_pdf_content), save=False)
doc.codigo_autenticacao = codigo
nova_assinatura_record = {
'tipo_certificado': 'A1',
'tipo_certificado_display': f'{tipo_cert} - A1',
'subject': str(cert_info.subject),
'issuer': str(cert_info.issuer),
'serial': str(cert_info.serial_number),
'valid_from': cert_info.not_valid_before.isoformat(),
'valid_to': cert_info.not_valid_after.isoformat(),
'signed_by': request.user.username,
'nome_assinante': nome_assinante,
'cargo': cargo,
'data_assinatura': data_formatada,
'validade_juridica': 'Assinatura Eletronica Qualificada'
}
assinaturas_existentes.append(nova_assinatura_record)
doc.assinatura_info = assinaturas_existentes
doc.assinado_em = timezone.now()
doc.assinado_por = request.user
doc.save()
logger.info(f"[lote-doc] DocAcessorio {pk} assinado por {request.user.username}")
resultados.append({'pk': pk, 'success': True, 'descricao': descricao})
sucesso_count += 1
except Exception as e:
logger.error(f"[lote-doc] Erro ao assinar docacessorio {pk}: {e}")
resultados.append({'pk': pk, 'success': False, 'descricao': descricao, 'error': str(e)})
erro_count += 1
finally:
if temp_stamped_path and os.path.exists(temp_stamped_path):
os.unlink(temp_stamped_path)
return JsonResponse({
'success': True,
'total': len(pks),
'sucesso': sucesso_count,
'erros': erro_count,
'resultados': resultados,
})
# =============================================================================
# Assinatura em Lote de Documentos Acessorios
# =============================================================================
@login_required
@csrf_exempt
@require_http_methods(["POST"])
def docacessorio_assinar_lote(request):
"""
Assina em lote documentos acessorios pendentes com certificado A1.
POST multipart:
- certificado: arquivo .pfx / .p12
- senha: senha do certificado
- ids: JSON array com os PKs dos documentos acessorios
Retorna JSON com resultado por documento.
"""
_tem_perm_django = request.user.has_perm('materia.change_documentoacessorio')
try:
_autor_lote = OperadorAutor.objects.get(user=request.user).autor
except OperadorAutor.DoesNotExist:
_autor_lote = None
if not (_tem_perm_django or _autor_lote or request.user.is_superuser):
return JsonResponse(
{'success': False, 'error': 'Sem permissao para assinar documentos acessorios.'},
status=403
)
ids_raw = request.POST.get('ids', '')
ids_multi = request.POST.getlist('ids[]')
if ids_multi:
pks = [int(i) for i in ids_multi if str(i).isdigit()]
elif ids_raw:
try:
parsed = json.loads(ids_raw)
pks = [int(i) for i in parsed if str(i).isdigit() or isinstance(i, int)]
except (json.JSONDecodeError, ValueError):
return JsonResponse(
{'success': False, 'error': 'Parametro "ids" invalido. Envie um array JSON.'},
status=400
)
else:
return JsonResponse({'success': False, 'error': 'Nenhum documento selecionado.'}, status=400)
if not pks:
return JsonResponse({'success': False, 'error': 'Lista de IDs vazia.'}, status=400)
if len(pks) > 200:
return JsonResponse(
{'success': False, 'error': 'Limite maximo de 200 documentos por lote.'},
status=400
)
certificado_file = request.FILES.get('certificado')
senha = request.POST.get('senha', '')
if not certificado_file:
return JsonResponse({'success': False, 'error': 'Certificado nao informado.'}, status=400)
if not senha:
return JsonResponse({'success': False, 'error': 'Senha do certificado nao informada.'}, status=400)
cert_bytes = certificado_file.read()
import tempfile as tmp_module
with tmp_module.NamedTemporaryFile(delete=False, suffix='.pfx') as tmp_cert:
tmp_cert.write(cert_bytes)
tmp_cert_path = tmp_cert.name
try:
from pyhanko.sign import signers
signer = signers.SimpleSigner.load_pkcs12(
pfx_file=tmp_cert_path,
passphrase=senha.encode('utf-8')
)
except Exception as cert_error:
logger.error(f"[lote-doc] Erro ao carregar certificado: {cert_error}")
err_msg = str(cert_error)
if 'password' in err_msg.lower() or 'mac' in err_msg.lower():
detail = 'Senha incorreta ou arquivo invalido.'
elif 'decode' in err_msg.lower() or 'parse' in err_msg.lower():
detail = 'Arquivo nao e um certificado valido (.pfx/.p12).'
else:
detail = f'Detalhes: {err_msg}'
return JsonResponse({'success': False, 'error': f'Erro ao carregar certificado: {detail}'}, status=400)
finally:
if os.path.exists(tmp_cert_path):
os.unlink(tmp_cert_path)
cert_info = signer.signing_cert
error_response = _validar_certificado(cert_info)
if error_response:
data = json.loads(error_response.content)
return JsonResponse({'success': False, 'error': data.get('error', 'Certificado invalido.')}, status=400)
docs = DocumentoAcessorio.objects.filter(pk__in=pks).select_related('materia', 'tipo', 'materia__tipo')
docs_map = {d.pk: d for d in docs}
resultados = []
sucesso_count = 0
erro_count = 0
for pk in pks:
doc = docs_map.get(pk)
if not doc:
resultados.append({'pk': pk, 'success': False, 'descricao': f'ID {pk}', 'error': 'Documento nao encontrado.'})
erro_count += 1
continue
descricao = f'{doc.nome} - {doc.materia.tipo.sigla} {doc.materia.numero}/{doc.materia.ano}'
if doc.pdf_assinado:
resultados.append({'pk': pk, 'success': False, 'descricao': descricao, 'error': 'Ja possui PDF assinado. Ignorado.'})
erro_count += 1
continue
assinaturas_existentes = _normalizar_assinatura_info(doc.assinatura_info)
if any(a.get('signed_by') == request.user.username for a in assinaturas_existentes):
resultados.append({'pk': pk, 'success': False, 'descricao': descricao, 'error': 'Voce ja assinou este documento.'})
erro_count += 1
continue
pdf_bytes, error = _gerar_pdf_do_docacessorio(doc, request)
if error:
resultados.append({'pk': pk, 'success': False, 'descricao': descricao, 'error': error})
erro_count += 1
continue
temp_stamped_path = None
try:
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from PyPDF4 import PdfFileReader, PdfFileWriter as PyPDF4Writer
nome_assinante, cargo, tipo_cert = _obter_info_assinante(request, cert_info)
data_assinatura = timezone.localtime(timezone.now())
data_formatada = data_assinatura.strftime('%d/%m/%Y %H:%M:%S')
data_simples = data_assinatura.strftime('%d/%m/%Y %H:%M')
codigo = _gerar_codigo_autenticacao(pdf_bytes)
url_verificacao = _construir_url_verificacao(request, 'docacessorio', pk, codigo)
nova_assinatura_info = {
'nome_assinante': nome_assinante,
'cargo': cargo,
'data_assinatura': data_simples,
}
original_pdf = PdfFileReader(io.BytesIO(pdf_bytes))
last_page = original_pdf.getPage(original_pdf.getNumPages() - 1)
page_box = last_page.mediaBox
page_width = float(page_box.getWidth())
page_height = float(page_box.getHeight())
auth_page_bytes = _gerar_pagina_autenticacao(
[nova_assinatura_info], codigo, url_verificacao,
page_width, page_height
)
auth_page_pdf = PdfFileReader(io.BytesIO(auth_page_bytes))
output_pdf = PyPDF4Writer()
for page_num in range(original_pdf.getNumPages()):
output_pdf.addPage(original_pdf.getPage(page_num))
output_pdf.addPage(auth_page_pdf.getPage(0))
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_f:
temp_stamped_path = tmp_f.name
output_pdf.write(tmp_f)
with open(temp_stamped_path, 'rb') as f:
stamped_bytes = f.read()
signed_buffer = io.BytesIO()
with io.BytesIO(stamped_bytes) as inf:
w = IncrementalPdfFileWriter(inf)
meta = signers.PdfSignatureMetadata(
field_name='AssinaturaDigital',
location='Camara Municipal',
reason='Documento assinado digitalmente nos termos da MP 2.200-2/2001',
name=nome_assinante
)
signers.sign_pdf(w, meta, signer=signer, output=signed_buffer)
signed_buffer.seek(0)
signed_pdf_content = signed_buffer.read()
filename = f"docacessorio_{doc.pk}_assinado_{int(timezone.now().timestamp())}.pdf"
doc.pdf_assinado.save(filename, ContentFile(signed_pdf_content), save=False)
doc.codigo_autenticacao = codigo
nova_assinatura_record = {
'tipo_certificado': 'A1',
'tipo_certificado_display': f'{tipo_cert} - A1',
'subject': str(cert_info.subject),
'issuer': str(cert_info.issuer),
'serial': str(cert_info.serial_number),
'valid_from': cert_info.not_valid_before.isoformat(),
'valid_to': cert_info.not_valid_after.isoformat(),
'signed_by': request.user.username,
'nome_assinante': nome_assinante,
'cargo': cargo,
'data_assinatura': data_formatada,
'validade_juridica': 'Assinatura Eletronica Qualificada'
}
assinaturas_existentes.append(nova_assinatura_record)
doc.assinatura_info = assinaturas_existentes
doc.assinado_em = timezone.now()
doc.assinado_por = request.user
doc.save()
logger.info(f"[lote-doc] DocAcessorio {pk} assinado por {request.user.username}")
resultados.append({'pk': pk, 'success': True, 'descricao': descricao})
sucesso_count += 1
except Exception as e:
logger.error(f"[lote-doc] Erro ao assinar docacessorio {pk}: {e}")
resultados.append({'pk': pk, 'success': False, 'descricao': descricao, 'error': str(e)})
erro_count += 1
finally:
if temp_stamped_path and os.path.exists(temp_stamped_path):
os.unlink(temp_stamped_path)
return JsonResponse({
'success': True,
'total': len(pks),
'sucesso': sucesso_count,
'erros': erro_count,
'resultados': resultados,
})

394
sapl/templates/materia/documentoacessorio_list.html

@ -58,6 +58,17 @@
<div class="actions btn-group float-right" role="group"> <div class="actions btn-group float-right" role="group">
<a href="{% url 'sapl.materia:compress_docacessorios' root_pk %}" class="btn btn-outline-primary">{% trans 'Baixar documentos compactados' %}</a> <a href="{% url 'sapl.materia:compress_docacessorios' root_pk %}" class="btn btn-outline-primary">{% trans 'Baixar documentos compactados' %}</a>
</div> </div>
{% if docs_pendentes_lote %}
<div class="actions btn-group float-right ml-2" role="group">
<button type="button" id="btn-assinar-doc-lote"
class="btn btn-outline-warning"
title="Assinar digitalmente os {{ docs_pendentes_lote|length }} documento(s) acessório(s) pendentes">
<i class="fa fa-certificate"></i>
Assinar em Lote
<span class="badge badge-warning text-dark ml-1">{{ docs_pendentes_lote|length }}</span>
</button>
</div>
{% endif %}
</div> </div>
{% if pode_upload %} {% if pode_upload %}
@ -169,4 +180,387 @@
})(); })();
</script> </script>
{% endif %} {% endif %}
{% if docs_pendentes_lote %}
{# ── Modal de Assinatura em Lote – Documentos Acessórios ─────────────── #}
<div class="modal fade" id="assinaturaDocLoteModal" tabindex="-1" role="dialog" aria-labelledby="assinaturaDocLoteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="assinaturaDocLoteModalLabel">
<i class="fa fa-certificate"></i>
Assinar Documentos Acessórios em Lote —
<strong id="doc-lote-titulo-total">{{ docs_pendentes_lote|length }}</strong> pendente(s)
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Fechar">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{# Passo 0: seleção #}
<div id="doc-lote-selecao-step">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="font-weight-bold">
<i class="fa fa-list-ul"></i> Selecione os documentos a assinar
</span>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary mr-1" id="doc-lote-btn-todas">
<i class="fa fa-check-square-o"></i> Todos
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="doc-lote-btn-nenhuma">
<i class="fa fa-square-o"></i> Nenhum
</button>
</div>
</div>
<div id="doc-lote-lista-checkboxes" style="max-height:320px;overflow-y:auto;border:1px solid #dee2e6;border-radius:4px;padding:8px;">
{% for d in docs_pendentes_lote %}
<div class="form-check py-1" style="border-bottom:1px solid #f0f0f0;">
<input class="form-check-input doc-lote-chk" type="checkbox"
id="doc-lote-chk-{{ d.id }}" value="{{ d.id }}" checked>
<label class="form-check-label w-100" for="doc-lote-chk-{{ d.id }}" style="cursor:pointer;">
{{ d.descricao }}
</label>
</div>
{% endfor %}
</div>
<p class="text-muted small mt-2 mb-0">
<span id="doc-lote-selecao-contagem">{{ docs_pendentes_lote|length }}</span> documento(s) selecionado(s)
</p>
</div>
{# Passo 1: certificado #}
<div id="doc-lote-form-step" style="display:none;">
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
<strong>Atenção:</strong> Esta operação assinará digitalmente
<strong><span id="doc-lote-alerta-total">{{ docs_pendentes_lote|length }}</span> documento(s)</strong>
com validade jurídica nos termos da MP 2.200-2/2001.
</div>
<form id="form-assinatura-doc-lote" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="doc-lote-certificado">
<i class="fa fa-file"></i> Certificado Digital (.pfx / .p12)
</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="doc-lote-certificado"
name="certificado" accept=".pfx,.p12" required>
<label class="custom-file-label" for="doc-lote-certificado">
Selecione o arquivo do certificado...
</label>
</div>
</div>
<div class="form-group">
<label for="doc-lote-senha">
<i class="fa fa-lock"></i> Senha do Certificado
</label>
<input type="password" class="form-control" id="doc-lote-senha"
name="senha" placeholder="Digite a senha do certificado" required>
</div>
</form>
</div>
{# Passo 1.5: prévia #}
<div id="doc-lote-previa-step" style="display:none;">
<div class="alert alert-info mb-3">
<i class="fa fa-eye"></i>
<strong>Revise os documentos antes de assinar.</strong>
Clique em cada item para visualizar o PDF em nova aba.
</div>
<div id="doc-lote-previa-lista" style="max-height:300px;overflow-y:auto;border:1px solid #dee2e6;border-radius:4px;padding:8px;" class="mb-3"></div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="doc-lote-previa-confirmacao">
<label class="form-check-label font-weight-bold text-danger" for="doc-lote-previa-confirmacao">
<i class="fa fa-check-square-o"></i>
Confirmo que revisei os documentos e que estão corretos para assinatura.
</label>
</div>
</div>
{# Passo 2: progresso #}
<div id="doc-lote-progresso-step" style="display:none;">
<h6 class="mb-3">
<i class="fa fa-spinner fa-spin text-primary"></i>
Processando assinaturas…
</h6>
<div class="progress mb-3" style="height:22px;">
<div id="doc-lote-progress-bar"
class="progress-bar progress-bar-striped progress-bar-animated bg-warning"
role="progressbar" style="width:0%">0%</div>
</div>
<p class="text-muted small" id="doc-lote-status-texto">Iniciando…</p>
</div>
{# Passo 3: resumo #}
<div id="doc-lote-resumo-step" style="display:none;">
<div id="doc-lote-resumo-alerta"></div>
<div id="doc-lote-resumo-lista" style="max-height:300px;overflow-y:auto;"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" id="doc-lote-btn-fechar">
<i class="fa fa-times"></i> Fechar
</button>
<button type="button" class="btn btn-warning" id="doc-lote-btn-proximo">
<i class="fa fa-arrow-right"></i>
Continuar — <span id="doc-lote-btn-proximo-contagem">{{ docs_pendentes_lote|length }}</span> Doc(s)
</button>
<button type="button" class="btn btn-outline-secondary" id="doc-lote-btn-voltar" style="display:none;">
<i class="fa fa-arrow-left"></i> Voltar
</button>
<button type="button" class="btn btn-info" id="doc-lote-btn-previa" style="display:none;">
<i class="fa fa-eye"></i> Visualizar Documentos
</button>
<button type="button" class="btn btn-outline-secondary" id="doc-lote-btn-voltar-form" style="display:none;">
<i class="fa fa-arrow-left"></i> Voltar
</button>
<button type="button" class="btn btn-warning" id="doc-lote-btn-assinar" style="display:none;" disabled>
<i class="fa fa-certificate"></i>
Assinar <span id="doc-lote-btn-assinar-contagem">{{ docs_pendentes_lote|length }}</span> Doc(s)
</button>
<button type="button" class="btn btn-success" id="doc-lote-btn-recarregar" style="display:none;"
onclick="location.reload()">
<i class="fa fa-refresh"></i> Atualizar página
</button>
</div>
</div>
</div>
</div>
<script>
(function () {
var DOCS_LOTE = {{ docs_pendentes_lote|safe }};
var URL_LOTE = "{% url 'sapl.materia:docacessorio_assinar_lote' %}";
function getCSRF() {
var el = document.querySelector('#form-assinatura-doc-lote [name=csrfmiddlewaretoken]');
if (el) return el.value;
var match = document.cookie.match(/csrftoken=([^;]+)/);
return match ? match[1] : '';
}
function getIdsSelecionados() {
return Array.from(document.querySelectorAll('.doc-lote-chk:checked'))
.map(function(c){ return parseInt(c.value, 10); });
}
function atualizarContagem() {
var n = getIdsSelecionados().length;
document.getElementById('doc-lote-selecao-contagem').textContent = n;
document.getElementById('doc-lote-btn-proximo-contagem').textContent = n;
document.getElementById('doc-lote-btn-assinar-contagem').textContent = n;
document.getElementById('doc-lote-alerta-total').textContent = n;
document.getElementById('doc-lote-titulo-total').textContent = n;
document.getElementById('doc-lote-btn-proximo').disabled = (n === 0);
}
document.getElementById('doc-lote-lista-checkboxes').addEventListener('change', function(e){
if (e.target && e.target.classList.contains('doc-lote-chk')) atualizarContagem();
});
document.getElementById('doc-lote-btn-todas').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = true; });
atualizarContagem();
});
document.getElementById('doc-lote-btn-nenhuma').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = false; });
atualizarContagem();
});
document.getElementById('doc-lote-certificado').addEventListener('change', function(){
var label = this.nextElementSibling;
label.textContent = this.files[0] ? this.files[0].name : 'Selecione o arquivo do certificado...';
});
function irParaSelecao() {
document.getElementById('doc-lote-selecao-step').style.display = 'block';
document.getElementById('doc-lote-form-step').style.display = 'none';
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'inline-block';
document.getElementById('doc-lote-btn-voltar').style.display = 'none';
document.getElementById('doc-lote-btn-previa').style.display = 'none';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
document.getElementById('doc-lote-btn-fechar').disabled = false;
}
function irParaForm() {
document.getElementById('doc-lote-selecao-step').style.display = 'none';
document.getElementById('doc-lote-form-step').style.display = 'block';
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'none';
document.getElementById('doc-lote-btn-voltar').style.display = 'inline-block';
document.getElementById('doc-lote-btn-previa').style.display = 'inline-block';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
}
function irParaPrevia() {
var certFile = document.getElementById('doc-lote-certificado').files[0];
var senha = document.getElementById('doc-lote-senha').value;
if (!certFile) { alert('Selecione o arquivo do certificado.'); return; }
if (!senha) { alert('Digite a senha do certificado.'); return; }
var idsSel = getIdsSelecionados();
var html = '';
DOCS_LOTE.filter(function(d){ return idsSel.indexOf(d.id) !== -1; })
.forEach(function(d) {
html += '<div class="d-flex align-items-center justify-content-between py-2" ' +
'style="border-bottom:1px solid #f0f0f0;">' +
'<span><i class="fa fa-file-pdf-o text-danger mr-1"></i>' +
'<strong>' + escHtml(d.descricao) + '</strong></span>' +
'<a href="/materia/documentoacessorio/' + d.id + '/pdf-previa" target="_blank" ' +
'class="btn btn-sm btn-outline-primary ml-2" title="Visualizar PDF">' +
'<i class="fa fa-eye"></i> Visualizar</a>' +
'</div>';
});
document.getElementById('doc-lote-previa-lista').innerHTML = html || '<p class="text-muted">Nenhum documento selecionado.</p>';
var chk = document.getElementById('doc-lote-previa-confirmacao');
chk.checked = false;
document.getElementById('doc-lote-btn-assinar').disabled = true;
document.getElementById('doc-lote-selecao-step').style.display = 'none';
document.getElementById('doc-lote-form-step').style.display = 'none';
document.getElementById('doc-lote-previa-step').style.display = 'block';
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'none';
document.getElementById('doc-lote-btn-proximo').style.display = 'none';
document.getElementById('doc-lote-btn-voltar').style.display = 'none';
document.getElementById('doc-lote-btn-previa').style.display = 'none';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'inline-block';
document.getElementById('doc-lote-btn-assinar').style.display = 'inline-block';
document.getElementById('doc-lote-btn-recarregar').style.display = 'none';
}
document.getElementById('btn-assinar-doc-lote').addEventListener('click', function(){
document.querySelectorAll('.doc-lote-chk').forEach(function(c){ c.checked = true; });
document.getElementById('form-assinatura-doc-lote').reset();
document.querySelector('#form-assinatura-doc-lote .custom-file-label').textContent =
'Selecione o arquivo do certificado...';
atualizarContagem();
irParaSelecao();
$('#assinaturaDocLoteModal').modal('show');
});
document.getElementById('doc-lote-btn-proximo').addEventListener('click', function(){
if (getIdsSelecionados().length === 0) { alert('Selecione ao menos um documento.'); return; }
irParaForm();
});
document.getElementById('doc-lote-btn-voltar').addEventListener('click', irParaSelecao);
document.getElementById('doc-lote-btn-previa').addEventListener('click', irParaPrevia);
document.getElementById('doc-lote-btn-voltar-form').addEventListener('click', irParaForm);
document.getElementById('doc-lote-previa-confirmacao').addEventListener('change', function(){
document.getElementById('doc-lote-btn-assinar').disabled = !this.checked;
});
document.getElementById('doc-lote-btn-assinar').addEventListener('click', function(){
var certFile = document.getElementById('doc-lote-certificado').files[0];
var senha = document.getElementById('doc-lote-senha').value;
var idsSel = getIdsSelecionados();
if (!certFile) { alert('Selecione o arquivo do certificado.'); return; }
if (!senha) { alert('Digite a senha do certificado.'); return; }
if (!idsSel.length) { alert('Nenhum documento selecionado.'); return; }
var fd = new FormData();
fd.append('certificado', certFile);
fd.append('senha', senha);
fd.append('ids', JSON.stringify(idsSel));
document.getElementById('doc-lote-previa-step').style.display = 'none';
document.getElementById('doc-lote-progresso-step').style.display = 'block';
document.getElementById('doc-lote-btn-voltar-form').style.display= 'none';
document.getElementById('doc-lote-btn-assinar').style.display = 'none';
document.getElementById('doc-lote-btn-fechar').disabled = true;
setProgresso(5, 'Enviando certificado e iniciando assinatura…');
fetch(URL_LOTE, {
method: 'POST',
body: fd,
headers: { 'X-CSRFToken': getCSRF() }
})
.then(function(r){
setProgresso(90, 'Processando resposta…');
if (!r.ok) return r.json().then(function(d){ throw new Error(d.error || ('HTTP ' + r.status)); });
return r.json();
})
.then(function(data){
setProgresso(100, 'Concluído.');
document.getElementById('doc-lote-btn-fechar').disabled = false;
mostrarResumo(data);
})
.catch(function(err){
document.getElementById('doc-lote-btn-fechar').disabled = false;
setProgresso(100, 'Erro.');
mostrarErroFatal(err.message || String(err));
});
});
function setProgresso(pct, texto) {
var bar = document.getElementById('doc-lote-progress-bar');
bar.style.width = pct + '%';
bar.textContent = pct + '%';
document.getElementById('doc-lote-status-texto').textContent = texto;
}
function mostrarResumo(data) {
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'block';
document.getElementById('doc-lote-btn-recarregar').style.display = 'inline-block';
var temErro = data.erros > 0;
var alertaCls = data.sucesso > 0 ? (temErro ? 'alert-warning' : 'alert-success') : 'alert-danger';
var icone = data.sucesso > 0 ? (temErro ? 'exclamation-triangle' : 'check-circle') : 'times-circle';
document.getElementById('doc-lote-resumo-alerta').innerHTML =
'<div class="alert ' + alertaCls + '">' +
'<i class="fa fa-' + icone + '"></i> ' +
'<strong>' + data.sucesso + ' assinado(s)</strong> com sucesso' +
(temErro ? ', <strong>' + data.erros + '</strong> com erro(s).' : '.') +
' Total: ' + data.total + ' documento(s).</div>';
var html = '<ul class="list-group">';
(data.resultados || []).forEach(function(r){
var cls = r.success ? 'list-group-item-success' : 'list-group-item-danger';
var icon = r.success ? 'check text-success' : 'times text-danger';
html += '<li class="list-group-item list-group-item-sm ' + cls + '">' +
'<i class="fa fa-' + icon + ' mr-1"></i>' +
'<strong>' + escHtml(r.descricao) + '</strong>' +
(r.error ? ' — <small class="text-muted">' + escHtml(r.error) + '</small>' : '') +
'</li>';
});
html += '</ul>';
document.getElementById('doc-lote-resumo-lista').innerHTML = html;
}
function mostrarErroFatal(msg) {
document.getElementById('doc-lote-progresso-step').style.display = 'none';
document.getElementById('doc-lote-resumo-step').style.display = 'block';
document.getElementById('doc-lote-resumo-alerta').innerHTML =
'<div class="alert alert-danger"><i class="fa fa-times-circle"></i> ' +
'<strong>Erro:</strong> ' + escHtml(msg) + '</div>';
document.getElementById('doc-lote-resumo-lista').innerHTML = '';
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
})();
</script>
{% endif %}
{% endblock %} {% endblock %}

Loading…
Cancel
Save