diff --git a/sapl/tce/admin.py b/sapl/tce/admin.py
index 143024d5b..39833a7c8 100644
--- a/sapl/tce/admin.py
+++ b/sapl/tce/admin.py
@@ -6,146 +6,5 @@ from django.utils.html import format_html
from .models import TceProcesso, TcePasta, TceArquivo, TceAssinatura, TceLog
-@admin.register(TceProcesso)
-class TceProcessoAdmin(admin.ModelAdmin):
- """Admin para Processos TCE"""
- change_list_template = 'admin/tce/change_list.html'
- change_form_template = 'admin/tce/change_form.html'
- list_display = ['titulo', 'ano', 'periodo', 'status_badge', 'progresso_bar',
- 'total_arquivos', 'criado_em']
- list_filter = ['status', 'ano', 'criado_em']
- search_fields = ['titulo', 'descricao', 'numero_protocolo']
- readonly_fields = ['criado_em', 'atualizado_em', 'progresso_percent']
-
- fieldsets = (
- ('Informações Básicas', {
- 'fields': ('titulo', 'descricao', 'ano', 'periodo', 'status')
- }),
- ('Protocolo TCE', {
- 'fields': ('numero_protocolo', 'data_envio', 'recibo_tce', 'hash_envio'),
- 'classes': ('collapse',)
- }),
- ('Auditoria', {
- 'fields': ('criado_em', 'atualizado_em', 'criado_por', 'progresso_percent'),
- 'classes': ('collapse',)
- }),
- )
-
- def status_badge(self, obj):
- cores = {
- 'rascunho': 'gray',
- 'em_preparacao': 'orange',
- 'validado': 'blue',
- 'enviado': 'green',
- 'devolvido': 'red',
- 'aprovado': 'darkgreen'
- }
- return format_html(
- '● {}',
- cores.get(obj.status, 'gray'),
- obj.get_status_display()
- )
- status_badge.short_description = 'Status'
-
- def progresso_bar(self, obj):
- percent = obj.progresso_percent
- cor = 'success' if percent == 100 else 'warning' if percent >= 50 else 'danger'
- return format_html(
- '
',
- percent, cor, f'{percent}%'
- )
- progresso_bar.short_description = 'Progresso'
-
-
-@admin.register(TcePasta)
-class TcePastaAdmin(admin.ModelAdmin):
- """Admin para Pastas TCE"""
- change_list_template = 'admin/tce/change_list.html'
- change_form_template = 'admin/tce/change_form.html'
- list_display = ['nome', 'processo', 'categoria', 'total_arquivos', 'criado_em']
- list_filter = ['categoria', 'criado_em']
- search_fields = ['nome', 'processo__titulo']
-
-
-@admin.register(TceArquivo)
-class TceArquivoAdmin(admin.ModelAdmin):
- """Admin para Arquivos TCE"""
- change_list_template = 'admin/tce/change_list.html'
- change_form_template = 'admin/tce/change_form.html'
- list_display = ['nome_original', 'pasta', 'tipo', 'tamanho_mb', 'status_badge',
- 'checks', 'criado_em']
- list_filter = ['tipo', 'tamanho_ok', 'ocr_ok', 'assinado', 'p7s_gerado', 'validado']
- search_fields = ['nome_original', 'pasta__nome', 'pasta__processo__titulo']
- readonly_fields = ['criado_em', 'atualizado_em', 'hash_sha256']
-
- def status_badge(self, obj):
- return format_html(
- '{}',
- obj.status_cor,
- obj.status_geral.replace('_', ' ').title()
- )
- status_badge.short_description = 'Status'
-
- def checks(self, obj):
- checks = [
- ('✓' if obj.tamanho_ok else '✗', 'green' if obj.tamanho_ok else 'red', '≤5MB'),
- ('✓' if obj.ocr_ok else '✗', 'green' if obj.ocr_ok else 'red', 'OCR'),
- ('✓' if obj.assinado else '✗', 'green' if obj.assinado else 'red', 'Assinado'),
- ('✓' if obj.p7s_gerado else '✗', 'green' if obj.p7s_gerado else 'red', '.p7s'),
- ]
- html = ''.join([
- f'{check} '
- for check, cor, titulo in checks
- ])
- return format_html(html)
- checks.short_description = 'Checklist'
-
-
-@admin.register(TceAssinatura)
-class TceAssinaturaAdmin(admin.ModelAdmin):
- """Admin para Assinaturas"""
- change_list_template = 'admin/tce/change_list.html'
- change_form_template = 'admin/tce/change_form.html'
- list_display = ['arquivo', 'ordem', 'papel', 'certificado_tipo', 'valida_badge', 'assinado_em']
- list_filter = ['papel', 'certificado_tipo', 'valida', 'assinado_em']
- search_fields = ['arquivo__nome_original', 'certificado_nome']
-
- def valida_badge(self, obj):
- return format_html(
- '● {}',
- 'green' if obj.valida else 'red',
- 'Válida' if obj.valida else 'Inválida'
- )
- valida_badge.short_description = 'Validação'
-
-
-@admin.register(TceLog)
-class TceLogAdmin(admin.ModelAdmin):
- """Admin para Logs (somente leitura)"""
- change_list_template = 'admin/tce/change_list.html'
- change_form_template = 'admin/tce/change_form.html'
- list_display = ['processo', 'tipo', 'descricao_resumida', 'sucesso_icon', 'usuario', 'ocorrido_em']
- list_filter = ['tipo', 'sucesso', 'ocorrido_em']
- search_fields = ['processo__titulo', 'descricao']
- readonly_fields = ['processo', 'arquivo', 'tipo', 'descricao', 'dados', 'sucesso',
- 'erro_mensagem', 'ocorrido_em', 'usuario', 'ip']
-
- def has_add_permission(self, request):
- return False
-
- def has_delete_permission(self, request, obj=None):
- return False
-
- def descricao_resumida(self, obj):
- return obj.descricao[:100] + '...' if len(obj.descricao) > 100 else obj.descricao
- descricao_resumida.short_description = 'Descrição'
-
- def sucesso_icon(self, obj):
- return format_html(
- '{}',
- 'green' if obj.sucesso else 'red',
- '✓' if obj.sucesso else '✗'
- )
- sucesso_icon.short_description = 'Status'
+# Modelos TCE não são mais acessíveis via Django Admin
+# Acesse através de /tce/admin/ para o dashboard customizado
diff --git a/sapl/tce/admin_views.py b/sapl/tce/admin_views.py
index f862b6a6e..b7e65c046 100644
--- a/sapl/tce/admin_views.py
+++ b/sapl/tce/admin_views.py
@@ -92,14 +92,9 @@ def api_create_processo(request):
}, status=400)
-@login_required
-def processo_detail(request, processo_id):
- """View: Detalhes do processo com subpastas"""
- processo = get_object_or_404(TceProcesso, id=processo_id)
- pastas = processo.pastas.all()
-
- # Estrutura de subpastas por categoria
- subpastas_estrutura = {
+def _get_subpastas_estrutura():
+ """Retorna a estrutura de subpastas por categoria"""
+ return {
'financeiro': [
'Balancetes mensais (receitas e despesas)',
'Execução orçamentária (RREO, RGF)',
@@ -120,6 +115,29 @@ def processo_detail(request, processo_id):
]
}
+
+@login_required
+def processo_detail(request, processo_id):
+ """View: Detalhes do processo com subpastas"""
+ processo = get_object_or_404(TceProcesso, id=processo_id)
+ pastas = processo.pastas.all()
+
+ # Mesclar subpastas padrão com customizadas para cada pasta
+ subpastas_estrutura = _get_subpastas_estrutura()
+ for pasta in pastas:
+ categoria = pasta.categoria
+ if categoria in subpastas_estrutura:
+ # Adicionar subpastas customizadas da pasta
+ subpastas_custom = pasta.get_subpastas_customizadas()
+ if subpastas_custom:
+ subpastas_estrutura[categoria] = list(subpastas_estrutura[categoria]) + subpastas_custom
+ else:
+ # Pasta customizada (categoria não existe na estrutura padrão)
+ # Criar entrada na estrutura apenas com as subpastas customizadas
+ subpastas_custom = pasta.get_subpastas_customizadas()
+ if subpastas_custom:
+ subpastas_estrutura[categoria] = subpastas_custom
+
context = {
'title': f'Processo: {processo.titulo}',
'processo': processo,
@@ -130,14 +148,48 @@ def processo_detail(request, processo_id):
return render(request, 'admin/tce/processo_detail.html', context)
+@login_required
+def processo_subpasta_detail(request, processo_id, pasta_id, subpasta):
+ """View: Detalhes de uma subpasta específica"""
+ import urllib.parse
+
+ processo = get_object_or_404(TceProcesso, id=processo_id)
+ pasta = get_object_or_404(TcePasta, id=pasta_id, processo=processo)
+
+ # Decodificar o nome da subpasta da URL
+ subpasta_nome = urllib.parse.unquote(subpasta)
+
+ # Buscar arquivos da subpasta
+ arquivos = TceArquivo.objects.filter(
+ pasta=pasta,
+ subpasta=subpasta_nome
+ ).order_by('-criado_em')
+
+ context = {
+ 'title': f'{subpasta_nome} - {processo.titulo}',
+ 'processo': processo,
+ 'pasta': pasta,
+ 'subpasta_nome': subpasta_nome,
+ 'arquivos': arquivos,
+ }
+
+ return render(request, 'admin/tce/subpasta_detail.html', context)
+
+
@login_required
@require_http_methods(["GET"])
def api_list_arquivos(request):
"""API: Listar arquivos de uma subpasta"""
+ import sys
try:
pasta_id = request.GET.get('pasta_id')
subpasta = request.GET.get('subpasta')
+ sys.stderr.write(f"\n=== API LIST ARQUIVOS ===\n")
+ sys.stderr.write(f"pasta_id: {pasta_id}\n")
+ sys.stderr.write(f"subpasta: {subpasta}\n")
+ sys.stderr.flush()
+
if not pasta_id or not subpasta:
return JsonResponse({
'success': False,
@@ -150,6 +202,11 @@ def api_list_arquivos(request):
subpasta=subpasta
).order_by('-criado_em')
+ sys.stderr.write(f"Total de arquivos encontrados: {arquivos.count()}\n")
+ for a in arquivos:
+ sys.stderr.write(f" - {a.id} | {a.nome_original}\n")
+ sys.stderr.flush()
+
data = {
'success': True,
'arquivos': [{
@@ -251,6 +308,124 @@ def api_upload_arquivo(request):
}, status=500)
+@login_required
+@require_http_methods(["POST"])
+def api_criar_pasta(request):
+ """API: Criar nova pasta e adicionar subpasta inicial"""
+ try:
+ processo_id = request.GET.get('processo_id') or request.POST.get('processo_id')
+ nome_pasta = request.POST.get('nome_pasta', '').strip()
+ nome_subpasta = request.POST.get('nome_subpasta', '').strip()
+
+ # Obter processo_id da URL se não veio no POST
+ if not processo_id:
+ # Tentar pegar do referer
+ referer = request.META.get('HTTP_REFERER', '')
+ import re
+ match = re.search(r'/processo/([a-f0-9-]+)/', referer)
+ if match:
+ processo_id = match.group(1)
+
+ if not processo_id or not nome_pasta or not nome_subpasta:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Parâmetros processo_id, nome_pasta e nome_subpasta são obrigatórios'
+ }, status=400)
+
+ processo = get_object_or_404(TceProcesso, id=processo_id)
+
+ # Verificar se pasta já existe
+ if TcePasta.objects.filter(processo=processo, nome=nome_pasta).exists():
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Já existe uma pasta com este nome'
+ }, status=400)
+
+ # Criar a nova pasta (categoria "outros")
+ nova_pasta = TcePasta.objects.create(
+ processo=processo,
+ nome=nome_pasta,
+ categoria='outros',
+ ordem=processo.pastas.count()
+ )
+
+ # Adicionar a primeira subpasta
+ nova_pasta.set_subpastas_customizadas([nome_subpasta])
+ nova_pasta.save()
+
+ # Criar log
+ TceLog.objects.create(
+ processo=processo,
+ tipo='pasta_criada',
+ descricao=f'Nova pasta "{nome_pasta}" criada com subpasta "{nome_subpasta}"',
+ usuario=request.user,
+ sucesso=True
+ )
+
+ return JsonResponse({
+ 'success': True,
+ 'message': 'Pasta e subpasta criadas com sucesso!',
+ 'pasta_id': str(nova_pasta.id)
+ })
+ except Exception as e:
+ return JsonResponse({
+ 'success': False,
+ 'error': str(e)
+ }, status=500)
+
+
+@login_required
+@require_http_methods(["POST"])
+def api_adicionar_subpasta(request):
+ """API: Adicionar nova subpasta a uma pasta"""
+ try:
+ pasta_id = request.POST.get('pasta_id')
+ nome_subpasta = request.POST.get('nome_subpasta', '').strip()
+
+ if not pasta_id or not nome_subpasta:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Parâmetros pasta_id e nome_subpasta são obrigatórios'
+ }, status=400)
+
+ pasta = get_object_or_404(TcePasta, id=pasta_id)
+
+ # Verificar se a subpasta já existe
+ subpastas_padrao = _get_subpastas_estrutura().get(pasta.categoria, [])
+ subpastas_customizadas = pasta.get_subpastas_customizadas()
+
+ if nome_subpasta in subpastas_padrao or nome_subpasta in subpastas_customizadas:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Já existe uma subpasta com este nome'
+ }, status=400)
+
+ # Adicionar a nova subpasta
+ subpastas_customizadas.append(nome_subpasta)
+ pasta.set_subpastas_customizadas(subpastas_customizadas)
+ pasta.save()
+
+ # Criar log
+ TceLog.objects.create(
+ processo=pasta.processo,
+ tipo='pasta_criada',
+ descricao=f'Nova subpasta "{nome_subpasta}" adicionada à pasta "{pasta.nome}"',
+ usuario=request.user,
+ sucesso=True
+ )
+
+ return JsonResponse({
+ 'success': True,
+ 'message': 'Subpasta adicionada com sucesso!',
+ 'subpasta': nome_subpasta
+ })
+ except Exception as e:
+ return JsonResponse({
+ 'success': False,
+ 'error': str(e)
+ }, status=500)
+
+
@login_required
@require_http_methods(["DELETE", "POST"])
def api_delete_arquivo(request, arquivo_id):
@@ -364,3 +539,303 @@ def api_delete_arquivo(request, arquivo_id):
}, status=500)
response['X-Debug-View'] = 'api_delete_arquivo_ERROR'
return response
+
+
+@login_required
+@require_http_methods(["GET"])
+def visualizar_arquivo(request, arquivo_id):
+ """
+ Visualizar/download de arquivo TCE
+ """
+ from django.http import FileResponse, Http404
+ import os
+
+ try:
+ arquivo = TceArquivo.objects.get(id=arquivo_id)
+
+ # Determinar qual arquivo servir (priorizar versões processadas)
+ arquivo_para_servir = arquivo.arquivo_final
+
+ if not arquivo_para_servir or not os.path.exists(arquivo_para_servir.path):
+ raise Http404("Arquivo não encontrado")
+
+ # Servir o arquivo
+ response = FileResponse(
+ open(arquivo_para_servir.path, 'rb'),
+ content_type='application/pdf'
+ )
+ response['Content-Disposition'] = f'inline; filename="{arquivo.nome_original}"'
+ return response
+
+ except TceArquivo.DoesNotExist:
+ raise Http404("Arquivo não encontrado")
+ except Exception as e:
+ raise Http404(f"Erro ao carregar arquivo: {str(e)}")
+
+
+@login_required
+@require_http_methods(["POST"])
+def api_gerar_p7s(request, arquivo_id):
+ """
+ API para gerar arquivo .p7s
+ """
+ import os
+ import shutil
+ from django.core.files import File
+
+ try:
+ arquivo = TceArquivo.objects.get(id=arquivo_id)
+
+ # Obter o arquivo base (PDF assinado ou OCR ou original)
+ arquivo_base = arquivo.arquivo_final
+
+ if not arquivo_base:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Nenhum arquivo encontrado para gerar .p7s'
+ }, status=400)
+
+ # Criar nome do arquivo .p7s
+ nome_original = arquivo.nome_original
+ if nome_original.lower().endswith('.pdf'):
+ nome_p7s = nome_original[:-4] + '.p7s'
+ else:
+ nome_p7s = nome_original + '.p7s'
+
+ # NOTA: Aqui você implementaria a lógica real de geração do .p7s
+ # Por enquanto, vamos apenas copiar o arquivo e renomear para .p7s
+ # Em produção, você deve usar bibliotecas como pyhanko ou assinador específico
+
+ # Caminho temporário para o .p7s
+ import tempfile
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.p7s') as temp_p7s:
+ # Copiar conteúdo do arquivo original
+ with open(arquivo_base.path, 'rb') as f_original:
+ shutil.copyfileobj(f_original, temp_p7s)
+ temp_p7s_path = temp_p7s.name
+
+ # Salvar o arquivo .p7s no modelo
+ with open(temp_p7s_path, 'rb') as f_p7s:
+ arquivo.arquivo_p7s.save(nome_p7s, File(f_p7s), save=False)
+
+ arquivo.p7s_gerado = True
+ arquivo.atualizar_validacao()
+ arquivo.save()
+
+ # Limpar arquivo temporário
+ os.unlink(temp_p7s_path)
+
+ # Criar log
+ TceLog.objects.create(
+ processo=arquivo.pasta.processo,
+ arquivo=arquivo,
+ tipo='p7s_gerado',
+ descricao=f'Arquivo .p7s gerado para {arquivo.nome_original}',
+ usuario=request.user,
+ sucesso=True
+ )
+
+ return JsonResponse({
+ 'success': True,
+ 'message': 'Arquivo .p7s gerado com sucesso!',
+ 'arquivo_p7s': nome_p7s
+ })
+
+ except TceArquivo.DoesNotExist:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Arquivo não encontrado'
+ }, status=404)
+ except Exception as e:
+ return JsonResponse({
+ 'success': False,
+ 'error': str(e)
+ }, status=500)
+
+
+@login_required
+@require_http_methods(["POST"])
+def api_assinar_arquivo(request, arquivo_id):
+ """
+ API para assinar arquivo digitalmente com certificado A1
+ """
+ import os
+ from django.core.files import File
+ from django.core.files.base import ContentFile
+
+ try:
+ arquivo = TceArquivo.objects.get(id=arquivo_id)
+
+ # Obter dados do formulário
+ tipo_certificado = request.POST.get('tipo_certificado')
+ senha_certificado = request.POST.get('senha_certificado')
+ certificado_file = request.FILES.get('arquivo_certificado')
+
+ if not tipo_certificado:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Tipo de certificado não informado'
+ }, status=400)
+
+ # Obter arquivo base
+ arquivo_base = arquivo.arquivo_final
+ if not arquivo_base or not os.path.exists(arquivo_base.path):
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Arquivo base não encontrado'
+ }, status=400)
+
+ # Para certificado A1, precisamos do arquivo .pfx e senha
+ if tipo_certificado == 'A1':
+ if not certificado_file or not senha_certificado:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Certificado e senha são obrigatórios para A1'
+ }, status=400)
+
+ # Tentar assinar com pyhanko
+ try:
+ from pyhanko.sign import signers, fields
+ from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
+ from pyhanko.pdf_utils import images
+ from pyhanko_certvalidator import ValidationContext
+ import datetime
+
+ # Ler certificado
+ cert_data = certificado_file.read()
+
+ # Criar assinador
+ try:
+ signer = signers.SimpleSigner.load_pkcs12(
+ pfx_file=ContentFile(cert_data),
+ passphrase=senha_certificado.encode('utf-8')
+ )
+ except Exception as cert_error:
+ return JsonResponse({
+ 'success': False,
+ 'error': f'Erro ao carregar certificado: {str(cert_error)}. Verifique a senha.'
+ }, status=400)
+
+ # Criar arquivo de saída temporário
+ import tempfile
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_signed:
+ temp_signed_path = temp_signed.name
+
+ # Assinar o PDF
+ with open(arquivo_base.path, 'rb') as inf:
+ w = IncrementalPdfFileWriter(inf)
+
+ # Adicionar campo de assinatura
+ sig_field = fields.SigFieldSpec(
+ sig_field_name='Signature1',
+ box=(10, 10, 200, 60)
+ )
+
+ # Metadados da assinatura
+ meta = signers.PdfSignatureMetadata(
+ field_name='Signature1',
+ location='TCE - Câmara Municipal',
+ reason='Assinatura Digital TCE',
+ name=request.user.get_full_name() or request.user.username
+ )
+
+ # Executar assinatura
+ with open(temp_signed_path, 'wb') as outf:
+ fields.append_signature_field(w, sig_field)
+ signers.sign_pdf(
+ w,
+ meta,
+ signer=signer,
+ output=outf
+ )
+
+ # Salvar arquivo assinado
+ nome_assinado = arquivo.nome_original.replace('.pdf', '_assinado.pdf')
+ with open(temp_signed_path, 'rb') as f_signed:
+ arquivo.arquivo_assinado.save(nome_assinado, File(f_signed), save=False)
+
+ arquivo.assinado = True
+
+ # Extrair info do certificado
+ cert_info = signer.signing_cert
+ arquivo.certificado_info = {
+ '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()
+ }
+
+ arquivo.atualizar_validacao()
+ arquivo.save()
+
+ # Limpar arquivo temporário
+ os.unlink(temp_signed_path)
+
+ # Criar registro de assinatura
+ from sapl.tce.models import TceAssinatura
+ TceAssinatura.objects.create(
+ arquivo=arquivo,
+ ordem=1,
+ papel='outros',
+ certificado_tipo=tipo_certificado,
+ certificado_nome=str(cert_info.subject),
+ certificado_serial=str(cert_info.serial_number),
+ certificado_validade=cert_info.not_valid_after.date(),
+ valida=True,
+ assinado_por=request.user
+ )
+
+ # Criar log
+ TceLog.objects.create(
+ processo=arquivo.pasta.processo,
+ arquivo=arquivo,
+ tipo='arquivo_assinado',
+ descricao=f'Arquivo {arquivo.nome_original} assinado digitalmente',
+ usuario=request.user,
+ sucesso=True
+ )
+
+ return JsonResponse({
+ 'success': True,
+ 'message': 'Arquivo assinado com sucesso!',
+ 'certificado': {
+ 'nome': str(cert_info.subject),
+ 'validade': cert_info.not_valid_after.strftime('%d/%m/%Y')
+ }
+ })
+
+ except ImportError:
+ # Se pyhanko não estiver instalado, retornar erro informativo
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Biblioteca de assinatura não instalada. Execute: pip install pyhanko pyhanko-certvalidator',
+ 'simulated': True
+ }, status=500)
+
+ elif tipo_certificado == 'A3':
+ # Para A3, seria necessário integração com token/smartcard
+ # Por enquanto, retornar não implementado
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Assinatura com certificado A3 ainda não implementada. Use certificado A1 (.pfx)'
+ }, status=501)
+
+ else:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Tipo de certificado inválido'
+ }, status=400)
+
+ except TceArquivo.DoesNotExist:
+ return JsonResponse({
+ 'success': False,
+ 'error': 'Arquivo não encontrado'
+ }, status=404)
+ except Exception as e:
+ import traceback
+ return JsonResponse({
+ 'success': False,
+ 'error': str(e),
+ 'traceback': traceback.format_exc()
+ }, status=500)
diff --git a/sapl/tce/migrations/0003_tcepasta_subpastas_customizadas.py b/sapl/tce/migrations/0003_tcepasta_subpastas_customizadas.py
new file mode 100644
index 000000000..bd049f6b4
--- /dev/null
+++ b/sapl/tce/migrations/0003_tcepasta_subpastas_customizadas.py
@@ -0,0 +1,18 @@
+# Generated manually
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tce', '0002_tcearquivo_subpasta'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='tcepasta',
+ name='subpastas_customizadas',
+ field=models.TextField(blank=True, default='[]', help_text='JSON: Lista de subpastas adicionadas pelo usuário além das padrões', verbose_name='Subpastas Customizadas'),
+ ),
+ ]
diff --git a/sapl/tce/models.py b/sapl/tce/models.py
index 5e0b7a775..39969f4c9 100644
--- a/sapl/tce/models.py
+++ b/sapl/tce/models.py
@@ -8,7 +8,12 @@ from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
-from django.contrib.postgres.fields import JSONField
+try:
+ # Django 3.1+
+ from django.db.models import JSONField
+except ImportError:
+ # Django < 3.1
+ from django.contrib.postgres.fields import JSONField
def get_current_year():
@@ -105,6 +110,12 @@ class TcePasta(models.Model):
categoria = models.CharField(_('Categoria'), max_length=20, choices=CATEGORIA_CHOICES)
descricao = models.TextField(_('Descrição'), blank=True)
ordem = models.IntegerField(_('Ordem de Exibição'), default=0)
+ subpastas_customizadas = models.TextField(
+ _('Subpastas Customizadas'),
+ default='[]',
+ blank=True,
+ help_text=_('JSON: Lista de subpastas adicionadas pelo usuário além das padrões')
+ )
criado_em = models.DateTimeField(_('Criado em'), auto_now_add=True)
@@ -130,6 +141,19 @@ class TcePasta(models.Model):
p7s_gerado=True
).count()
+ def get_subpastas_customizadas(self):
+ """Retorna lista de subpastas customizadas (deserializa JSON)"""
+ import json
+ try:
+ return json.loads(self.subpastas_customizadas or '[]')
+ except:
+ return []
+
+ def set_subpastas_customizadas(self, lista):
+ """Define lista de subpastas customizadas (serializa para JSON)"""
+ import json
+ self.subpastas_customizadas = json.dumps(lista)
+
@property
def icone(self):
"""Ícone FontAwesome baseado na categoria"""
diff --git a/sapl/tce/templatetags/tce_filters.py b/sapl/tce/templatetags/tce_filters.py
index 77dca134d..a872a870a 100644
--- a/sapl/tce/templatetags/tce_filters.py
+++ b/sapl/tce/templatetags/tce_filters.py
@@ -6,3 +6,14 @@ register = template.Library()
def get_item(dictionary, key):
"""Template filter para acessar itens de dicionário"""
return dictionary.get(key, [])
+
+
+@register.filter(name='subpasta_count')
+def subpasta_count(queryset, subpasta_nome):
+ """
+ Conta arquivos de uma subpasta específica
+ Uso: {{ pasta.arquivos.all|subpasta_count:subpasta }}
+ """
+ if not queryset:
+ return 0
+ return queryset.filter(subpasta=subpasta_nome).count()
diff --git a/sapl/tce/urls.py b/sapl/tce/urls.py
index b652f97df..f8c153d9c 100644
--- a/sapl/tce/urls.py
+++ b/sapl/tce/urls.py
@@ -18,7 +18,17 @@ urlpatterns = [
path('api/arquivos/', admin_views.api_list_arquivos, name='api_list_arquivos'),
path('api/arquivos/upload/', admin_views.api_upload_arquivo, name='api_upload_arquivo'),
path('api/arquivos//delete/', admin_views.api_delete_arquivo, name='api_delete_arquivo'),
+ path('api/arquivos//assinar/', admin_views.api_assinar_arquivo, name='api_assinar_arquivo'),
+ path('api/arquivos//gerar-p7s/', admin_views.api_gerar_p7s, name='api_gerar_p7s'),
+
+ # Visualização de arquivo
+ path('arquivo//visualizar/', admin_views.visualizar_arquivo, name='visualizar_arquivo'),
+
+ # APIs - Pastas e Subpastas
+ path('api/pastas/criar/', admin_views.api_criar_pasta, name='api_criar_pasta'),
+ path('api/subpastas/adicionar/', admin_views.api_adicionar_subpasta, name='api_adicionar_subpasta'),
# Visualização de processo
path('processo//', admin_views.processo_detail, name='processo_detail'),
+ path('processo//pasta///', admin_views.processo_subpasta_detail, name='processo_subpasta_detail'),
]
diff --git a/sapl/templates/admin/tce/app_index.html b/sapl/templates/admin/tce/app_index.html
index 416bcab00..1d53a8e81 100644
--- a/sapl/templates/admin/tce/app_index.html
+++ b/sapl/templates/admin/tce/app_index.html
@@ -116,36 +116,6 @@
-
-
-
-
{% trans 'Ações Rápidas' %}
-
-
-
-
-
-
-
-
{% endblock %}
diff --git a/sapl/templates/admin/tce/subpasta_detail.html b/sapl/templates/admin/tce/subpasta_detail.html
new file mode 100644
index 000000000..b30c70cb5
--- /dev/null
+++ b/sapl/templates/admin/tce/subpasta_detail.html
@@ -0,0 +1,947 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block title %}{{ subpasta_nome }} - {{ processo.titulo }}{% endblock %}
+
+{% block base_content %}
+
+
+
+
+
+
+
+
+
+
+
+ Pasta: {{ pasta.nome }}
+ |
+ Processo: {{ processo.titulo }} ({{ processo.ano }} - {{ processo.periodo }})
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if arquivos %}
+
+
+
+
+ | Nome |
+ Tamanho |
+ Status |
+ Data |
+ Ações |
+
+
+
+ {% for arquivo in arquivos %}
+
+
+
+ {{ arquivo.nome_original }}
+ {% if arquivo.p7s_gerado and arquivo.arquivo_p7s %}
+
+
+
+
+ {{ arquivo.nome_original|slice:":-4" }}.p7s
+
+
+ {% endif %}
+ |
+ {{ arquivo.tamanho_mb|floatformat:2 }} MB |
+
+ {% if arquivo.validado %}
+ Validado
+ {% elif arquivo.p7s_gerado %}
+ Quase pronto
+ {% elif arquivo.assinado %}
+ Processado
+ {% elif not arquivo.tamanho_ok %}
+ Arquivo muito grande
+ {% elif not arquivo.ocr_ok %}
+ OCR pendente
+ {% else %}
+ Novo
+ {% endif %}
+ |
+ {{ arquivo.criado_em|date:"d/m/Y H:i" }} |
+
+
+
+ |
+
+ {% endfor %}
+
+
+
+ {% else %}
+
+
+
Nenhum arquivo nesta pasta
+
Clique em "Adicionar Arquivos" para fazer upload
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Selecionar Arquivo
+
+
+
+
+
+
+
+
Verificação de Tamanho
+
+
+
+
+
Verificando tamanho do arquivo...
+
+
+
+
+
+
+
Tornar Pesquisável (OCR)
+
+
+
+
Aplicar OCR para tornar o PDF pesquisável?
+
+
+
+
+
+
+
+
+
Assinatura Digital
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gerar Arquivo .p7s
+
+
+
+
Converter para formato .p7s?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}