diff --git a/docker/startup_scripts/start.sh b/docker/startup_scripts/start.sh index bd98bdfc0..686d0db5c 100755 --- a/docker/startup_scripts/start.sh +++ b/docker/startup_scripts/start.sh @@ -141,6 +141,10 @@ wait_for_pg() { migrate_db() { 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 } diff --git a/modulo_tribunal_de_contas_tce_especificacao_funcional_e_tecnica.md b/modulo_tribunal_de_contas_tce_especificacao_funcional_e_tecnica.md new file mode 100644 index 000000000..cac34b010 --- /dev/null +++ b/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. + diff --git a/requirements/requirements.txt b/requirements/requirements.txt index aea9f52db..71760e624 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -39,4 +39,5 @@ django-prometheus==2.2.0 asn1crypto==1.5.1 XlsxWriter==3.2.0 + git+https://github.com/interlegis/django-admin-bootstrapped diff --git a/sapl/settings.py b/sapl/settings.py index 20ff48ad7..6730d1f1c 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -66,7 +66,8 @@ SAPL_APPS = ( 'sapl.redireciona_urls', 'sapl.compilacao', 'sapl.api', - 'sapl.rules' + 'sapl.rules', + 'sapl.tce', ) INSTALLED_APPS = ( diff --git a/sapl/tce/README.md b/sapl/tce/README.md new file mode 100644 index 000000000..89212274f --- /dev/null +++ b/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. diff --git a/sapl/tce/__init__.py b/sapl/tce/__init__.py new file mode 100644 index 000000000..050079d20 --- /dev/null +++ b/sapl/tce/__init__.py @@ -0,0 +1 @@ +default_app_config = 'sapl.tce.apps.TceConfig' diff --git a/sapl/tce/admin.py b/sapl/tce/admin.py new file mode 100644 index 000000000..143024d5b --- /dev/null +++ b/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( + '● {}', + cores.get(obj.status, 'gray'), + obj.get_status_display() + ) + status_badge.short_description = 'Status' + + def progresso_bar(self, obj): + percent = obj.progresso_percent + cor = 'success' if percent == 100 else 'warning' if percent >= 50 else 'danger' + return format_html( + '
' + '
{}
' + '
', + percent, cor, f'{percent}%' + ) + progresso_bar.short_description = 'Progresso' + + +@admin.register(TcePasta) +class TcePastaAdmin(admin.ModelAdmin): + """Admin para Pastas TCE""" + change_list_template = 'admin/tce/change_list.html' + change_form_template = 'admin/tce/change_form.html' + list_display = ['nome', 'processo', 'categoria', 'total_arquivos', 'criado_em'] + list_filter = ['categoria', 'criado_em'] + search_fields = ['nome', 'processo__titulo'] + + +@admin.register(TceArquivo) +class TceArquivoAdmin(admin.ModelAdmin): + """Admin para Arquivos TCE""" + change_list_template = 'admin/tce/change_list.html' + change_form_template = 'admin/tce/change_form.html' + list_display = ['nome_original', 'pasta', 'tipo', 'tamanho_mb', 'status_badge', + 'checks', 'criado_em'] + list_filter = ['tipo', 'tamanho_ok', 'ocr_ok', 'assinado', 'p7s_gerado', 'validado'] + search_fields = ['nome_original', 'pasta__nome', 'pasta__processo__titulo'] + readonly_fields = ['criado_em', 'atualizado_em', 'hash_sha256'] + + def status_badge(self, obj): + return format_html( + '{}', + obj.status_cor, + obj.status_geral.replace('_', ' ').title() + ) + status_badge.short_description = 'Status' + + def checks(self, obj): + checks = [ + ('✓' if obj.tamanho_ok else '✗', 'green' if obj.tamanho_ok else 'red', '≤5MB'), + ('✓' if obj.ocr_ok else '✗', 'green' if obj.ocr_ok else 'red', 'OCR'), + ('✓' if obj.assinado else '✗', 'green' if obj.assinado else 'red', 'Assinado'), + ('✓' if obj.p7s_gerado else '✗', 'green' if obj.p7s_gerado else 'red', '.p7s'), + ] + html = ''.join([ + f'{check} ' + for check, cor, titulo in checks + ]) + return format_html(html) + checks.short_description = 'Checklist' + + +@admin.register(TceAssinatura) +class TceAssinaturaAdmin(admin.ModelAdmin): + """Admin para Assinaturas""" + change_list_template = 'admin/tce/change_list.html' + change_form_template = 'admin/tce/change_form.html' + list_display = ['arquivo', 'ordem', 'papel', 'certificado_tipo', 'valida_badge', 'assinado_em'] + list_filter = ['papel', 'certificado_tipo', 'valida', 'assinado_em'] + search_fields = ['arquivo__nome_original', 'certificado_nome'] + + def valida_badge(self, obj): + return format_html( + '● {}', + 'green' if obj.valida else 'red', + 'Válida' if obj.valida else 'Inválida' + ) + valida_badge.short_description = 'Validação' + + +@admin.register(TceLog) +class TceLogAdmin(admin.ModelAdmin): + """Admin para Logs (somente leitura)""" + change_list_template = 'admin/tce/change_list.html' + change_form_template = 'admin/tce/change_form.html' + list_display = ['processo', 'tipo', 'descricao_resumida', 'sucesso_icon', 'usuario', 'ocorrido_em'] + list_filter = ['tipo', 'sucesso', 'ocorrido_em'] + search_fields = ['processo__titulo', 'descricao'] + readonly_fields = ['processo', 'arquivo', 'tipo', 'descricao', 'dados', 'sucesso', + 'erro_mensagem', 'ocorrido_em', 'usuario', 'ip'] + + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + return False + + def descricao_resumida(self, obj): + return obj.descricao[:100] + '...' if len(obj.descricao) > 100 else obj.descricao + descricao_resumida.short_description = 'Descrição' + + def sucesso_icon(self, obj): + return format_html( + '{}', + 'green' if obj.sucesso else 'red', + '✓' if obj.sucesso else '✗' + ) + sucesso_icon.short_description = 'Status' diff --git a/sapl/tce/admin_views.py b/sapl/tce/admin_views.py new file mode 100644 index 000000000..cff2b62ee --- /dev/null +++ b/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) diff --git a/sapl/tce/apps.py b/sapl/tce/apps.py new file mode 100644 index 000000000..ea478144f --- /dev/null +++ b/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)' diff --git a/sapl/tce/migrations/0001_initial.py b/sapl/tce/migrations/0001_initial.py new file mode 100644 index 000000000..7196c5373 --- /dev/null +++ b/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'), + ), + ] diff --git a/sapl/tce/migrations/0002_tcearquivo_subpasta.py b/sapl/tce/migrations/0002_tcearquivo_subpasta.py new file mode 100644 index 000000000..1b27af3ae --- /dev/null +++ b/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'), + ), + ] diff --git a/sapl/tce/migrations/__init__.py b/sapl/tce/migrations/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/sapl/tce/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/sapl/tce/models.py b/sapl/tce/models.py new file mode 100644 index 000000000..5e0b7a775 --- /dev/null +++ b/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')}" diff --git a/sapl/tce/templatetags/__init__.py b/sapl/tce/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sapl/tce/templatetags/tce_filters.py b/sapl/tce/templatetags/tce_filters.py new file mode 100644 index 000000000..77dca134d --- /dev/null +++ b/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, []) diff --git a/sapl/tce/urls.py b/sapl/tce/urls.py new file mode 100644 index 000000000..b652f97df --- /dev/null +++ b/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//delete/', admin_views.api_delete_arquivo, name='api_delete_arquivo'), + + # Visualização de processo + path('processo//', admin_views.processo_detail, name='processo_detail'), +] diff --git a/sapl/tce/views.py b/sapl/tce/views.py new file mode 100644 index 000000000..20cb560bf --- /dev/null +++ b/sapl/tce/views.py @@ -0,0 +1,2 @@ +# Views do módulo TCE +# Aguardando nova versão diff --git a/sapl/templates/admin/tce/app_index.html b/sapl/templates/admin/tce/app_index.html new file mode 100644 index 000000000..416bcab00 --- /dev/null +++ b/sapl/templates/admin/tce/app_index.html @@ -0,0 +1,1108 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block page_title %}{% trans 'Tribunal de Contas (TCE)' %}{% endblock %} + +{% block base_content %} +
+
+
+

+ {% trans 'Tribunal de Contas (TCE)' %} +

+

{% trans 'Sistema de gestão de processos e prestação de contas ao TCE' %}

+
+
+
+ + +
+
+
+
+ + +
+ {% if total_processos == 0 %} + +
+ +

{% trans 'Nenhum processo criado ainda' %}

+

{% trans 'Crie seu primeiro processo para começar a organizar os documentos do TCE' %}

+ +
+ {% else %} + +
+
{% trans 'Meus Processos' %}
+ +
+ +
+ +
+
+ {% trans 'Carregando...' %} +
+
+
+ {% endif %} +
+ +
+
+
+
+ + +
+
+

{% trans 'Estatísticas Rápidas' %}

+
+
+
+ +
+
+
+
+
+ {% trans 'Processos' %} +
+

{{ total_processos|default:0 }}

+ {% trans 'Total cadastrados' %} +
+
+
+ +
+
+
+
+ {% trans 'Arquivos' %} +
+

{{ total_arquivos|default:0 }}

+ {% trans 'Total enviados' %} +
+
+
+ +
+
+
+
+ {% trans 'Validados' %} +
+

{{ total_validados|default:0 }}

+ {% trans 'Prontos para envio' %} +
+
+
+ +
+
+
+
+ {% trans 'Assinaturas' %} +
+

{{ total_assinaturas|default:0 }}

+ {% trans 'Total aplicadas' %} +
+
+
+
+ + +
+
+

{% trans 'Ações Rápidas' %}

+
+
+
+ + +
+ + + + + + + + + + +{% endblock %} diff --git a/sapl/templates/admin/tce/base_site.html b/sapl/templates/admin/tce/base_site.html new file mode 100644 index 000000000..20cdbe318 --- /dev/null +++ b/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 %} +
+
+
+ +
+
+ +
+
+ {% block messages %} + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + {% endblock messages %} + +
+
+

+ + {% block branding %}{% trans 'Administração do Módulo TCE' %}{% endblock %} +

+
+
+ {% block content %}{% endblock %} +
+
+
+
+
+ + +{% endblock %} diff --git a/sapl/templates/admin/tce/change_form.html b/sapl/templates/admin/tce/change_form.html new file mode 100644 index 000000000..fcd228ead --- /dev/null +++ b/sapl/templates/admin/tce/change_form.html @@ -0,0 +1,72 @@ +{% extends "admin/change_form.html" %} +{% load i18n static menus %} + +{% block extrahead %} + {{ block.super }} + + +{% endblock %} + +{% block bodyclass %}{{ block.super }} tce-admin{% endblock %} + +{% block content_title %} +

+ {{ title }} +

+ +{% endblock %} + +{% block nav-global %} + + +{% endblock %} diff --git a/sapl/templates/admin/tce/change_list.html b/sapl/templates/admin/tce/change_list.html new file mode 100644 index 000000000..ee33ccc85 --- /dev/null +++ b/sapl/templates/admin/tce/change_list.html @@ -0,0 +1,75 @@ +{% extends "admin/change_list.html" %} +{% load i18n static menus %} + +{% block extrahead %} + {{ block.super }} + + +{% endblock %} + +{% block bodyclass %}{{ block.super }} tce-admin{% endblock %} + +{% block content_title %} +

+ {{ title }} +

+ +{% endblock %} + +{% block nav-global %} + + +{% endblock %} diff --git a/sapl/templates/admin/tce/processo_detail.html b/sapl/templates/admin/tce/processo_detail.html new file mode 100644 index 000000000..65dd256eb --- /dev/null +++ b/sapl/templates/admin/tce/processo_detail.html @@ -0,0 +1,1130 @@ +{% extends "base.html" %} +{% load i18n %} +{% load tce_filters %} + +{% block title %}{{ processo.titulo }}{% endblock %} + +{% block base_content %} +
+ + + + +
+
+
+
+
+

+ {{ processo.titulo }} +

+ {{ processo.ano }} - {{ processo.periodo }} +
+
+
+

{{ processo.descricao }}

+
+
+
+
+ + +
+ {% for pasta in pastas %} +
+
+
+
+ {{ pasta.nome }} +
+
+
+ +
+ {% for subpasta in subpastas_estrutura|get_item:pasta.categoria %} +
+
+
+ + {{ subpasta }} + + 0 arquivo(s) + +
+ +
+
+ {% endfor %} +
+
+
+
+ {% endfor %} +
+ + + + + + +
+ + + + + + + + + + +{% endblock %} diff --git a/sapl/templates/index.html b/sapl/templates/index.html index ca1553267..be5dcbf16 100644 --- a/sapl/templates/index.html +++ b/sapl/templates/index.html @@ -145,6 +145,20 @@ + + +
+
+ +
+

Tribunal de Contas

+

Gestão de processos e prestação de contas ao TCE

+ +
{% endblock %} diff --git a/sapl/templates/navbar.yaml b/sapl/templates/navbar.yaml index e4dc39c10..ad021854b 100644 --- a/sapl/templates/navbar.yaml +++ b/sapl/templates/navbar.yaml @@ -1,7 +1,5 @@ {% load i18n common_tags %} -- title: {% trans 'Início' %} - url: '/' {% if not sapl_as_sapn %} - title: {% trans 'Institucional' %} children: diff --git a/sapl/urls.py b/sapl/urls.py index d2967e927..985d655a2 100644 --- a/sapl/urls.py +++ b/sapl/urls.py @@ -35,6 +35,7 @@ import sapl.protocoloadm.urls import sapl.redireciona_urls.urls import sapl.relatorios.urls import sapl.sessao.urls +import sapl.tce.urls from sapl.api.views_health import AppzVersionView, HealthzView, ReadyzView @@ -56,6 +57,9 @@ urlpatterns += [ url(r'', include(sapl.relatorios.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'), # must come at the end # so that base /sistema/ url doesn't capture its children