Browse Source

feat: Estrutura inicial do módulo TCE

pull/3858/head
rangelbruno 12 months ago
parent
commit
a0f5adfbc4
  1. 4
      docker/startup_scripts/start.sh
  2. 278
      modulo_tribunal_de_contas_tce_especificacao_funcional_e_tecnica.md
  3. 1
      requirements/requirements.txt
  4. 3
      sapl/settings.py
  5. 288
      sapl/tce/README.md
  6. 1
      sapl/tce/__init__.py
  7. 151
      sapl/tce/admin.py
  8. 319
      sapl/tce/admin_views.py
  9. 7
      sapl/tce/apps.py
  10. 146
      sapl/tce/migrations/0001_initial.py
  11. 18
      sapl/tce/migrations/0002_tcearquivo_subpasta.py
  12. 1
      sapl/tce/migrations/__init__.py
  13. 369
      sapl/tce/models.py
  14. 0
      sapl/tce/templatetags/__init__.py
  15. 8
      sapl/tce/templatetags/tce_filters.py
  16. 24
      sapl/tce/urls.py
  17. 2
      sapl/tce/views.py
  18. 1108
      sapl/templates/admin/tce/app_index.html
  19. 132
      sapl/templates/admin/tce/base_site.html
  20. 72
      sapl/templates/admin/tce/change_form.html
  21. 75
      sapl/templates/admin/tce/change_list.html
  22. 1130
      sapl/templates/admin/tce/processo_detail.html
  23. 14
      sapl/templates/index.html
  24. 2
      sapl/templates/navbar.yaml
  25. 4
      sapl/urls.py

4
docker/startup_scripts/start.sh

@ -141,6 +141,10 @@ wait_for_pg() {
migrate_db() { migrate_db() {
log "Running Django migrations..." log "Running Django migrations..."
# Cria migrations do TCE se necessário
log "Creating TCE module migrations if needed..."
python3 manage.py makemigrations tce --noinput || true
# Aplica todas as migrations
python3 manage.py migrate --noinput python3 manage.py migrate --noinput
} }

278
modulo_tribunal_de_contas_tce_especificacao_funcional_e_tecnica.md

@ -0,0 +1,278 @@
# Módulo Tribunal de Contas (TCE)
> **Objetivo**: prover um pacote completo para integração com o Tribunal de Contas (TCE), cobrindo *coleta/centralização de documentos*, *validação automática*, *preferência por PDF/A*, *assinatura digital ICP‑Brasil*, *gestão de prazos e calendário do TCE*, *painel de pendências*, *notificações*, *controle de versões e múltiplas assinaturas*, *organização por processos*, *portal de auditoria/transparência* e *relatórios internos*.
---
## 1) Visão Geral do Fluxo
```mermaid
flowchart LR
A[Origem: Proposições/Processos Internos] --> B[Preparação de Dossiê TCE]
B --> C[Validação Automática (PDF/A, metadados, assinaturas)]
C --> D[Assinaturas ICP-Brasil (múltiplos signatários)]
D --> E[Versões & Carimbo do Tempo]
E --> F[Envio/Protocolo TCE]
F --> G[Recepção de Devolutivas (pendências/alertas)]
G --> H[Painel de Pendências & Prazos]
H --> I[Relatórios Internos & Portal de Auditoria]
```
---
## 2) Escopo Funcional (por tela)
### 2.1 Tela: **Dashboard TCE**
**Objetivo:** visão executiva do status de conformidade e dos prazos.
- **Cards-chave**: Processos abertos | Itens prontos para envio | Pendências críticas | Prazos a vencer (7/15/30 dias) | Taxa de conformidade PDF/A | Assinaturas pendentes.
- **Gráficos**: evolução mensal de processos enviados, tempo médio de regularização de pendências, distribuição por secretaria/unidade.
- **Atalhos**: Criar processo TCE · Importar dossiê · Validar em lote · Assinar em lote · Configurar calendário.
### 2.2 Tela: **Catálogo de Processos (TCE)**
- **Lista** com filtros: status (rascunho, em validação, pronto para envio, enviado, devolvido, concluído), órgão/unidade, período, tipo de processo (contas, contratos, licitações, atos), responsável, presença de pendências.
- **Colunas**: Nº Interno · Nº TCE (quando disponível) · Assunto · Responsável · Versão atual · Assinaturas pendentes · Conformidade PDF/A · Data limite · Situação.
- **Ações em massa**: validar, assinar, enviar, exportar (ZIP, planilha), atribuir responsável.
### 2.3 Tela: **Processo TCE – Detalhe**
- **Abas**:
1. **Resumo** (metadados principais: exercício, unidade gestora, natureza do processo, palavras‑chave, histórico de eventos)
2. **Documentos** (lista, ordenação, pré‑visualização, metadados, *tags* de classificação, indicação de *documento principal*)
3. **Validações** (checklist automático com resultado por regra: PDF/A, campos obrigatórios, assinaturas válidas, integridade de anexos, nomeação padrão)
4. **Assinaturas** (workflow de múltiplos signatários, ordem, políticas PAdES, carimbo do tempo)
5. **Envio & Protocolo** (credenciais, endpoint, recibo, hash e logs)
6. **Pendências & Devolutivas** (lista, origem, prazo, SLA, tratativa, anexos de correção)
7. **Linha do Tempo** (eventos: upload, validação, assinatura, envio, retorno, reenvios)
### 2.4 Tela: **Painel de Pendências**
- **Quadro Kanban**: *Nova**Em análise**Aguardando correção**Pronta para reenvio**Resolvida*.
- **Filtros**: por órgão, tipo de pendência (documental, assinatura, metadado, prazo), criticidade, responsável.
- **SLA**: indicador de atraso, *badge* com dias restantes, notificação automática.
### 2.5 Tela: **Calendário do TCE**
- **Visões**: mensal/semanal/lista.
- **Eventos**: prazos legais, sessões, marcos de prestação de contas, datas de remessa por tipo de processo.
- **Sincronização**: iCal/ICS de leitura; importação de calendário oficial.
### 2.6 Tela: **Assinaturas (Lote e Individual)**
- **Suporte a múltiplos signatários** (papéis: ordenador de despesas, presidente, contador, controlador interno, procurador, etc.).
- **Políticas**: PAdES‑B/LT, inclusão de *timestamp* (TSA), cadeia ICP‑Brasil completa.
- **Experiência**: seleção de documentos, ordem de assinatura, aviso de pendências, pré-validação antes de aposição.
### 2.7 Tela: **Relatórios Internos**
- **Modelos**: processos por unidade, pendências por criticidade, tempo de ciclo, conformidade por regra, comparativo por exercício, histórico por fornecedor/contrato.
- **Exportações**: PDF, XLSX, CSV, JSON (com metadados e hashes).
### 2.8 Tela: **Portal de Auditoria & Transparência** (público)
- **Busca** por processo, assunto, exercício, unidade gestora.
- **Publicação seletiva**: ocultar dados sigilosos, publicar hashes/recibos, *watermark* "Cópia – Sem Valor Legal" quando aplicável.
---
## 3) Regras de Negócio & Validações
### 3.1 Conformidade de Documentos
- **PDF/A preferencial**: detectar perfil (A‑1, A‑2, A‑3), recusar quando obrigatório.
- **Nomeação padrão**: `TIPO_NUMERO_ANO_VERSAO.pdf` (ex.: `CONTRATO_045_2025_v2.pdf`).
- **Metadados mínimos**: tipo, exercício, UG, assunto, palavras‑chave, referência processual.
- **Proibições**: senhas em PDFs, campos editáveis em versão final, anexos duplicados.
### 3.2 Assinaturas Digitais (ICP‑Brasil)
- **Obrigatório**: certificados ICP‑Brasil (A1/A3) com verificação de cadeia e status revogação (CRL/OCSP).
- **Política PAdES**: validação pós‑assinatura, presença de *timestamp*, LTV quando possível.
- **Múltiplos signatários**: ordem, substituição (delegação), rejeição com justificativa, reabertura controlada.
### 3.3 Gestão de Prazos
- **Data limite por tipo de processo** (parametrizável).
- **Recalculo automático** ao receber devolutiva.
- **Alertas**: D‑15/D‑7/D‑1, *push* e e‑mail, canal Webhook.
### 3.4 Devolutivas & Pendências
- **Classificação**: documental, formal, técnica, assinatura, metadado.
- **Tratativa**: responsável, prazo, plano de ação, anexos de correção.
- **Auditoria**: tudo fica na linha do tempo com usuário, IP, hash e carimbo de tempo.
---
## 4) Modelo de Dados (alto nível)
```mermaid
classDiagram
class TceProcesso {
+uuid id
+string numero_interno
+string numero_tce
+string assunto
+string tipo
+int exercicio
+string unidade_gestora
+json metadados
+enum status
+datetime prazo_limite
+datetime enviado_em
+datetime concluido_em
}
class TceDocumento {
+uuid id
+uuid processo_id
+string nome_arquivo
+string tipo_documento
+int versao
+bool principal
+bool pdfa_ok
+bool assinatura_ok
+string hash_sha256
+json metadados
+datetime criado_em
}
class TceAssinatura {
+uuid id
+uuid documento_id
+uuid usuario_id
+string papel_assinante
+string certificado_serial
+enum politica_pades
+bool ltv
+datetime assinado_em
+string hash_assinatura
}
class TcePendencia {
+uuid id
+uuid processo_id
+string origem
+string descricao
+enum criticidade
+enum situacao
+datetime prazo
+uuid responsavel_id
+datetime resolvido_em
}
class TceEvento {
+uuid id
+uuid processo_id
+string tipo_evento
+string detalhes
+datetime ocorrido_em
+uuid usuario_id
+string ip
}
TceProcesso "1" o-- "*" TceDocumento
TceDocumento "1" o-- "*" TceAssinatura
TceProcesso "1" o-- "*" TcePendencia
TceProcesso "1" o-- "*" TceEvento
```
**Enum `status`**: `rascunho`, `validando`, `pronto_envio`, `enviado`, `devolvido`, `concluido`.
**Enum `criticidade`**: `baixa`, `media`, `alta`, `bloqueante`.
**Enum `situacao pendência`**: `nova`, `em_analise`, `aguardando_correcao`, `pronta_reenvio`, `resolvida`.
---
## 5) APIs & Integrações (exemplo)
### 5.1 Endpoints Internos (REST)
- `POST /api/tce/processos` – cria processo
- `GET /api/tce/processos?status=...` – lista com filtros/paginação
- `GET /api/tce/processos/{id}` – detalhe
- `POST /api/tce/processos/{id}/validar` – dispara validação
- `POST /api/tce/processos/{id}/assinar` – inicia fluxo de assinatura (individual ou em lote)
- `POST /api/tce/processos/{id}/enviar` – envia ao TCE (ou queue)
- `POST /api/tce/processos/{id}/receber` – callback/devolutiva do TCE
- `POST /api/tce/processos/{id}/pendencias` – abre pendência
- `PATCH /api/tce/pendencias/{id}` – atualiza situação
**Observação**: para **assinaturas**, expor também endpoints de *preflight* (validar certificado, política) e *postflight* (verificar LTV, timestamp, cadeia).
### 5.2 Webhooks
- `POST /hooks/tce/devolutiva` – recebe pendências/retornos
- `POST /hooks/tce/recibo` – recebe comprovante com nº de protocolo
### 5.3 Integrações externas
- **Assinatura**: serviço de assinatura ICP‑Brasil (PyHanko/Outros) com PAdES‑B/LT.
- **Validação PDF/A**: biblioteca/serviço que detecta e valida o perfil PDF/A.
- **Armazenamento**: S3/compatível para versões e ARNs de *object lock* (imutabilidade quando exigida).
- **Calendário**: ingestão ICS, parametrização de eventos oficiais.
---
## 6) Regras de Segurança & Auditoria
- **RBAC**: perfis (Administrador TCE, Responsável UG, Assinante, Auditor Interno, Leitura).
- **Assinaturas**: nunca trafegar PFX sem criptografia; HSM/A3 preferível; *pinning* de certificados confiáveis.
- **Logs**: 100% dos eventos relevantes com IP, *user‑agent*, hash do conteúdo e horário (UTC e local).
- **Imutabilidade**: ao concluir, bloquear alterações de documentos, preservando versões e provas de integridade (hash + recibos).
- **LGPD**: mascaramento/pseudonimização em relatórios públicos.
---
## 7) UX/Usabilidade & Acessibilidade
- **Ações em massa** (validar, assinar, enviar) com *progress* e *rollback* por item.
- **Rótulos claros**: “Pronto para envio”, “Assinaturas pendentes (2/4)”.
- **A11y**: contraste AA, navegação por teclado, *aria‑labels*, foco visível.
- **Feedbacks**: *toasts* (sucesso/erro), *modals* para confirmações de envio.
---
## 8) Tarefas Assíncronas & Rotinas
- **Fila `tce-validate`**: validação de PDF/A, metadados, duplicidade.
- **Fila `tce-sign`**: aposição de assinaturas e verificação posterior (LTV/TSA).
- **Fila `tce-send`**: empacotamento, envio e confirmação de protocolo.
- **Agendadores**: verificação diária de prazos, ingestão de calendário, reprocessamento de pendências.
---
## 9) Critérios de Aceitação (PoC)
1. **Criar processo**, anexar documentos, validar automaticamente → status “pronto_envio”.
2. **Assinar** 2+ documentos com 3 perfis distintos → PAdES‑LT válido.
3. **Enviar** e **registrar recibo** (simulado ou real) com número de protocolo → guardar hash/recibo.
4. **Receber devolutiva** → abrir pendência com prazo e classificar criticidade.
5. **Resolver pendência** → revalidar, reenviar, registrar nova versão.
6. **Relatórios** funcionam (pendências por unidade, SLA, evolução mensal).
7. **Portal público** exibe processo/recibo (quando marcado como público) sem dados sigilosos.
---
## 10) Métricas & KPIs
- Tempo médio de validação/assinatura/envio.
- % de documentos em PDF/A na primeira tentativa.
- % de processos com pendência e tempo de resolução.
- Adesão a prazos (no prazo x atrasados).
---
## 11) Configuração & Deploy
- **Variáveis**: `TCE_ENDPOINT`, `TCE_TOKEN`, `TCE_CALENDAR_ICS_URL`, `PDFA_ENFORCE=true`, `P7S_POLICY=PAdES-LT`, `TSA_URL`, `OCSP_CRL_CHECK=true`.
- **Docker/Compose**: serviços para *worker* e *scheduler*; volume S3; healthchecks em `/health/tce`.
- **Ambientes**: *dev* (sandbox), *homolog* (dados mascarados), *prod* (chaves e segredos via vault).
---
## 12) Anexos (Modelos)
- **Checklist de Validação** (JSON) – regras parametrizáveis por tipo de processo.
- **Template de Relatório** (XLSX/PDF) – pivot por unidade/criticidade.
- **Modelo de *Webhook* de Devolutiva** (JSON):
```json
{
"numero_tce": "2025-000123",
"processo_interno": "PROC-45-2025",
"itens": [
{"tipo": "assinatura", "descricao": "Assinatura do ordenador ausente", "criticidade": "alta", "prazo": "2025-10-20"},
{"tipo": "pdfa", "descricao": "Documento X não está PDF/A", "criticidade": "media", "prazo": "2025-10-15"}
]
}
```
---
## 13) Roadmap Sugerido
- **Fase 1**: Validação, Assinaturas, Catálogo de Processos, Painel de Pendências.
- **Fase 2**: Envio/Protocolo + Webhooks + Calendário + Relatórios.
- **Fase 3**: Portal de Auditoria + LTV/TSA + Políticas avançadas e integrações específicas do TCE.
> Este documento é base para desenho de UI, backlog e PoC. Ajuste as regras/validações conforme o caderno técnico do TCE local e as normativas vigentes.

1
requirements/requirements.txt

@ -39,4 +39,5 @@ django-prometheus==2.2.0
asn1crypto==1.5.1 asn1crypto==1.5.1
XlsxWriter==3.2.0 XlsxWriter==3.2.0
git+https://github.com/interlegis/django-admin-bootstrapped git+https://github.com/interlegis/django-admin-bootstrapped

3
sapl/settings.py

@ -66,7 +66,8 @@ SAPL_APPS = (
'sapl.redireciona_urls', 'sapl.redireciona_urls',
'sapl.compilacao', 'sapl.compilacao',
'sapl.api', 'sapl.api',
'sapl.rules' 'sapl.rules',
'sapl.tce',
) )
INSTALLED_APPS = ( INSTALLED_APPS = (

288
sapl/tce/README.md

@ -0,0 +1,288 @@
# Módulo TCE - Tribunal de Contas
Módulo completo para integração com o Tribunal de Contas (TCE), implementado conforme especificação funcional e técnica.
## 📋 Funcionalidades Implementadas
### 1. Gestão de Processos TCE
- ✅ Criação e gerenciamento de processos
- ✅ Controle de status (rascunho → validação → pronto para envio → enviado → devolvido → concluído)
- ✅ Gestão de prazos e calendário
- ✅ Dashboard executivo com métricas
### 2. Documentos
- ✅ Upload e versionamento de documentos
- ✅ Validação de formato PDF/A
- ✅ Cálculo de hash SHA-256 para integridade
- ✅ Classificação e metadados
- ✅ Controle de documento principal
### 3. Assinaturas Digitais
- ✅ Suporte a múltiplos signatários
- ✅ Políticas PAdES (B, LT, LTA)
- ✅ Controle de ordem de assinatura
- ✅ Validação de certificados ICP-Brasil (estrutura pronta)
- ✅ Timestamp (TSA)
### 4. Validações Automáticas
- ✅ Validação de PDF/A (estrutura pronta para integração com veraPDF)
- ✅ Validação de metadados obrigatórios
- ✅ Validação de nomeação de arquivos
- ✅ Checklist de conformidade
### 5. Pendências e Devolutivas
- ✅ Recepção de pendências do TCE
- ✅ Classificação por criticidade
- ✅ Gestão de SLA e prazos
- ✅ Quadro Kanban de situações
- ✅ Controle de atrasos
### 6. Envio ao TCE
- ✅ Registro de envios com protocolo
- ✅ Controle de tentativas
- ✅ Recibos e confirmações
- ✅ Logs detalhados
### 7. Linha do Tempo
- ✅ Auditoria completa de eventos
- ✅ Registro de IP e usuário
- ✅ Metadados de ações
### 8. Calendário TCE
- ✅ Eventos e prazos legais
- ✅ Sessões e marcos
- ✅ Eventos recorrentes
## 🏗️ Estrutura do Módulo
```
sapl/tce/
├── __init__.py
├── apps.py # Configuração do app
├── models.py # 7 models principais
├── serializers.py # Serializers DRF
├── views.py # ViewSets e endpoints
├── urls.py # Rotas da API
├── admin.py # Interface administrativa
├── utils.py # Utilitários de validação
├── migrations/ # Migrations do banco
└── templates/tce/ # Templates (a implementar)
```
## 📊 Modelos de Dados
### TceProcesso
Processo principal com controle de status, prazos e metadados.
### TceDocumento
Documentos anexados com validação PDF/A, hash e versões.
### TceAssinatura
Assinaturas digitais ICP-Brasil com políticas PAdES.
### TcePendencia
Pendências e devolutivas do TCE com gestão de SLA.
### TceEvento
Linha do tempo completa de auditoria.
### TceCalendario
Calendário de prazos e eventos do TCE.
### TceEnvio
Registro de envios com protocolos e recibos.
## 🔌 API REST
Todas as rotas estão disponíveis em `/tce/api/`:
### Processos
- `GET /tce/api/processos/` - Lista processos (com filtros)
- `POST /tce/api/processos/` - Cria processo
- `GET /tce/api/processos/{id}/` - Detalhe do processo
- `PUT/PATCH /tce/api/processos/{id}/` - Atualiza processo
- `GET /tce/api/processos/dashboard/` - Dashboard com métricas
- `POST /tce/api/processos/{id}/validar/` - Valida processo
- `POST /tce/api/processos/{id}/enviar/` - Envia ao TCE
- `POST /tce/api/processos/{id}/receber_devolutiva/` - Webhook para devolutivas
### Documentos
- `GET /tce/api/documentos/` - Lista documentos
- `POST /tce/api/documentos/` - Upload de documento
- `POST /tce/api/documentos/{id}/validar_pdfa/` - Valida PDF/A
### Pendências
- `GET /tce/api/pendencias/` - Lista pendências
- `POST /tce/api/pendencias/{id}/resolver/` - Resolve pendência
### Outros Endpoints
- `/tce/api/assinaturas/` - Gestão de assinaturas
- `/tce/api/eventos/` - Eventos (read-only)
- `/tce/api/calendario/` - Calendário TCE
- `/tce/api/envios/` - Envios (read-only)
## 🔧 Configuração
### 1. Registrar no settings.py
```python
SAPL_APPS = (
...
'sapl.tce',
)
```
### 2. Registrar URLs
```python
# sapl/urls.py
import sapl.tce.urls
urlpatterns = [
...
url(r'^tce/', include(sapl.tce.urls)),
]
```
### 3. Criar migrations e aplicar
```bash
python manage.py makemigrations tce
python manage.py migrate tce
```
### 4. Variáveis de Ambiente (opcional)
```bash
# .env
TCE_ENDPOINT=https://tce.exemplo.gov.br/api/processos
TCE_TOKEN=seu_token_aqui
TCE_CALENDAR_ICS_URL=https://tce.exemplo.gov.br/calendario.ics
PDFA_ENFORCE=true
P7S_POLICY=PAdES-LT
TSA_URL=https://tsa.exemplo.gov.br
OCSP_CRL_CHECK=true
```
## 🧪 Utilização
### Criar um processo
```python
from sapl.tce.models import TceProcesso
processo = TceProcesso.objects.create(
numero_interno='PROC-001-2025',
assunto='Prestação de Contas 2024',
tipo='contas',
exercicio=2024,
unidade_gestora='Secretaria de Finanças',
responsavel=user
)
```
### Upload de documento
```python
from sapl.tce.models import TceDocumento
documento = TceDocumento.objects.create(
processo=processo,
nome_arquivo='CONTAS_2024_v1.pdf',
arquivo=uploaded_file,
tipo_documento='principal',
principal=True
)
```
### Validar processo
```python
from sapl.tce.utils import validar_processo
resultado = validar_processo(processo)
if resultado['valido']:
processo.status = 'pronto_envio'
processo.save()
```
### Enviar ao TCE
```python
from sapl.tce.utils import enviar_processo_tce
resultado = enviar_processo_tce(processo, usuario=request.user)
if resultado['sucesso']:
print(f"Protocolo: {resultado['protocolo']}")
```
## 📝 Interface Administrativa
Acesse `/admin/tce/` para gerenciar:
- Processos TCE
- Documentos
- Assinaturas
- Pendências
- Eventos (auditoria)
- Calendário
- Envios
## 🚀 Próximos Passos
### Integrações Recomendadas
1. **PDF/A Validation**
- Integrar com [veraPDF](https://verapdf.org/)
- Implementar validação completa de perfis PDF/A
2. **Assinaturas Digitais**
- Integrar com [PyHanko](https://github.com/MatthiasValvekens/pyHanko)
- Implementar verificação de cadeia ICP-Brasil
- Validação OCSP/CRL
3. **Tasks Assíncronas**
- Implementar filas Celery/Django-Q
- Validação em background
- Envios assíncronos
4. **Frontend/Templates**
- Dashboard interativo
- Quadro Kanban de pendências
- Calendário visual
- Upload drag-and-drop
5. **Relatórios**
- Exportação em PDF/XLSX
- Gráficos de evolução
- Relatórios por unidade gestora
## ⚠️ Observações
### Implementações Simplificadas (Produção)
Algumas funcionalidades estão com implementação simplificada e devem ser integradas em produção:
1. **Validação PDF/A**: Estrutura pronta, requer integração com veraPDF
2. **Assinaturas Digitais**: Estrutura pronta, requer PyHanko para validação completa
3. **Envio ao TCE**: Implementado com simulação, requer integração com API real do TCE
4. **Verificação ICP-Brasil**: Estrutura pronta, requer bibliotecas de criptografia
### Segurança
- ✅ Controle de permissões via Django permissions
- ✅ Auditoria completa de ações
- ✅ Hash SHA-256 para integridade
- ⚠️ Implementar rate limiting para API
- ⚠️ Validação de certificados com HSM/A3
## 📚 Documentação de Referência
- Especificação completa: `modulo_tribunal_de_contas_tce_especificacao_funcional_e_tecnica.md`
- Django REST Framework: https://www.django-rest-framework.org/
- veraPDF: https://verapdf.org/
- PyHanko: https://github.com/MatthiasValvekens/pyHanko
## 🤝 Contribuindo
Para contribuir com o módulo TCE:
1. Seguir os padrões de código do SAPL
2. Adicionar testes unitários
3. Documentar novas funcionalidades
4. Manter compatibilidade com a API REST
## 📄 Licença
Este módulo segue a mesma licença do projeto SAPL.

1
sapl/tce/__init__.py

@ -0,0 +1 @@
default_app_config = 'sapl.tce.apps.TceConfig'

151
sapl/tce/admin.py

@ -0,0 +1,151 @@
"""
Admin do módulo TCE
"""
from django.contrib import admin
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(
'<span style="color: {}; font-weight: bold;">● {}</span>',
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(
'<div style="width:100px; background:#eee; border-radius:3px;">'
'<div style="width:{}%; background:{}; color:white; text-align:center; border-radius:3px;">{}</div>'
'</div>',
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(
'<span class="badge" style="background:{}; color:white; padding:3px 8px; border-radius:3px;">{}</span>',
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'<span style="color:{cor}; font-weight:bold;" title="{titulo}">{check}</span> '
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(
'<span style="color: {};">● {}</span>',
'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(
'<span style="color: {}; font-size: 16px;">{}</span>',
'green' if obj.sucesso else 'red',
'' if obj.sucesso else ''
)
sucesso_icon.short_description = 'Status'

319
sapl/tce/admin_views.py

@ -0,0 +1,319 @@
"""
Views customizadas para o admin do TCE
"""
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from .models import TceProcesso, TcePasta, TceArquivo, TceAssinatura, TceLog
import json
@login_required
def tce_admin_index(request):
"""
Dashboard principal do TCE no admin
Com navbar e footer padrão do SAPL
"""
context = {
'title': 'Tribunal de Contas (TCE)',
'total_processos': TceProcesso.objects.count(),
'total_arquivos': TceArquivo.objects.count(),
'total_validados': TceArquivo.objects.filter(validado=True).count(),
'total_assinaturas': TceAssinatura.objects.count(),
}
return render(request, 'admin/tce/app_index.html', context)
@login_required
@require_http_methods(["GET"])
def api_list_processos(request):
"""API: Listar todos os processos"""
processos = TceProcesso.objects.all().order_by('-criado_em')
data = {
'processos': [{
'id': str(p.id),
'titulo': p.titulo,
'ano': p.ano,
'periodo': p.periodo,
'status': p.status,
'total_arquivos': p.total_arquivos,
'criado_em': p.criado_em.isoformat()
} for p in processos]
}
return JsonResponse(data)
@login_required
@require_http_methods(["POST"])
def api_create_processo(request):
"""API: Criar novo processo"""
try:
titulo = request.POST.get('titulo')
ano = int(request.POST.get('ano'))
periodo = request.POST.get('periodo')
descricao = request.POST.get('descricao', '')
processo = TceProcesso.objects.create(
titulo=titulo,
ano=ano,
periodo=periodo,
descricao=descricao,
criado_por=request.user
)
# Criar pastas padrão automaticamente
pastas_padrao = [
('Gestão Orçamentária e Financeira', 'financeiro'),
('Gestão Administrativa', 'administrativo'),
('Gestão Legislativa', 'legislativo'),
]
for nome, categoria in pastas_padrao:
TcePasta.objects.create(
processo=processo,
nome=nome,
categoria=categoria
)
return JsonResponse({
'success': True,
'processo_id': str(processo.id),
'message': 'Processo criado com sucesso!'
})
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, 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 = {
'financeiro': [
'Balancetes mensais (receitas e despesas)',
'Execução orçamentária (RREO, RGF)',
'Demonstrativos de empenhos, liquidações e pagamentos',
'Folha de pagamento de servidores e vereadores'
],
'administrativo': [
'Atos de pessoal (nomeações, exonerações, aposentadorias, licenças)',
'Processos licitatórios, dispensas, inexigibilidades',
'Contratos administrativos e aditivos',
'Inventário patrimonial e relatórios de controle interno'
],
'legislativo': [
'Leis, resoluções, decretos legislativos aprovados',
'Atas das sessões plenárias',
'Relatórios de comissões e pareceres',
'Regimento Interno atualizado'
]
}
context = {
'title': f'Processo: {processo.titulo}',
'processo': processo,
'pastas': pastas,
'subpastas_estrutura': subpastas_estrutura,
}
return render(request, 'admin/tce/processo_detail.html', context)
@login_required
@require_http_methods(["GET"])
def api_list_arquivos(request):
"""API: Listar arquivos de uma subpasta"""
try:
pasta_id = request.GET.get('pasta_id')
subpasta = request.GET.get('subpasta')
if not pasta_id or not subpasta:
return JsonResponse({
'success': False,
'error': 'Parâmetros pasta_id e subpasta são obrigatórios'
}, status=400)
# Buscar arquivos filtrados
arquivos = TceArquivo.objects.filter(
pasta_id=pasta_id,
subpasta=subpasta
).order_by('-criado_em')
data = {
'success': True,
'arquivos': [{
'id': str(a.id),
'nome': a.nome_original,
'tipo': a.tipo,
'tamanho': f"{a.tamanho_mb:.2f} MB",
'tamanho_mb': a.tamanho_mb,
'tamanho_ok': a.tamanho_ok,
'ocr_ok': a.ocr_ok,
'assinado': a.assinado,
'p7s_gerado': a.p7s_gerado,
'validado': a.validado,
'status': a.status_geral,
'status_cor': a.status_cor,
'criado_em': a.criado_em.strftime('%d/%m/%Y %H:%M'),
'url': a.arquivo.url if a.arquivo else None,
} for a in arquivos]
}
return JsonResponse(data)
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
@login_required
@require_http_methods(["POST"])
@csrf_exempt
def api_upload_arquivo(request):
"""API: Upload de arquivo para uma subpasta"""
try:
pasta_id = request.POST.get('pasta_id')
subpasta = request.POST.get('subpasta')
arquivo = request.FILES.get('arquivo')
if not pasta_id or not subpasta or not arquivo:
return JsonResponse({
'success': False,
'error': 'Parâmetros pasta_id, subpasta e arquivo são obrigatórios'
}, status=400)
# Verificar se pasta existe
pasta = get_object_or_404(TcePasta, id=pasta_id)
# Detectar tipo de arquivo
nome_lower = arquivo.name.lower()
if nome_lower.endswith('.pdf'):
tipo = 'pdf'
elif nome_lower.endswith('.xml'):
tipo = 'xml'
else:
return JsonResponse({
'success': False,
'error': 'Apenas arquivos PDF e XML são permitidos'
}, status=400)
# Calcular tamanho
tamanho_bytes = arquivo.size
tamanho_mb = tamanho_bytes / (1024 * 1024)
tamanho_ok = tamanho_mb <= 5.0
# Criar arquivo
novo_arquivo = TceArquivo.objects.create(
pasta=pasta,
subpasta=subpasta,
arquivo=arquivo,
nome_original=arquivo.name,
tipo=tipo,
tamanho_bytes=tamanho_bytes,
tamanho_mb=tamanho_mb,
tamanho_ok=tamanho_ok,
enviado_por=request.user
)
# Criar log
TceLog.objects.create(
processo=pasta.processo,
arquivo=novo_arquivo,
tipo='arquivo_upload',
descricao=f'Upload do arquivo {arquivo.name} para {subpasta}',
usuario=request.user,
sucesso=True
)
return JsonResponse({
'success': True,
'arquivo_id': str(novo_arquivo.id),
'message': 'Arquivo enviado com sucesso!',
'tamanho_ok': tamanho_ok,
'tamanho_mb': round(tamanho_mb, 2)
})
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):
"""API: Excluir arquivo"""
print(f"=== DEBUG: Iniciando api_delete_arquivo para ID: {arquivo_id} ===")
try:
# Buscar arquivo
try:
arquivo = TceArquivo.objects.get(id=arquivo_id)
print(f"=== DEBUG: Arquivo encontrado: {arquivo.nome_original} ===")
except TceArquivo.DoesNotExist:
print(f"=== DEBUG: Arquivo {arquivo_id} não encontrado ===")
return JsonResponse({
'success': False,
'error': 'Arquivo não encontrado'
}, status=404)
nome_arquivo = arquivo.nome_original
subpasta_nome = arquivo.subpasta
print(f"=== DEBUG: Preparando exclusão - Nome: {nome_arquivo}, Subpasta: {subpasta_nome} ===")
# Criar log antes de excluir
try:
pasta = arquivo.pasta
processo = pasta.processo
TceLog.objects.create(
processo=processo,
tipo='erro',
descricao=f'Arquivo {nome_arquivo} excluído da subpasta {subpasta_nome}',
usuario=request.user,
sucesso=True
)
print(f"=== DEBUG: Log criado ===")
except Exception as log_error:
print(f"=== DEBUG: Erro ao criar log: {log_error} ===")
# Excluir arquivos físicos
try:
if arquivo.arquivo:
arquivo.arquivo.delete(save=False)
print(f"=== DEBUG: Arquivo físico excluído ===")
except Exception as file_error:
print(f"=== DEBUG: Erro ao excluir arquivo físico: {file_error} ===")
# Excluir registro do banco
print(f"=== DEBUG: Chamando arquivo.delete() ===")
arquivo.delete()
print(f"=== DEBUG: arquivo.delete() executado ===")
# Verificar se foi realmente excluído
existe = TceArquivo.objects.filter(id=arquivo_id).exists()
print(f"=== DEBUG: Arquivo ainda existe no banco? {existe} ===")
return JsonResponse({
'success': True,
'message': 'Arquivo excluído com sucesso!'
})
except Exception as e:
print(f"=== DEBUG: ERRO GERAL: {str(e)} ===")
import traceback
print(traceback.format_exc())
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)

7
sapl/tce/apps.py

@ -0,0 +1,7 @@
from django.apps import AppConfig
class TceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'sapl.tce'
verbose_name = 'Tribunal de Contas (TCE)'

146
sapl/tce/migrations/0001_initial.py

@ -0,0 +1,146 @@
# Generated by Django 2.2.28 on 2025-10-02 22:27
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TceArquivo',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('arquivo', models.FileField(upload_to='tce/arquivos/%Y/%m/', verbose_name='Arquivo')),
('nome_original', models.CharField(max_length=255, verbose_name='Nome Original')),
('nome_sugerido', models.CharField(blank=True, max_length=255, verbose_name='Nome Sugerido (Padrão TCE)')),
('tipo', models.CharField(choices=[('pdf', 'PDF'), ('xml', 'XML')], max_length=10, verbose_name='Tipo')),
('tamanho_bytes', models.BigIntegerField(default=0, verbose_name='Tamanho (bytes)')),
('tamanho_mb', models.FloatField(default=0, verbose_name='Tamanho (MB)')),
('tamanho_ok', models.BooleanField(default=False, verbose_name='Tamanho ≤ 5MB')),
('arquivo_reduzido', models.FileField(blank=True, null=True, upload_to='tce/reduzidos/%Y/%m/', verbose_name='Arquivo Reduzido')),
('tamanho_reduzido_mb', models.FloatField(blank=True, null=True, verbose_name='Tamanho Reduzido (MB)')),
('ocr_aplicado', models.BooleanField(default=False, verbose_name='OCR Aplicado')),
('ocr_ok', models.BooleanField(default=False, verbose_name='OCR OK (Pesquisável)')),
('arquivo_ocr', models.FileField(blank=True, null=True, upload_to='tce/ocr/%Y/%m/', verbose_name='Arquivo com OCR')),
('assinado', models.BooleanField(default=False, verbose_name='Assinado')),
('arquivo_assinado', models.FileField(blank=True, null=True, upload_to='tce/assinados/%Y/%m/', verbose_name='Arquivo Assinado')),
('certificado_info', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, verbose_name='Informações do Certificado')),
('p7s_gerado', models.BooleanField(default=False, verbose_name='.p7s Gerado')),
('arquivo_p7s', models.FileField(blank=True, null=True, upload_to='tce/p7s/%Y/%m/', verbose_name='Arquivo .p7s')),
('hash_sha256', models.CharField(blank=True, max_length=64, verbose_name='Hash SHA-256')),
('validado', models.BooleanField(default=False, verbose_name='Validado (Pronto para Envio)')),
('metadados', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, verbose_name='Metadados')),
('criado_em', models.DateTimeField(auto_now_add=True, verbose_name='Criado em')),
('atualizado_em', models.DateTimeField(auto_now=True, verbose_name='Atualizado em')),
('enviado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='arquivos_tce_enviados', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Arquivo TCE',
'verbose_name_plural': 'Arquivos TCE',
'ordering': ['pasta', 'nome_original'],
},
),
migrations.CreateModel(
name='TceProcesso',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('titulo', models.CharField(max_length=255, verbose_name='Título do Processo')),
('descricao', models.TextField(blank=True, verbose_name='Descrição')),
('ano', models.IntegerField(default=2025, verbose_name='Ano')),
('periodo', models.CharField(blank=True, help_text='Ex: 3º Quadrimestre, Anual, Mensal', max_length=100, verbose_name='Período')),
('status', models.CharField(choices=[('rascunho', 'Rascunho'), ('em_preparacao', 'Em Preparação'), ('validado', 'Validado'), ('enviado', 'Enviado ao TCE'), ('devolvido', 'Devolvido pelo TCE'), ('aprovado', 'Aprovado')], default='rascunho', max_length=20, verbose_name='Status')),
('numero_protocolo', models.CharField(blank=True, max_length=100, verbose_name='Número de Protocolo TCE')),
('data_envio', models.DateTimeField(blank=True, null=True, verbose_name='Data de Envio')),
('recibo_tce', models.TextField(blank=True, verbose_name='Recibo TCE')),
('hash_envio', models.CharField(blank=True, max_length=64, verbose_name='Hash do Envio')),
('metadados', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, verbose_name='Metadados')),
('criado_em', models.DateTimeField(auto_now_add=True, verbose_name='Criado em')),
('atualizado_em', models.DateTimeField(auto_now=True, verbose_name='Atualizado em')),
('criado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='processos_tce_criados', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Processo TCE',
'verbose_name_plural': 'Processos TCE',
'ordering': ['-ano', '-criado_em'],
},
),
migrations.CreateModel(
name='TcePasta',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('nome', models.CharField(max_length=200, verbose_name='Nome da Pasta')),
('categoria', models.CharField(choices=[('financeiro', 'Financeiro - Gestão Orçamentária'), ('administrativo', 'Administrativo - Gestão Administrativa'), ('legislativo', 'Legislativo - Gestão Legislativa'), ('outros', 'Outros')], max_length=20, verbose_name='Categoria')),
('descricao', models.TextField(blank=True, verbose_name='Descrição')),
('ordem', models.IntegerField(default=0, verbose_name='Ordem de Exibição')),
('criado_em', models.DateTimeField(auto_now_add=True, verbose_name='Criado em')),
('processo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pastas', to='tce.TceProcesso')),
],
options={
'verbose_name': 'Pasta TCE',
'verbose_name_plural': 'Pastas TCE',
'ordering': ['processo', 'ordem', 'nome'],
'unique_together': {('processo', 'nome')},
},
),
migrations.CreateModel(
name='TceLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('tipo', models.CharField(choices=[('processo_criado', 'Processo Criado'), ('pasta_criada', 'Pasta Criada'), ('arquivo_upload', 'Upload de Arquivo'), ('arquivo_reduzido', 'Arquivo Reduzido'), ('ocr_aplicado', 'OCR Aplicado'), ('arquivo_assinado', 'Arquivo Assinado'), ('p7s_gerado', 'Arquivo .p7s Gerado'), ('processo_validado', 'Processo Validado'), ('processo_enviado', 'Processo Enviado ao TCE'), ('recibo_recebido', 'Recibo TCE Recebido'), ('erro', 'Erro')], max_length=30, verbose_name='Tipo de Evento')),
('descricao', models.TextField(verbose_name='Descrição')),
('dados', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, verbose_name='Dados do Evento')),
('sucesso', models.BooleanField(default=True, verbose_name='Sucesso')),
('erro_mensagem', models.TextField(blank=True, verbose_name='Mensagem de Erro')),
('ocorrido_em', models.DateTimeField(auto_now_add=True, verbose_name='Ocorrido em')),
('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='Endereço IP')),
('arquivo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='logs', to='tce.TceArquivo')),
('processo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='tce.TceProcesso')),
('usuario', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='logs_tce', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Log TCE',
'verbose_name_plural': 'Logs TCE',
'ordering': ['-ocorrido_em'],
},
),
migrations.CreateModel(
name='TceAssinatura',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('ordem', models.IntegerField(default=1, verbose_name='Ordem da Assinatura')),
('papel', models.CharField(choices=[('presidente', 'Presidente'), ('ordenador', 'Ordenador de Despesas'), ('contador', 'Contador'), ('controlador', 'Controlador Interno'), ('procurador', 'Procurador'), ('outros', 'Outros')], max_length=20, verbose_name='Papel do Assinante')),
('certificado_tipo', models.CharField(choices=[('A1', 'A1'), ('A3', 'A3')], max_length=10, verbose_name='Tipo de Certificado')),
('certificado_serial', models.CharField(blank=True, max_length=100, verbose_name='Serial do Certificado')),
('certificado_nome', models.CharField(blank=True, max_length=255, verbose_name='Nome no Certificado')),
('certificado_validade', models.DateField(blank=True, null=True, verbose_name='Validade do Certificado')),
('valida', models.BooleanField(default=True, verbose_name='Assinatura Válida')),
('cadeia_icp_ok', models.BooleanField(default=False, verbose_name='Cadeia ICP Válida')),
('carimbo_tempo', models.BooleanField(default=False, verbose_name='Carimbo de Tempo Presente')),
('ltv', models.BooleanField(default=False, verbose_name='LTV (Long Term Validation)')),
('hash_assinatura', models.CharField(blank=True, max_length=64, verbose_name='Hash da Assinatura')),
('assinado_em', models.DateTimeField(auto_now_add=True, verbose_name='Assinado em')),
('arquivo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assinaturas', to='tce.TceArquivo')),
('assinado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assinaturas_realizadas', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Assinatura Digital',
'verbose_name_plural': 'Assinaturas Digitais',
'ordering': ['arquivo', 'ordem'],
},
),
migrations.AddField(
model_name='tcearquivo',
name='pasta',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arquivos', to='tce.TcePasta'),
),
]

18
sapl/tce/migrations/0002_tcearquivo_subpasta.py

@ -0,0 +1,18 @@
# Generated manually on 2025-10-02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tce', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='tcearquivo',
name='subpasta',
field=models.CharField(blank=True, help_text='Nome da subpasta dentro da pasta (ex: Balancetes mensais)', max_length=255, verbose_name='Subpasta'),
),
]

1
sapl/tce/migrations/__init__.py

@ -0,0 +1 @@

369
sapl/tce/models.py

@ -0,0 +1,369 @@
"""
Modelos do módulo TCE - Nova versão simplificada
Estrutura: Processo Pastas Arquivos Logs
"""
import uuid
from datetime import datetime
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
def get_current_year():
"""Retorna o ano atual"""
return datetime.now().year
class TceProcesso(models.Model):
"""
Processo principal TCE (ex: Prestação de Contas - 3º Quadrimestre/2025)
Similar a uma pasta principal no Google Drive
"""
STATUS_CHOICES = [
('rascunho', _('Rascunho')),
('em_preparacao', _('Em Preparação')),
('validado', _('Validado')),
('enviado', _('Enviado ao TCE')),
('devolvido', _('Devolvido pelo TCE')),
('aprovado', _('Aprovado')),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
titulo = models.CharField(_('Título do Processo'), max_length=255)
descricao = models.TextField(_('Descrição'), blank=True)
ano = models.IntegerField(_('Ano'), default=get_current_year)
periodo = models.CharField(_('Período'), max_length=100, blank=True,
help_text=_('Ex: 3º Quadrimestre, Anual, Mensal'))
status = models.CharField(_('Status'), max_length=20, choices=STATUS_CHOICES, default='rascunho')
# Protocolo TCE
numero_protocolo = models.CharField(_('Número de Protocolo TCE'), max_length=100, blank=True)
data_envio = models.DateTimeField(_('Data de Envio'), null=True, blank=True)
recibo_tce = models.TextField(_('Recibo TCE'), blank=True)
hash_envio = models.CharField(_('Hash do Envio'), max_length=64, blank=True)
# Metadados
metadados = JSONField(_('Metadados'), default=dict, blank=True)
# Auditoria
criado_em = models.DateTimeField(_('Criado em'), auto_now_add=True)
atualizado_em = models.DateTimeField(_('Atualizado em'), auto_now=True)
criado_por = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True,
related_name='processos_tce_criados')
class Meta:
verbose_name = _('Processo TCE')
verbose_name_plural = _('Processos TCE')
ordering = ['-ano', '-criado_em']
def __str__(self):
return f"{self.titulo} - {self.ano}"
@property
def total_arquivos(self):
"""Total de arquivos em todas as pastas"""
return TceArquivo.objects.filter(pasta__processo=self).count()
@property
def total_arquivos_prontos(self):
"""Total de arquivos prontos (≤5MB, OCR, assinados, com .p7s)"""
return TceArquivo.objects.filter(
pasta__processo=self,
tamanho_ok=True,
ocr_ok=True,
assinado=True,
p7s_gerado=True
).count()
@property
def progresso_percent(self):
"""Percentual de conclusão"""
total = self.total_arquivos
if total == 0:
return 0
return int((self.total_arquivos_prontos / total) * 100)
class TcePasta(models.Model):
"""
Pasta/Categoria dentro de um processo
(ex: Financeiro, Administrativa, Legislativa)
"""
CATEGORIA_CHOICES = [
('financeiro', _('Financeiro - Gestão Orçamentária')),
('administrativo', _('Administrativo - Gestão Administrativa')),
('legislativo', _('Legislativo - Gestão Legislativa')),
('outros', _('Outros')),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
processo = models.ForeignKey(TceProcesso, on_delete=models.CASCADE, related_name='pastas')
nome = models.CharField(_('Nome da Pasta'), max_length=200)
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)
criado_em = models.DateTimeField(_('Criado em'), auto_now_add=True)
class Meta:
verbose_name = _('Pasta TCE')
verbose_name_plural = _('Pastas TCE')
ordering = ['processo', 'ordem', 'nome']
unique_together = [['processo', 'nome']]
def __str__(self):
return f"{self.processo.titulo} / {self.nome}"
@property
def total_arquivos(self):
return self.arquivos.count()
@property
def total_arquivos_prontos(self):
return self.arquivos.filter(
tamanho_ok=True,
ocr_ok=True,
assinado=True,
p7s_gerado=True
).count()
@property
def icone(self):
"""Ícone FontAwesome baseado na categoria"""
icones = {
'financeiro': 'fa-coins',
'administrativo': 'fa-building',
'legislativo': 'fa-landmark',
'outros': 'fa-folder'
}
return icones.get(self.categoria, 'fa-folder')
@property
def cor(self):
"""Cor do badge baseado na categoria"""
cores = {
'financeiro': 'primary',
'administrativo': 'info',
'legislativo': 'success',
'outros': 'secondary'
}
return cores.get(self.categoria, 'secondary')
class TceArquivo(models.Model):
"""
Arquivo PDF ou XML dentro de uma pasta
Rastreia todas as etapas: tamanho, OCR, assinatura, .p7s
"""
TIPO_CHOICES = [
('pdf', 'PDF'),
('xml', 'XML'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
pasta = models.ForeignKey(TcePasta, on_delete=models.CASCADE, related_name='arquivos')
subpasta = models.CharField(_('Subpasta'), max_length=255, blank=True,
help_text=_('Nome da subpasta dentro da pasta (ex: Balancetes mensais)'))
# Arquivo original
arquivo = models.FileField(_('Arquivo'), upload_to='tce/arquivos/%Y/%m/')
nome_original = models.CharField(_('Nome Original'), max_length=255)
nome_sugerido = models.CharField(_('Nome Sugerido (Padrão TCE)'), max_length=255, blank=True)
tipo = models.CharField(_('Tipo'), max_length=10, choices=TIPO_CHOICES)
# Tamanho
tamanho_bytes = models.BigIntegerField(_('Tamanho (bytes)'), default=0)
tamanho_mb = models.FloatField(_('Tamanho (MB)'), default=0)
tamanho_ok = models.BooleanField(_('Tamanho ≤ 5MB'), default=False)
# Arquivo reduzido (se necessário)
arquivo_reduzido = models.FileField(_('Arquivo Reduzido'), upload_to='tce/reduzidos/%Y/%m/',
blank=True, null=True)
tamanho_reduzido_mb = models.FloatField(_('Tamanho Reduzido (MB)'), null=True, blank=True)
# OCR
ocr_aplicado = models.BooleanField(_('OCR Aplicado'), default=False)
ocr_ok = models.BooleanField(_('OCR OK (Pesquisável)'), default=False)
arquivo_ocr = models.FileField(_('Arquivo com OCR'), upload_to='tce/ocr/%Y/%m/',
blank=True, null=True)
# Assinatura Digital
assinado = models.BooleanField(_('Assinado'), default=False)
arquivo_assinado = models.FileField(_('Arquivo Assinado'), upload_to='tce/assinados/%Y/%m/',
blank=True, null=True)
certificado_info = JSONField(_('Informações do Certificado'), default=dict, blank=True)
# Arquivo .p7s
p7s_gerado = models.BooleanField(_('.p7s Gerado'), default=False)
arquivo_p7s = models.FileField(_('Arquivo .p7s'), upload_to='tce/p7s/%Y/%m/',
blank=True, null=True)
# Hash e validação
hash_sha256 = models.CharField(_('Hash SHA-256'), max_length=64, blank=True)
validado = models.BooleanField(_('Validado (Pronto para Envio)'), default=False)
# Metadados
metadados = JSONField(_('Metadados'), default=dict, blank=True)
# Auditoria
criado_em = models.DateTimeField(_('Criado em'), auto_now_add=True)
atualizado_em = models.DateTimeField(_('Atualizado em'), auto_now=True)
enviado_por = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True,
related_name='arquivos_tce_enviados')
class Meta:
verbose_name = _('Arquivo TCE')
verbose_name_plural = _('Arquivos TCE')
ordering = ['pasta', 'nome_original']
def __str__(self):
return f"{self.nome_original}"
@property
def arquivo_final(self):
"""Retorna o arquivo mais recente/processado"""
if self.arquivo_assinado:
return self.arquivo_assinado
elif self.arquivo_ocr:
return self.arquivo_ocr
elif self.arquivo_reduzido:
return self.arquivo_reduzido
return self.arquivo
@property
def status_geral(self):
"""Status geral do arquivo para exibição"""
if self.validado:
return 'validado'
elif self.assinado and self.p7s_gerado:
return 'quase_pronto'
elif self.tamanho_ok and self.ocr_ok:
return 'processado'
elif not self.tamanho_ok:
return 'tamanho_pendente'
elif not self.ocr_ok:
return 'ocr_pendente'
return 'novo'
@property
def status_cor(self):
"""Cor do status"""
cores = {
'validado': 'success',
'quase_pronto': 'info',
'processado': 'primary',
'tamanho_pendente': 'warning',
'ocr_pendente': 'warning',
'novo': 'secondary'
}
return cores.get(self.status_geral, 'secondary')
def atualizar_validacao(self):
"""Atualiza flag de validação baseado nos critérios"""
self.validado = (
self.tamanho_ok and
self.ocr_ok and
self.assinado and
self.p7s_gerado
)
self.save(update_fields=['validado'])
class TceAssinatura(models.Model):
"""
Registro de cada assinatura aplicada em um arquivo
Suporta múltiplos signatários
"""
PAPEL_CHOICES = [
('presidente', _('Presidente')),
('ordenador', _('Ordenador de Despesas')),
('contador', _('Contador')),
('controlador', _('Controlador Interno')),
('procurador', _('Procurador')),
('outros', _('Outros')),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
arquivo = models.ForeignKey(TceArquivo, on_delete=models.CASCADE, related_name='assinaturas')
ordem = models.IntegerField(_('Ordem da Assinatura'), default=1)
papel = models.CharField(_('Papel do Assinante'), max_length=20, choices=PAPEL_CHOICES)
# Dados do certificado
certificado_tipo = models.CharField(_('Tipo de Certificado'), max_length=10,
choices=[('A1', 'A1'), ('A3', 'A3')])
certificado_serial = models.CharField(_('Serial do Certificado'), max_length=100, blank=True)
certificado_nome = models.CharField(_('Nome no Certificado'), max_length=255, blank=True)
certificado_validade = models.DateField(_('Validade do Certificado'), null=True, blank=True)
# Validação
valida = models.BooleanField(_('Assinatura Válida'), default=True)
cadeia_icp_ok = models.BooleanField(_('Cadeia ICP Válida'), default=False)
carimbo_tempo = models.BooleanField(_('Carimbo de Tempo Presente'), default=False)
ltv = models.BooleanField(_('LTV (Long Term Validation)'), default=False)
# Hash
hash_assinatura = models.CharField(_('Hash da Assinatura'), max_length=64, blank=True)
# Auditoria
assinado_em = models.DateTimeField(_('Assinado em'), auto_now_add=True)
assinado_por = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True,
related_name='assinaturas_realizadas')
class Meta:
verbose_name = _('Assinatura Digital')
verbose_name_plural = _('Assinaturas Digitais')
ordering = ['arquivo', 'ordem']
def __str__(self):
return f"{self.arquivo.nome_original} - {self.get_papel_display()} ({self.ordem}ª)"
class TceLog(models.Model):
"""
Log completo de todas as ações no processo
Para auditoria e linha do tempo
"""
TIPO_CHOICES = [
('processo_criado', _('Processo Criado')),
('pasta_criada', _('Pasta Criada')),
('arquivo_upload', _('Upload de Arquivo')),
('arquivo_reduzido', _('Arquivo Reduzido')),
('ocr_aplicado', _('OCR Aplicado')),
('arquivo_assinado', _('Arquivo Assinado')),
('p7s_gerado', _('Arquivo .p7s Gerado')),
('processo_validado', _('Processo Validado')),
('processo_enviado', _('Processo Enviado ao TCE')),
('recibo_recebido', _('Recibo TCE Recebido')),
('erro', _('Erro')),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
processo = models.ForeignKey(TceProcesso, on_delete=models.CASCADE, related_name='logs')
arquivo = models.ForeignKey(TceArquivo, on_delete=models.SET_NULL, null=True, blank=True,
related_name='logs')
tipo = models.CharField(_('Tipo de Evento'), max_length=30, choices=TIPO_CHOICES)
descricao = models.TextField(_('Descrição'))
# Dados adicionais
dados = JSONField(_('Dados do Evento'), default=dict, blank=True)
sucesso = models.BooleanField(_('Sucesso'), default=True)
erro_mensagem = models.TextField(_('Mensagem de Erro'), blank=True)
# Auditoria
ocorrido_em = models.DateTimeField(_('Ocorrido em'), auto_now_add=True)
usuario = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True,
related_name='logs_tce')
ip = models.GenericIPAddressField(_('Endereço IP'), null=True, blank=True)
class Meta:
verbose_name = _('Log TCE')
verbose_name_plural = _('Logs TCE')
ordering = ['-ocorrido_em']
def __str__(self):
return f"{self.processo.titulo} - {self.get_tipo_display()} - {self.ocorrido_em.strftime('%d/%m/%Y %H:%M')}"

0
sapl/tce/templatetags/__init__.py

8
sapl/tce/templatetags/tce_filters.py

@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.filter(name='get_item')
def get_item(dictionary, key):
"""Template filter para acessar itens de dicionário"""
return dictionary.get(key, [])

24
sapl/tce/urls.py

@ -0,0 +1,24 @@
"""
URLs do módulo TCE
"""
from django.urls import path
from . import admin_views
app_name = 'tce'
urlpatterns = [
# Dashboard admin customizado
path('admin/', admin_views.tce_admin_index, name='admin_index'),
# APIs - Processos
path('api/processos/', admin_views.api_list_processos, name='api_list_processos'),
path('api/processos/create/', admin_views.api_create_processo, name='api_create_processo'),
# APIs - Arquivos
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/<uuid:arquivo_id>/delete/', admin_views.api_delete_arquivo, name='api_delete_arquivo'),
# Visualização de processo
path('processo/<uuid:processo_id>/', admin_views.processo_detail, name='processo_detail'),
]

2
sapl/tce/views.py

@ -0,0 +1,2 @@
# Views do módulo TCE
# Aguardando nova versão

1108
sapl/templates/admin/tce/app_index.html

File diff suppressed because it is too large

132
sapl/templates/admin/tce/base_site.html

@ -0,0 +1,132 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}{% trans 'Administração TCE' %} - {{ block.super }}{% endblock %}
{% block base_content %}
<div class="container-fluid mt-4">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{% trans 'Início' %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'sapl.tce:admin_index' %}">{% trans 'TCE' %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans 'Administração' %}</li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col-12">
{% block messages %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock messages %}
<div class="card">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<i class="fas fa-cog"></i>
{% block branding %}{% trans 'Administração do Módulo TCE' %}{% endblock %}
</h3>
</div>
<div class="card-body">
{% block content %}{% endblock %}
</div>
</div>
</div>
</div>
</div>
<style>
/* Estilos para melhorar a aparência dos formulários do admin */
.form-row {
margin-bottom: 15px;
}
.submit-row {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
}
.submit-row input[type="submit"],
.submit-row button {
margin-right: 10px;
}
.change-list {
margin-top: 20px;
}
.changelist-filter {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
}
.object-tools {
margin-bottom: 15px;
}
.object-tools a {
margin-right: 10px;
}
table {
width: 100%;
}
.button, input[type="submit"], input[type="button"], .submit-row input, button {
background: #007bff;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.button:hover, input[type="submit"]:hover, input[type="button"]:hover {
background: #0056b3;
color: white;
}
.button.default, input[type="submit"].default {
background: #28a745;
}
.button.default:hover, input[type="submit"].default:hover {
background: #218838;
}
.deletelink {
background: #dc3545 !important;
}
.deletelink:hover {
background: #c82333 !important;
}
.addlink {
background: #28a745 !important;
}
.addlink:hover {
background: #218838 !important;
}
</style>
{% endblock %}

72
sapl/templates/admin/tce/change_form.html

@ -0,0 +1,72 @@
{% extends "admin/change_form.html" %}
{% load i18n static menus %}
{% block extrahead %}
{{ block.super }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous">
<style>
/* Remove o header padrão do Django Admin */
#header {
display: none !important;
}
/* Ajustes para o container principal */
#container {
margin-top: 0;
padding-top: 0;
}
.breadcrumbs {
background: #f8f9fa;
padding: 10px 20px;
border-bottom: 1px solid #dee2e6;
}
</style>
{% endblock %}
{% block bodyclass %}{{ block.super }} tce-admin{% endblock %}
{% block content_title %}
<h1 class="mb-4">
<i class="fas fa-edit"></i> {{ title }}
</h1>
<div class="mb-3">
<a href="{% url 'admin:'|add:opts.app_label|add:'_'|add:opts.model_name|add:'_changelist' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> {% trans 'Voltar à Lista' %}
</a>
<a href="{% url 'sapl.tce:admin_index' %}" class="btn btn-secondary">
<i class="fas fa-home"></i> {% trans 'Dashboard TCE' %}
</a>
</div>
{% endblock %}
{% block nav-global %}
<!-- Navbar do SAPL -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">legisinc</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
{% navbar 'navbar.yaml' %}
<ul class="nav navbar-nav justify-content-end" id="autenticacao">
{% if user.is_authenticated %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<img height="30" width="30" src="/static/sapl/frontend/img/authenticated.png">
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li class="dropdown-item"><a>{{user.username}}</a></li>
<li class="dropdown-item"><a href="{% url 'sapl.base:alterar_senha' %}">Alterar senha</a></li>
<li class="dropdown-item"><a href="{% url 'sapl.base:logout' %}">Sair</a></li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% endblock %}

75
sapl/templates/admin/tce/change_list.html

@ -0,0 +1,75 @@
{% extends "admin/change_list.html" %}
{% load i18n static menus %}
{% block extrahead %}
{{ block.super }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous">
<style>
/* Remove o header padrão do Django Admin */
#header {
display: none !important;
}
/* Injeta o navbar do SAPL antes do conteúdo */
body::before {
content: '';
display: block;
}
/* Ajustes para o container principal */
#container {
margin-top: 0;
padding-top: 0;
}
.breadcrumbs {
background: #f8f9fa;
padding: 10px 20px;
border-bottom: 1px solid #dee2e6;
}
</style>
{% endblock %}
{% block bodyclass %}{{ block.super }} tce-admin{% endblock %}
{% block content_title %}
<h1 class="mb-4">
<i class="fas fa-list"></i> {{ title }}
</h1>
<div class="mb-3">
<a href="{% url 'sapl.tce:admin_index' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> {% trans 'Voltar ao Dashboard TCE' %}
</a>
</div>
{% endblock %}
{% block nav-global %}
<!-- Navbar do SAPL -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">legisinc</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
{% navbar 'navbar.yaml' %}
<ul class="nav navbar-nav justify-content-end" id="autenticacao">
{% if user.is_authenticated %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<img height="30" width="30" src="/static/sapl/frontend/img/authenticated.png">
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li class="dropdown-item"><a>{{user.username}}</a></li>
<li class="dropdown-item"><a href="{% url 'sapl.base:alterar_senha' %}">Alterar senha</a></li>
<li class="dropdown-item"><a href="{% url 'sapl.base:logout' %}">Sair</a></li>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% endblock %}

1130
sapl/templates/admin/tce/processo_detail.html

File diff suppressed because it is too large

14
sapl/templates/index.html

@ -145,6 +145,20 @@
</a> </a>
</div> </div>
</div> </div>
<!-- Tribunal de Contas (TCE) -->
<div class="homeBlock">
<div class="card-icon">
<i class="fas fa-file-invoice"></i>
</div>
<h3 class="card-title">Tribunal de Contas</h3>
<p class="card-description">Gestão de processos e prestação de contas ao TCE</p>
<div class="card-actions">
<a href="{% url 'sapl.tce:admin_index' %}" class="btn-action" title="Acessar Módulo TCE">
<i class="fas fa-folder-open"></i> Acessar
</a>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

2
sapl/templates/navbar.yaml

@ -1,7 +1,5 @@
{% load i18n common_tags %} {% load i18n common_tags %}
- title: {% trans 'Início' %}
url: '/'
{% if not sapl_as_sapn %} {% if not sapl_as_sapn %}
- title: {% trans 'Institucional' %} - title: {% trans 'Institucional' %}
children: children:

4
sapl/urls.py

@ -35,6 +35,7 @@ import sapl.protocoloadm.urls
import sapl.redireciona_urls.urls import sapl.redireciona_urls.urls
import sapl.relatorios.urls import sapl.relatorios.urls
import sapl.sessao.urls import sapl.sessao.urls
import sapl.tce.urls
from sapl.api.views_health import AppzVersionView, HealthzView, ReadyzView from sapl.api.views_health import AppzVersionView, HealthzView, ReadyzView
@ -56,6 +57,9 @@ urlpatterns += [
url(r'', include(sapl.relatorios.urls)), url(r'', include(sapl.relatorios.urls)),
url(r'', include(sapl.audiencia.urls)), url(r'', include(sapl.audiencia.urls)),
# TCE - Tribunal de Contas
url(r'^tce/', include((sapl.tce.urls, 'tce'), namespace='sapl.tce')),
# name='sapl_index'), # name='sapl_index'),
# must come at the end # must come at the end
# so that base /sistema/ url doesn't capture its children # so that base /sistema/ url doesn't capture its children

Loading…
Cancel
Save