mirror of https://github.com/interlegis/sapl.git
25 changed files with 4154 additions and 3 deletions
@ -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. |
||||
|
|
||||
@ -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. |
||||
@ -0,0 +1 @@ |
|||||
|
default_app_config = 'sapl.tce.apps.TceConfig' |
||||
@ -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' |
||||
@ -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) |
||||
@ -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)' |
||||
@ -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'), |
||||
|
), |
||||
|
] |
||||
@ -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'), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1 @@ |
|||||
|
|
||||
@ -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,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, []) |
||||
@ -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'), |
||||
|
] |
||||
@ -0,0 +1,2 @@ |
|||||
|
# Views do módulo TCE |
||||
|
# Aguardando nova versão |
||||
File diff suppressed because it is too large
@ -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">×</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 %} |
||||
@ -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 %} |
||||
@ -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 %} |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue