Browse Source

Feat(Templates): Sistema de templates de documentos para OnlyOffice

Implementa funcionalidade para gerenciar templates de documentos com
cabeçalho, rodapé e formatação pré-definidos. Novos documentos criados
via OnlyOffice usam esses templates automaticamente.

Funcionalidades:
- CRUD completo para gerenciar templates (/sistema/template-documento/)
- Edição de templates via OnlyOffice com cabeçalho/rodapé
- Busca hierárquica de templates (específico > genérico)
- Templates vinculáveis a tipos específicos (TipoMateriaLegislativa, etc.)
- Criação automática de documento em branco se não enviar arquivo
- Integração com todas as views de download OnlyOffice

Arquivos novos:
- sapl/base/onlyoffice_template_views.py
- sapl/base/migrations/0061_documenttemplate.py
- sapl/utils_template.py
- sapl/templates/base/documenttemplate_*.html
- docs/templates-documentos.md
pull/3858/head
rangelbruno 8 months ago
parent
commit
2bd536b40b
  1. 214
      docs/templates-documentos.md
  2. 97
      sapl/base/forms.py
  3. 62
      sapl/base/migrations/0061_documenttemplate.py
  4. 139
      sapl/base/models.py
  5. 254
      sapl/base/onlyoffice_template_views.py
  6. 19
      sapl/base/urls.py
  7. 113
      sapl/base/views.py
  8. 78
      sapl/materia/onlyoffice_materia_views.py
  9. 40
      sapl/materia/onlyoffice_views.py
  10. 38
      sapl/norma/onlyoffice_views.py
  11. 38
      sapl/protocoloadm/onlyoffice_views.py
  12. 18
      sapl/templates/base/documenttemplate_detail.html
  13. 22
      sapl/templates/base/documenttemplate_form.html
  14. 7
      sapl/templates/base/layouts.yaml
  15. 3
      sapl/templates/menu_tabelas_auxiliares.yaml
  16. 215
      sapl/utils_template.py

214
docs/templates-documentos.md

@ -0,0 +1,214 @@
# Templates de Documentos - SAPL
## Visão Geral
O sistema de Templates de Documentos permite que administradores configurem modelos padrão com cabeçalho, rodapé e formatação pré-definidos. Quando um usuário cria um novo documento via OnlyOffice (proposição, matéria legislativa, norma, etc.), o sistema automaticamente aplica o template correspondente.
## Arquitetura
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Admin SAPL │────▶│ CRUD Templates │────▶│ OnlyOffice │
│ │ │ │ │ (Edita .docx) │
└─────────────────┘ └──────────────────┘ └────────┬────────┘
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Usuário Comum │────▶│ Criar Documento │────▶│ Aplica Template │
│ │ │ (Matéria, etc) │ │ (cabeç/rodapé) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
```
## Como Acessar
### Menu Principal
1. Acesse o SAPL como administrador
2. Vá em **Sistema** → **Tabelas Auxiliares**
3. Na seção **Configurações Gerais**, clique em **Templates de Documentos**
### URL Direta
```
/sistema/template-documento/
```
## Criando um Template
### Passo 1: Acessar a Criação
1. Acesse `/sistema/template-documento/`
2. Clique em **Adicionar Template de Documento**
### Passo 2: Preencher o Formulário
| Campo | Descrição | Obrigatório | Exemplo |
|-------|-----------|-------------|---------|
| **Nome** | Nome identificador do template | Sim | "Template Oficial Câmara" |
| **Tipo de Conteúdo** | Tipo de documento que usará este template | Sim | Matéria Legislativa |
| **Tipo Específico** | Vincular a um tipo específico | Não | TipoMateriaLegislativa |
| **ID do Tipo** | ID do tipo específico | Não | 1 (Projeto de Lei) |
| **Descrição** | Descrição do template | Não | "Template com brasão e rodapé padrão" |
| **Arquivo** | Arquivo .docx inicial | **Não** | template.docx |
| **Ativo** | Se o template está disponível para uso | Sim | ✓ |
| **Padrão** | Se é o template padrão para este tipo | Não | ✓ |
> **Nota:** O campo **Arquivo** é opcional. Se não enviar um arquivo, o sistema criará automaticamente um documento em branco com cabeçalho e rodapé de exemplo.
### Passo 3: Editar no OnlyOffice
Após salvar, você será **automaticamente redirecionado** para o editor OnlyOffice onde poderá:
- Configurar o **cabeçalho** (logo, nome da casa legislativa)
- Configurar o **rodapé** (numeração de página, data)
- Definir **estilos** de fonte e formatação padrão
- Adicionar **elementos fixos** que aparecerão em todos os documentos
### Passo 4: Salvar
O OnlyOffice salva automaticamente. Quando terminar, clique em **Voltar** para retornar ao SAPL.
### Editando um Template Existente
Na tela de **Detalhes** do template, você encontrará dois botões:
- **Editar no OnlyOffice** - Abre o editor para modificar cabeçalho, rodapé e formatação
- **Editar Dados** - Modifica os campos do formulário (nome, tipo, etc.)
Na tela de **Edição de Dados**, há um alerta no topo com link direto para o OnlyOffice.
## Tipos de Conteúdo Suportados
| Tipo | Descrição | Usado em |
|------|-----------|----------|
| `proposicao` | Proposições | Módulo Matéria → Proposições |
| `materia` | Matérias Legislativas | Módulo Matéria → Matérias |
| `docacessorio` | Documentos Acessórios | Anexos de Matérias |
| `docadm` | Documentos Administrativos | Módulo Protocolo Administrativo |
| `norma` | Normas Jurídicas | Módulo Normas |
## Hierarquia de Templates
O sistema busca templates na seguinte ordem de prioridade:
1. **Template Específico**: Vinculado ao tipo exato (ex: "Projeto de Lei")
2. **Template Genérico**: Apenas com tipo de conteúdo (ex: "Matéria Legislativa")
3. **Documento em Branco**: Se nenhum template for encontrado
### Exemplo Prático
Se você criar:
- Template A: Matéria Legislativa (genérico)
- Template B: Matéria Legislativa → Projeto de Lei (específico)
Ao criar um **Projeto de Lei**, o sistema usará o **Template B**.
Ao criar um **Requerimento**, o sistema usará o **Template A**.
## Configurando Templates Específicos
Para vincular um template a um tipo específico:
### 1. Identificar o Tipo
Primeiro, descubra o ID do tipo desejado. Por exemplo, para TipoMateriaLegislativa:
- Acesse `/materia/tipo-materia-legislativa/`
- Anote o ID do tipo (visível na URL ao editar)
### 2. Configurar no Template
No formulário de criação/edição do template:
- **Tipo Específico**: Selecione o ContentType correspondente
- `materia | tipomaterialegislativa` para tipos de matéria
- `materia | tipoproposicao` para tipos de proposição
- `protocoloadm | tipodocumentoadministrativo` para tipos de doc. administrativo
- `norma | tiponormajuridica` para tipos de norma
- **ID do Tipo Específico**: Digite o ID anotado
## Estrutura Técnica
### Arquivos Criados
| Arquivo | Descrição |
|---------|-----------|
| `sapl/base/models.py` | Modelo `DocumentTemplate` |
| `sapl/base/forms.py` | Formulário `DocumentTemplateForm` |
| `sapl/base/views.py` | CRUD `DocumentTemplateCrud` |
| `sapl/base/urls.py` | URLs do CRUD e OnlyOffice |
| `sapl/base/onlyoffice_template_views.py` | Views OnlyOffice para templates |
| `sapl/utils_template.py` | Funções utilitárias |
| `sapl/base/migrations/0061_documenttemplate.py` | Migration do banco |
| `sapl/templates/base/layouts.yaml` | Layout do formulário |
| `sapl/templates/menu_tabelas_auxiliares.yaml` | Link no menu |
| `sapl/templates/base/documenttemplate_detail.html` | Template da tela de detalhes |
| `sapl/templates/base/documenttemplate_form.html` | Template da tela de edição |
### URLs Disponíveis
| URL | Descrição |
|-----|-----------|
| `/sistema/template-documento/` | Lista de templates |
| `/sistema/template-documento/create` | Criar template |
| `/sistema/template-documento/{pk}` | Detalhes do template |
| `/sistema/template-documento/{pk}/edit` | Editar template |
| `/sistema/template-documento/{pk}/delete` | Excluir template |
| `/sistema/template-documento/{pk}/onlyoffice/editor` | Editor OnlyOffice |
### Modelo de Dados
```python
class DocumentTemplate(models.Model):
nome = CharField(max_length=100)
descricao = TextField(blank=True)
tipo_conteudo = CharField(choices=TIPO_CONTEUDO_TEMPLATE)
# GenericFK para tipo específico (opcional)
content_type = ForeignKey(ContentType, null=True, blank=True)
object_id = PositiveIntegerField(null=True, blank=True)
tipo_especifico = GenericForeignKey()
arquivo = FileField(upload_to='templates/')
ativo = BooleanField(default=True)
padrao = BooleanField(default=False)
data_criacao = DateTimeField(auto_now_add=True)
data_modificacao = DateTimeField(auto_now=True)
```
## Boas Práticas
### 1. Nomenclatura
Use nomes descritivos que identifiquem claramente o propósito:
- ✅ "Template Oficial - Projetos de Lei"
- ✅ "Modelo Padrão Câmara - Requerimentos"
- ❌ "Template 1"
### 2. Organização
- Crie um template **genérico** para cada tipo de conteúdo
- Crie templates **específicos** apenas quando necessário
- Mantenha apenas **um template padrão** por tipo
### 3. Cabeçalho e Rodapé
No OnlyOffice, configure:
- **Cabeçalho**: Brasão, nome da casa legislativa, endereço
- **Rodapé**: Número da página, data, informações de contato
### 4. Manutenção
- Revise os templates periodicamente
- Desative templates obsoletos em vez de excluí-los
- Mantenha backup dos arquivos .docx
## Solução de Problemas
### Template não está sendo aplicado
1. Verifique se o template está **Ativo**
2. Verifique se o **Tipo de Conteúdo** está correto
3. Se é específico, verifique se o **ID do Tipo** está correto
### Cabeçalho/rodapé não aparecem
1. Edite o template no OnlyOffice
2. Verifique se o cabeçalho/rodapé foram salvos corretamente
3. O OnlyOffice salva automaticamente, mas aguarde alguns segundos
### Erro ao criar template
1. Verifique se o arquivo é um **.docx** válido
2. Verifique se você tem permissão de administrador
3. Consulte os logs do sistema para mais detalhes
## Permissões Necessárias
Para gerenciar templates, o usuário precisa das permissões:
- `base.add_documenttemplate` - Criar templates
- `base.change_documenttemplate` - Editar templates
- `base.delete_documenttemplate` - Excluir templates
- `base.view_documenttemplate` - Visualizar templates
Essas permissões são automaticamente concedidas ao grupo **Operador Geral** e administradores.

97
sapl/base/forms.py

@ -1293,6 +1293,103 @@ class PartidoForm(FileFieldCheckMixin, ModelForm):
return cleaned_data
class DocumentTemplateForm(FileFieldCheckMixin, ModelForm):
class Meta:
from .models import DocumentTemplate
model = DocumentTemplate
fields = ['nome', 'descricao', 'tipo_conteudo', 'content_type',
'object_id', 'arquivo', 'ativo', 'padrao']
def __init__(self, *args, **kwargs):
super(DocumentTemplateForm, self).__init__(*args, **kwargs)
# Limita os content_types às opções válidas de tipos específicos
from django.contrib.contenttypes.models import ContentType
from sapl.materia.models import TipoMateriaLegislativa, TipoProposicao, TipoDocumento
from sapl.protocoloadm.models import TipoDocumentoAdministrativo
from sapl.norma.models import TipoNormaJuridica
tipos_validos = [
ContentType.objects.get_for_model(TipoMateriaLegislativa),
ContentType.objects.get_for_model(TipoProposicao),
ContentType.objects.get_for_model(TipoDocumento),
ContentType.objects.get_for_model(TipoDocumentoAdministrativo),
ContentType.objects.get_for_model(TipoNormaJuridica),
]
self.fields['content_type'].queryset = ContentType.objects.filter(
pk__in=[ct.pk for ct in tipos_validos]
)
self.fields['content_type'].required = False
self.fields['object_id'].required = False
# Arquivo é opcional - pode criar em branco e editar no OnlyOffice
self.fields['arquivo'].required = False
self.fields['arquivo'].help_text = _(
'Opcional. Se não enviar arquivo, um documento em branco será criado. '
'Você poderá editar o template no OnlyOffice após salvar.'
)
row1 = to_row([
('nome', 6),
('tipo_conteudo', 6),
])
row2 = to_row([
('content_type', 6),
('object_id', 6),
])
row3 = to_row([
('descricao', 12),
])
row4 = to_row([
('arquivo', 8),
('ativo', 2),
('padrao', 2),
])
self.helper = SaplFormHelper()
self.helper.layout = Layout(
Fieldset(
_('Template de Documento'),
row1, row2, row3, row4,
),
form_actions(label='Salvar'))
def clean(self):
cleaned_data = super().clean()
if not self.is_valid():
return cleaned_data
content_type = cleaned_data.get('content_type')
object_id = cleaned_data.get('object_id')
# Se informou content_type, deve informar object_id
if content_type and not object_id:
raise ValidationError(
_('Se informar o tipo específico, deve informar o ID do tipo.'))
# Se informou object_id, deve informar content_type
if object_id and not content_type:
raise ValidationError(
_('Se informar o ID do tipo, deve informar o tipo específico.'))
# Valida se o object_id existe para o content_type
if content_type and object_id:
model_class = content_type.model_class()
if not model_class.objects.filter(pk=object_id).exists():
raise ValidationError(
_('O tipo específico informado não existe.'))
# Valida extensão do arquivo
arquivo = cleaned_data.get('arquivo')
if arquivo and hasattr(arquivo, 'name'):
if not arquivo.name.lower().endswith('.docx'):
raise ValidationError(
_('O arquivo deve ser um documento .docx'))
return cleaned_data
class SaplSearchForm(ModelSearchForm):
def search(self):

62
sapl/base/migrations/0061_documenttemplate.py

@ -0,0 +1,62 @@
# Generated by Django 2.2 on 2026-01-30 00:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('base', '0060_auto_20240812_1628'),
]
operations = [
migrations.CreateModel(
name='DocumentTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=100, verbose_name='Nome do Template')),
('descricao', models.TextField(blank=True, verbose_name='Descrição')),
('tipo_conteudo', models.CharField(
choices=[
('proposicao', 'Proposição'),
('materia', 'Matéria Legislativa'),
('docacessorio', 'Documento Acessório'),
('docadm', 'Documento Administrativo'),
('norma', 'Norma Jurídica'),
],
max_length=20,
verbose_name='Tipo de Conteúdo',
help_text='Tipo de documento ao qual este template se aplica'
)),
('object_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='ID do Tipo Específico')),
('arquivo', models.FileField(
upload_to='templates/',
verbose_name='Arquivo do Template',
help_text='Arquivo .docx com o template do documento'
)),
('ativo', models.BooleanField(default=True, verbose_name='Ativo')),
('padrao', models.BooleanField(
default=False,
verbose_name='Padrão',
help_text='Se marcado, será o template padrão para este tipo de conteúdo'
)),
('data_criacao', models.DateTimeField(auto_now_add=True, verbose_name='Data de Criação')),
('data_modificacao', models.DateTimeField(auto_now=True, verbose_name='Data de Modificação')),
('content_type', models.ForeignKey(
blank=True,
help_text='Tipo específico de documento (ex: Projeto de Lei, Requerimento)',
null=True,
on_delete=django.db.models.deletion.PROTECT,
to='contenttypes.ContentType',
verbose_name='Tipo Específico'
)),
],
options={
'verbose_name': 'Template de Documento',
'verbose_name_plural': 'Templates de Documentos',
'ordering': ['tipo_conteudo', 'nome'],
},
),
]

139
sapl/base/models.py

@ -4,7 +4,7 @@ from django.contrib.postgres.fields.jsonb import JSONField
from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.db.models.deletion import CASCADE
from django.db.models.deletion import CASCADE, PROTECT
from django.db.models.signals import post_migrate
from django.db.utils import DEFAULT_DB_ALIAS
from django.utils.translation import ugettext_lazy as _
@ -13,6 +13,14 @@ from sapl.utils import (LISTA_DE_UFS, YES_NO_CHOICES,
get_settings_auth_user_model, models_with_gr_for_model)
TIPO_CONTEUDO_TEMPLATE = (
('proposicao', _('Proposição')),
('materia', _('Matéria Legislativa')),
('docacessorio', _('Documento Acessório')),
('docadm', _('Documento Administrativo')),
('norma', _('Norma Jurídica')),
)
DOC_ADM_OSTENSIVO = 'O'
DOC_ADM_RESTRITIVO = 'R'
@ -482,3 +490,132 @@ class Metadata(models.Model):
def __str__(self):
return f'Metadata de {self.content_object}'
class DocumentTemplate(models.Model):
"""
Template de documento para ser usado na criação de novos documentos
via OnlyOffice. Permite definir cabeçalho, rodapé e formatação padrão.
"""
nome = models.CharField(
max_length=100,
verbose_name=_('Nome do Template'))
descricao = models.TextField(
blank=True,
verbose_name=_('Descrição'))
tipo_conteudo = models.CharField(
max_length=20,
choices=TIPO_CONTEUDO_TEMPLATE,
verbose_name=_('Tipo de Conteúdo'),
help_text=_('Tipo de documento ao qual este template se aplica'))
# GenericFK para tipo específico (opcional)
# Ex: TipoMateriaLegislativa, TipoProposicao, etc.
content_type = models.ForeignKey(
ContentType,
null=True,
blank=True,
on_delete=PROTECT,
verbose_name=_('Tipo Específico'),
help_text=_('Tipo específico de documento (ex: Projeto de Lei, Requerimento)'))
object_id = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name=_('ID do Tipo Específico'))
tipo_especifico = GenericForeignKey('content_type', 'object_id')
arquivo = models.FileField(
upload_to='templates/',
verbose_name=_('Arquivo do Template'),
help_text=_('Arquivo .docx com o template do documento'))
ativo = models.BooleanField(
default=True,
verbose_name=_('Ativo'))
padrao = models.BooleanField(
default=False,
verbose_name=_('Padrão'),
help_text=_('Se marcado, será o template padrão para este tipo de conteúdo'))
data_criacao = models.DateTimeField(
auto_now_add=True,
verbose_name=_('Data de Criação'))
data_modificacao = models.DateTimeField(
auto_now=True,
verbose_name=_('Data de Modificação'))
class Meta:
verbose_name = _('Template de Documento')
verbose_name_plural = _('Templates de Documentos')
ordering = ['tipo_conteudo', 'nome']
def __str__(self):
if self.tipo_especifico:
return f'{self.nome} ({self.get_tipo_conteudo_display()} - {self.tipo_especifico})'
return f'{self.nome} ({self.get_tipo_conteudo_display()})'
def save(self, *args, **kwargs):
# Se este template está sendo marcado como padrão,
# desmarcar outros templates padrão do mesmo tipo
if self.padrao:
qs = DocumentTemplate.objects.filter(
tipo_conteudo=self.tipo_conteudo,
padrao=True
)
if self.pk:
qs = qs.exclude(pk=self.pk)
# Se tem tipo específico, filtrar apenas os do mesmo tipo
if self.content_type and self.object_id:
qs = qs.filter(
content_type=self.content_type,
object_id=self.object_id
)
else:
# Se não tem tipo específico, desmarcar apenas os genéricos
qs = qs.filter(content_type__isnull=True)
qs.update(padrao=False)
super().save(*args, **kwargs)
@classmethod
def get_template_for(cls, tipo_conteudo, tipo_especifico=None):
"""
Busca o template mais adequado para o tipo de conteúdo.
Prioridade: específico > genérico > None
Args:
tipo_conteudo: string com o tipo (proposicao, materia, etc.)
tipo_especifico: objeto do tipo específico (TipoMateriaLegislativa, etc.)
Returns:
DocumentTemplate ou None
"""
# Primeiro, tenta encontrar template específico
if tipo_especifico:
ct = ContentType.objects.get_for_model(tipo_especifico)
template = cls.objects.filter(
tipo_conteudo=tipo_conteudo,
content_type=ct,
object_id=tipo_especifico.pk,
ativo=True
).order_by('-padrao', '-data_modificacao').first()
if template:
return template
# Se não encontrou específico, busca genérico
template = cls.objects.filter(
tipo_conteudo=tipo_conteudo,
content_type__isnull=True,
ativo=True
).order_by('-padrao', '-data_modificacao').first()
return template

254
sapl/base/onlyoffice_template_views.py

@ -0,0 +1,254 @@
"""
Views e utilitários para integração com OnlyOffice Document Server
para edição de Templates de Documentos
"""
import hashlib
import json
import logging
import time
from django.conf import settings
from django.contrib.auth.decorators import login_required, permission_required
from django.http import JsonResponse, HttpResponse
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.contrib import messages
from sapl.base.models import DocumentTemplate
logger = logging.getLogger(__name__)
def generate_file_key(template_id, user_id):
"""
Gera uma chave única para o documento no OnlyOffice
A chave muda a cada edição para forçar o OnlyOffice a recarregar
"""
timestamp = str(int(time.time()))
string_to_hash = f"template_{template_id}_user_{user_id}_{timestamp}"
return hashlib.md5(string_to_hash.encode()).hexdigest()
@login_required
@permission_required('base.change_documenttemplate', raise_exception=True)
@require_http_methods(["GET"])
def template_onlyoffice_config(request, pk):
"""
Retorna a configuração JSON para inicializar o editor OnlyOffice
"""
template = get_object_or_404(DocumentTemplate, pk=pk)
# URLs para o OnlyOffice acessar (dentro da rede Docker)
download_url = request.build_absolute_uri(
reverse('sapl.base:template_onlyoffice_download', kwargs={'pk': pk})
)
callback_url = request.build_absolute_uri(
reverse('sapl.base:template_onlyoffice_callback', kwargs={'pk': pk})
)
# Substituir localhost/host externo pelo nome do container na rede Docker
host = request.get_host()
download_url = download_url.replace(f'http://{host}', 'http://sapl-dev:8000')
download_url = download_url.replace(f'https://{host}', 'http://sapl-dev:8000')
callback_url = callback_url.replace(f'http://{host}', 'http://sapl-dev:8000')
callback_url = callback_url.replace(f'https://{host}', 'http://sapl-dev:8000')
# Configuração do documento
document_config = {
"fileType": "docx",
"key": generate_file_key(template.pk, request.user.pk),
"title": f"Template_{template.pk}.docx",
"url": download_url,
}
# Configuração do editor
editor_config = {
"mode": "edit",
"lang": "pt-BR",
"callbackUrl": callback_url,
"user": {
"id": str(request.user.pk),
"name": request.user.get_full_name() or request.user.username,
},
"customization": {
"autosave": True,
"forcesave": True,
"comments": True,
"chat": False,
},
}
config = {
"documentType": "word",
"document": document_config,
"editorConfig": editor_config,
"height": "600px",
"width": "100%",
}
# Adiciona JWT se estiver habilitado
if settings.ONLYOFFICE_JWT_ENABLED and settings.ONLYOFFICE_JWT_SECRET:
import jwt
token = jwt.encode(config, settings.ONLYOFFICE_JWT_SECRET, algorithm='HS256')
config['token'] = token
return JsonResponse(config)
@require_http_methods(["GET"])
def template_onlyoffice_download(request, pk):
"""
Endpoint para o OnlyOffice baixar o documento do template
Se não existe arquivo, retorna um documento em branco
"""
template = get_object_or_404(DocumentTemplate, pk=pk)
# Se já tem arquivo, retorna ele
if template.arquivo:
try:
with open(template.arquivo.path, 'rb') as f:
content = f.read()
response = HttpResponse(
content,
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
response['Content-Disposition'] = f'attachment; filename="Template_{pk}.docx"'
return response
except Exception as e:
logger.error(f"Erro ao ler arquivo do template: {e}")
# Se não tem arquivo, cria um documento em branco usando python-docx
try:
from docx import Document
from docx.shared import Inches, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from io import BytesIO
doc = Document()
# Adiciona cabeçalho padrão
section = doc.sections[0]
header = section.header
header_para = header.paragraphs[0]
header_para.text = f"[CABEÇALHO - {template.nome}]"
header_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Adiciona rodapé padrão
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.text = "[RODAPÉ]"
footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Adiciona conteúdo inicial
doc.add_heading(f'Template: {template.nome}', 0)
doc.add_paragraph(f'Tipo: {template.get_tipo_conteudo_display()}')
if template.descricao:
doc.add_paragraph(f'Descrição: {template.descricao}')
doc.add_paragraph('')
doc.add_paragraph('Edite este documento para configurar o template.')
doc.add_paragraph('O cabeçalho e rodapé definidos aqui serão aplicados aos novos documentos.')
doc.add_paragraph('')
file_stream = BytesIO()
doc.save(file_stream)
file_stream.seek(0)
response = HttpResponse(
file_stream.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
response['Content-Disposition'] = f'attachment; filename="Template_{pk}.docx"'
return response
except ImportError:
logger.error("python-docx não está instalado")
return HttpResponse("Erro: python-docx não instalado", status=500)
@csrf_exempt
@require_http_methods(["POST"])
def template_onlyoffice_callback(request, pk):
"""
Callback chamado pelo OnlyOffice quando o documento é salvo
"""
try:
body = json.loads(request.body.decode('utf-8'))
status = body.get('status')
download_url = body.get('url')
logger.info(f"OnlyOffice callback para template {pk}: status={status}, url={download_url}")
# Status 2 ou 6 significa que o documento foi salvo
if status in [2, 6] and download_url:
# Substitui localhost:8001 por onlyoffice:80 para acesso interno Docker
if 'localhost:8001' in download_url:
download_url = download_url.replace('localhost:8001', 'onlyoffice:80')
template = get_object_or_404(DocumentTemplate, pk=pk)
# Baixa o documento do OnlyOffice
import requests
try:
response = requests.get(download_url, timeout=30)
except Exception as e:
logger.error(f"Erro ao fazer requisição de download: {e}")
return JsonResponse({"error": 1})
if response.status_code == 200:
from django.core.files.base import ContentFile
filename = f"template_{pk}_{int(time.time())}.docx"
# Remove arquivo antigo se existir
if template.arquivo:
template.arquivo.delete(save=False)
try:
template.arquivo.save(
filename,
ContentFile(response.content),
save=True
)
logger.info(f"Template salvo com sucesso: {filename}")
return JsonResponse({"error": 0})
except Exception as e:
logger.error(f"Erro ao salvar arquivo: {e}")
return JsonResponse({"error": 1})
else:
logger.error(f"Erro ao baixar documento: status={response.status_code}")
return JsonResponse({"error": 1})
return JsonResponse({"error": 0})
except Exception as e:
logger.error(f"Erro no callback OnlyOffice: {e}")
return JsonResponse({"error": 1})
@login_required
@permission_required('base.change_documenttemplate', raise_exception=True)
def template_onlyoffice_editor(request, pk):
"""
Renderiza a página com o editor OnlyOffice integrado para editar template
"""
template = get_object_or_404(DocumentTemplate, pk=pk)
# URL do OnlyOffice acessível pelo navegador do usuário
onlyoffice_url = settings.ONLYOFFICE_URL
if 'onlyoffice:' in onlyoffice_url or 'onlyoffice/' in onlyoffice_url:
protocol = 'https' if request.is_secure() else 'http'
host = request.get_host().split(':')[0]
onlyoffice_url = f"{protocol}://{host}:8001"
context = {
'documento': template,
'documento_tipo': 'Template de Documento',
'documento_titulo': template.nome,
'documento_descricao': template.descricao or f'Template para {template.get_tipo_conteudo_display()}',
'onlyoffice_url': onlyoffice_url,
'config_url': reverse('sapl.base:template_onlyoffice_config', kwargs={'pk': pk}),
'voltar_url': reverse('sapl.base:documenttemplate_detail', kwargs={'pk': pk}),
}
return render(request, 'onlyoffice/onlyoffice_editor.html', context)

19
sapl/base/urls.py

@ -8,7 +8,11 @@ from django.views.generic.base import RedirectView, TemplateView
from sapl.base.views import (AutorCrud, ConfirmarEmailView, TipoAutorCrud, get_estatistica,
RecuperarSenhaEmailView, RecuperarSenhaFinalizadoView,
RecuperarSenhaConfirmaView, RecuperarSenhaCompletoView, IndexView, UserCrud,
GuiaProjetoView, FluxoProposicoesView, CriarAutorAjaxView)
GuiaProjetoView, FluxoProposicoesView, CriarAutorAjaxView,
DocumentTemplateCrud)
from sapl.base.onlyoffice_template_views import (
template_onlyoffice_editor, template_onlyoffice_config,
template_onlyoffice_download, template_onlyoffice_callback)
from sapl.settings import MEDIA_URL, LOGOUT_REDIRECT_URL
from .apps import AppConfig
from .views import (LoginSapl, AlterarSenha, AppConfigCrud, CasaLegislativaCrud,
@ -53,6 +57,19 @@ urlpatterns = [
url(r'^sistema/autor/criar-ajax/$', CriarAutorAjaxView.as_view(), name='criar_autor_ajax'),
url(r'^sistema/autor/', include(AutorCrud.get_urls())),
# Template de Documento - CRUD
url(r'^sistema/template-documento/', include(DocumentTemplateCrud.get_urls())),
# Template de Documento - OnlyOffice
url(r'^sistema/template-documento/(?P<pk>\d+)/onlyoffice/editor$',
template_onlyoffice_editor, name='template_onlyoffice_editor'),
url(r'^sistema/template-documento/(?P<pk>\d+)/onlyoffice/config$',
template_onlyoffice_config, name='template_onlyoffice_config'),
url(r'^sistema/template-documento/(?P<pk>\d+)/onlyoffice/download$',
template_onlyoffice_download, name='template_onlyoffice_download'),
url(r'^sistema/template-documento/(?P<pk>\d+)/onlyoffice/callback$',
template_onlyoffice_callback, name='template_onlyoffice_callback'),
url(r'^sistema/guia-projeto/$', GuiaProjetoView.as_view(),
name='guia_projeto'),
url(r'^sistema/fluxo-proposicoes/$', FluxoProposicoesView.as_view(),

113
sapl/base/views.py

@ -52,8 +52,9 @@ from sapl.settings import EMAIL_SEND_USER, RATE_LIMITER_RATE
from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured,
SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, google_recaptcha_configured,
get_client_ip, sapn_is_enabled, is_weak_password, ratelimit_ip)
from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, EstatisticasAcessoNormasForm)
from .models import AppConfig, CasaLegislativa
from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm,
EstatisticasAcessoNormasForm, DocumentTemplateForm)
from .models import AppConfig, CasaLegislativa, DocumentTemplate
def get_casalegislativa():
@ -1747,3 +1748,111 @@ class CriarAutorAjaxView(PermissionRequiredMixin, FormView):
'success': False,
'error': f'Erro ao criar autor: {str(e)}'
}, status=500)
class DocumentTemplateCrud(CrudAux):
model = DocumentTemplate
help_topic = 'template-documento'
class BaseMixin(CrudAux.BaseMixin):
list_field_names = ['nome', 'tipo_conteudo', 'ativo', 'padrao']
form_class = DocumentTemplateForm
class ListView(CrudAux.ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _('Templates de Documentos')
return context
class DetailView(CrudAux.DetailView):
template_name = 'base/documenttemplate_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Sempre adiciona link para editar via OnlyOffice
context['onlyoffice_edit_url'] = reverse(
'sapl.base:template_onlyoffice_editor',
kwargs={'pk': self.object.pk}
)
return context
def hook_tipo_especifico(self, obj):
if obj.tipo_especifico:
return 'Tipo Específico', str(obj.tipo_especifico)
return 'Tipo Específico', _('Nenhum (template genérico)')
class CreateView(CrudAux.CreateView):
template_name = 'base/documenttemplate_form.html'
def form_valid(self, form):
# Salva o objeto primeiro
response = super().form_valid(form)
# Se não foi enviado arquivo, cria um documento em branco
if not self.object.arquivo:
self._criar_arquivo_branco()
return response
def _criar_arquivo_branco(self):
"""Cria um arquivo .docx em branco com cabeçalho e rodapé padrão"""
try:
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from django.core.files.base import ContentFile
from io import BytesIO
doc = Document()
# Configura cabeçalho padrão
section = doc.sections[0]
header = section.header
header_para = header.paragraphs[0]
header_para.text = f"[CABEÇALHO - {self.object.nome}]"
header_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Configura rodapé padrão
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.text = "[RODAPÉ]"
footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Adiciona conteúdo inicial
doc.add_heading(f'Template: {self.object.nome}', 0)
doc.add_paragraph(f'Tipo: {self.object.get_tipo_conteudo_display()}')
if self.object.descricao:
doc.add_paragraph(f'Descrição: {self.object.descricao}')
doc.add_paragraph('')
doc.add_paragraph('Edite este documento para configurar o template.')
doc.add_paragraph('O cabeçalho e rodapé definidos aqui serão aplicados aos novos documentos.')
# Salva em memória
file_stream = BytesIO()
doc.save(file_stream)
file_stream.seek(0)
# Salva no modelo
filename = f"template_{self.object.pk}.docx"
self.object.arquivo.save(filename, ContentFile(file_stream.getvalue()), save=True)
except ImportError:
pass # python-docx não instalado, ignora
def get_success_url(self):
# Sempre redireciona para o editor OnlyOffice após criação
return reverse(
'sapl.base:template_onlyoffice_editor',
kwargs={'pk': self.object.pk}
)
class UpdateView(CrudAux.UpdateView):
template_name = 'base/documenttemplate_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Sempre adiciona link para editar via OnlyOffice
context['onlyoffice_edit_url'] = reverse(
'sapl.base:template_onlyoffice_editor',
kwargs={'pk': self.object.pk}
)
return context

78
sapl/materia/onlyoffice_materia_views.py

@ -121,7 +121,43 @@ def materia_onlyoffice_download(request, pk):
except Exception as e:
logger.error(f"Erro ao ler arquivo: {e}")
# Se não tem arquivo, cria um documento em branco usando python-docx
# Tenta criar documento usando template
try:
from sapl.utils_template import criar_documento_com_template, criar_documento_em_branco
# Busca tipo específico da matéria (TipoMateriaLegislativa)
tipo_especifico = materia.tipo if materia.tipo else None
file_stream = criar_documento_com_template(
'materia',
tipo_especifico,
{
'titulo': f'{materia.tipo} {materia.numero}/{materia.ano}',
'descricao': materia.ementa,
'tipo_display': 'Matéria Legislativa'
}
)
# Se não encontrou template, cria documento em branco
if not file_stream:
file_stream = criar_documento_em_branco(
f'{materia.tipo} {materia.numero}/{materia.ano}',
materia.ementa,
'Matéria Legislativa'
)
if file_stream:
response = HttpResponse(
file_stream.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
response['Content-Disposition'] = f'attachment; filename="Materia_{pk}.docx"'
return response
except Exception as e:
logger.error(f"Erro ao criar documento: {e}")
# Fallback: Se python-docx não está instalado ou houve erro
try:
from docx import Document
from io import BytesIO
@ -332,7 +368,45 @@ def docacessorio_onlyoffice_download(request, pk):
except Exception as e:
logger.error(f"Erro ao ler arquivo: {e}")
# Se não tem arquivo, cria um documento em branco usando python-docx
# Tenta criar documento usando template
try:
from sapl.utils_template import criar_documento_com_template, criar_documento_em_branco
# Busca tipo específico do documento acessório (TipoDocumento)
tipo_especifico = documento.tipo if documento.tipo else None
descricao = documento.ementa if documento.ementa else f'Matéria: {documento.materia}'
file_stream = criar_documento_com_template(
'docacessorio',
tipo_especifico,
{
'titulo': f'{documento.tipo} - {documento.nome}',
'descricao': descricao,
'tipo_display': 'Documento Acessório'
}
)
# Se não encontrou template, cria documento em branco
if not file_stream:
file_stream = criar_documento_em_branco(
f'{documento.tipo} - {documento.nome}',
descricao,
'Documento Acessório'
)
if file_stream:
response = HttpResponse(
file_stream.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
response['Content-Disposition'] = f'attachment; filename="DocAcessorio_{pk}.docx"'
return response
except Exception as e:
logger.error(f"Erro ao criar documento: {e}")
# Fallback: Se python-docx não está instalado ou houve erro
try:
from docx import Document
from io import BytesIO

40
sapl/materia/onlyoffice_views.py

@ -130,7 +130,43 @@ def onlyoffice_download(request, pk):
except Exception as e:
logger.error(f"Erro ao ler arquivo: {e}")
# Se não tem arquivo, cria um documento em branco usando python-docx
# Tenta criar documento usando template
try:
from sapl.utils_template import criar_documento_com_template, criar_documento_em_branco
# Busca tipo específico da proposição (TipoProposicao)
tipo_especifico = proposicao.tipo if proposicao.tipo else None
file_stream = criar_documento_com_template(
'proposicao',
tipo_especifico,
{
'titulo': f'Proposição {proposicao.tipo}',
'descricao': proposicao.descricao,
'tipo_display': 'Proposição'
}
)
# Se não encontrou template, cria documento em branco
if not file_stream:
file_stream = criar_documento_em_branco(
f'Proposição {proposicao.tipo}',
proposicao.descricao,
'Proposição'
)
if file_stream:
response = HttpResponse(
file_stream.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
response['Content-Disposition'] = f'attachment; filename="Proposicao_{pk}.docx"'
return response
except Exception as e:
logger.error(f"Erro ao criar documento: {e}")
# Fallback: Se python-docx não está instalado ou houve erro
try:
from docx import Document
from io import BytesIO
@ -142,7 +178,6 @@ def onlyoffice_download(request, pk):
doc.add_paragraph('Digite o texto da proposição abaixo:')
doc.add_paragraph('')
# Salva em memória
file_stream = BytesIO()
doc.save(file_stream)
file_stream.seek(0)
@ -155,7 +190,6 @@ def onlyoffice_download(request, pk):
return response
except ImportError:
# Se python-docx não está instalado, retorna erro
logger.error("python-docx não está instalado")
return HttpResponse("Erro: python-docx não instalado", status=500)

38
sapl/norma/onlyoffice_views.py

@ -117,7 +117,43 @@ def norma_onlyoffice_download(request, pk):
except Exception as e:
logger.error(f"Erro ao ler arquivo: {e}")
# Se não tem arquivo, cria um documento em branco usando python-docx
# Tenta criar documento usando template
try:
from sapl.utils_template import criar_documento_com_template, criar_documento_em_branco
# Busca tipo específico da norma (TipoNormaJuridica)
tipo_especifico = norma.tipo if norma.tipo else None
file_stream = criar_documento_com_template(
'norma',
tipo_especifico,
{
'titulo': f'{norma.tipo} {norma.numero}/{norma.ano}',
'descricao': norma.ementa,
'tipo_display': 'Norma Jurídica'
}
)
# Se não encontrou template, cria documento em branco
if not file_stream:
file_stream = criar_documento_em_branco(
f'{norma.tipo} {norma.numero}/{norma.ano}',
norma.ementa,
'Norma Jurídica'
)
if file_stream:
response = HttpResponse(
file_stream.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
response['Content-Disposition'] = f'attachment; filename="Norma_{pk}.docx"'
return response
except Exception as e:
logger.error(f"Erro ao criar documento: {e}")
# Fallback: Se python-docx não está instalado ou houve erro
try:
from docx import Document
from io import BytesIO

38
sapl/protocoloadm/onlyoffice_views.py

@ -117,7 +117,43 @@ def docadm_onlyoffice_download(request, pk):
except Exception as e:
logger.error(f"Erro ao ler arquivo: {e}")
# Se não tem arquivo, cria um documento em branco usando python-docx
# Tenta criar documento usando template
try:
from sapl.utils_template import criar_documento_com_template, criar_documento_em_branco
# Busca tipo específico do documento administrativo (TipoDocumentoAdministrativo)
tipo_especifico = documento.tipo if documento.tipo else None
file_stream = criar_documento_com_template(
'docadm',
tipo_especifico,
{
'titulo': f'{documento.tipo} {documento.numero}/{documento.ano}',
'descricao': documento.assunto,
'tipo_display': 'Documento Administrativo'
}
)
# Se não encontrou template, cria documento em branco
if not file_stream:
file_stream = criar_documento_em_branco(
f'{documento.tipo} {documento.numero}/{documento.ano}',
documento.assunto,
'Documento Administrativo'
)
if file_stream:
response = HttpResponse(
file_stream.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
response['Content-Disposition'] = f'attachment; filename="DocAdm_{pk}.docx"'
return response
except Exception as e:
logger.error(f"Erro ao criar documento: {e}")
# Fallback: Se python-docx não está instalado ou houve erro
try:
from docx import Document
from io import BytesIO

18
sapl/templates/base/documenttemplate_detail.html

@ -0,0 +1,18 @@
{% extends "crud/detail.html" %}
{% load i18n %}
{% block editions %}
<div class="actions btn-group" role="group">
{% if onlyoffice_edit_url %}
<a href="{{ onlyoffice_edit_url }}" class="btn btn-primary">
<i class="fa fa-file-word-o"></i> {% trans 'Editar no OnlyOffice' %}
</a>
{% endif %}
{% if view.update_url %}
<a href="{{ view.update_url }}" class="btn btn-outline-primary">{% trans 'Editar Dados' %}</a>
{% endif %}
{% if view.delete_url %}
<a href="{{ view.delete_url }}" class="btn btn-outline-primary btn-outline-danger">{% trans 'Excluir' %}</a>
{% endif %}
</div>
{% endblock %}

22
sapl/templates/base/documenttemplate_form.html

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% load i18n crispy_forms_tags %}
{% block base_content %}
{% if onlyoffice_edit_url %}
<div class="alert alert-info">
<strong><i class="fa fa-info-circle"></i> {% trans 'Dica:' %}</strong>
{% trans 'Para editar o cabeçalho, rodapé e formatação do template, use o editor OnlyOffice.' %}
<a href="{{ onlyoffice_edit_url }}" class="btn btn-primary btn-sm ml-2">
<i class="fa fa-file-word-o"></i> {% trans 'Abrir no OnlyOffice' %}
</a>
</div>
{% else %}
<div class="alert alert-success">
<strong><i class="fa fa-file-word-o"></i> {% trans 'Novo Template' %}</strong><br>
{% trans 'Após salvar, você será redirecionado automaticamente para o editor OnlyOffice onde poderá configurar o cabeçalho, rodapé e formatação do template.' %}
<br><small class="text-muted">{% trans 'O campo "Arquivo" é opcional. Se não enviar um arquivo, um documento em branco será criado automaticamente.' %}</small>
</div>
{% endif %}
{% crispy form %}
{% endblock base_content %}

7
sapl/templates/base/layouts.yaml

@ -76,3 +76,10 @@ AutorCreate:
OperadorAutor:
{% trans 'Operador de Autor' %}:
- user
DocumentTemplate:
{% trans 'Template de Documento' %}:
- nome:6 tipo_conteudo:6
- content_type:6 object_id:6
- descricao
- arquivo:8 ativo:2 padrao:2

3
sapl/templates/menu_tabelas_auxiliares.yaml

@ -14,6 +14,9 @@
- title: {% trans 'Autores' %}
url: sapl.base:autor_list
css_class: btn btn-link
- title: {% trans 'Templates de Documentos' %}
url: sapl.base:documenttemplate_list
css_class: btn btn-link
{% if not sapl_as_sapn%}
- title: {% trans 'Módulo Parlamentares' %}
css_class: head_title

215
sapl/utils_template.py

@ -0,0 +1,215 @@
"""
Utilitários para aplicar templates de documentos aos novos documentos
criados via OnlyOffice.
"""
import logging
from io import BytesIO
logger = logging.getLogger(__name__)
def criar_documento_com_template(tipo_conteudo, tipo_especifico, dados):
"""
Cria um novo documento usando um template existente.
1. Busca template adequado via DocumentTemplate.get_template_for()
2. Carrega .docx do template com python-docx
3. Mantém cabeçalho/rodapé, limpa ou adapta o corpo
4. Adiciona dados iniciais do documento
5. Retorna BytesIO com documento pronto
Args:
tipo_conteudo: string com o tipo (proposicao, materia, docacessorio, docadm, norma)
tipo_especifico: objeto do tipo específico (TipoMateriaLegislativa, TipoProposicao, etc.)
ou None para usar template genérico
dados: dicionário com dados do documento:
- titulo: título ou identificação do documento
- descricao: descrição, ementa ou assunto
- tipo_display: nome do tipo para exibição (opcional)
Returns:
BytesIO com o documento pronto ou None se não houver template
"""
try:
from docx import Document
from docx.shared import Pt
from sapl.base.models import DocumentTemplate
# Busca o template mais adequado
template = DocumentTemplate.get_template_for(tipo_conteudo, tipo_especifico)
if not template or not template.arquivo:
logger.debug(f"Nenhum template encontrado para {tipo_conteudo}")
return None
logger.info(f"Usando template '{template.nome}' para {tipo_conteudo}")
# Carrega o documento do template
try:
doc = Document(template.arquivo.path)
except Exception as e:
logger.error(f"Erro ao carregar template {template.pk}: {e}")
return None
# Limpa o corpo do documento mantendo cabeçalho e rodapé
# Remove todos os parágrafos existentes
for paragraph in doc.paragraphs:
p = paragraph._element
p.getparent().remove(p)
# Remove todas as tabelas existentes
for table in doc.tables:
t = table._element
t.getparent().remove(t)
# Adiciona o conteúdo inicial do novo documento
titulo = dados.get('titulo', 'Documento')
descricao = dados.get('descricao', '')
tipo_display = dados.get('tipo_display', tipo_conteudo.title())
# Adiciona título
doc.add_heading(titulo, level=0)
# Adiciona informações do documento
if descricao:
if tipo_conteudo in ['proposicao', 'materia', 'norma']:
doc.add_paragraph(f'Ementa: {descricao}')
else:
doc.add_paragraph(f'Assunto: {descricao}')
doc.add_paragraph('')
# Texto orientativo
doc.add_paragraph(f'Digite o texto do documento abaixo:')
doc.add_paragraph('')
doc.add_paragraph('')
# Salva em memória
file_stream = BytesIO()
doc.save(file_stream)
file_stream.seek(0)
return file_stream
except ImportError:
logger.error("python-docx não está instalado")
return None
except Exception as e:
logger.error(f"Erro ao criar documento com template: {e}")
return None
def criar_documento_em_branco(titulo, descricao, tipo_documento='Documento'):
"""
Cria um documento em branco básico quando não template disponível.
Fallback para manter compatibilidade com o comportamento anterior.
Args:
titulo: título ou identificação do documento
descricao: descrição, ementa ou assunto
tipo_documento: tipo para exibição (ex: 'Proposição', 'Matéria Legislativa')
Returns:
BytesIO com o documento ou None em caso de erro
"""
try:
from docx import Document
from io import BytesIO
doc = Document()
doc.add_heading(titulo, 0)
if descricao:
doc.add_paragraph(f'Ementa: {descricao}')
doc.add_paragraph('')
doc.add_paragraph(f'Digite o texto do documento abaixo:')
doc.add_paragraph('')
file_stream = BytesIO()
doc.save(file_stream)
file_stream.seek(0)
return file_stream
except ImportError:
logger.error("python-docx não está instalado")
return None
except Exception as e:
logger.error(f"Erro ao criar documento em branco: {e}")
return None
def aplicar_template_existente(documento_path, tipo_conteudo, tipo_especifico=None):
"""
Aplica cabeçalho e rodapé de um template a um documento existente.
Útil para documentos que foram criados mas precisam ter o
cabeçalho/rodapé atualizado.
Args:
documento_path: caminho para o documento .docx existente
tipo_conteudo: string com o tipo (proposicao, materia, etc.)
tipo_especifico: objeto do tipo específico ou None
Returns:
BytesIO com o documento atualizado ou None se não houver template
"""
try:
from docx import Document
from sapl.base.models import DocumentTemplate
from copy import deepcopy
# Busca o template
template = DocumentTemplate.get_template_for(tipo_conteudo, tipo_especifico)
if not template or not template.arquivo:
logger.debug(f"Nenhum template encontrado para aplicar")
return None
# Carrega o template e o documento existente
template_doc = Document(template.arquivo.path)
doc = Document(documento_path)
# Copia cabeçalho do template para o documento
if template_doc.sections:
template_section = template_doc.sections[0]
for section in doc.sections:
# Copia cabeçalho
if template_section.header:
for para in section.header.paragraphs:
p = para._element
p.getparent().remove(p)
for para in template_section.header.paragraphs:
new_para = section.header.add_paragraph()
new_para.text = para.text
new_para.style = para.style
new_para.alignment = para.alignment
# Copia rodapé
if template_section.footer:
for para in section.footer.paragraphs:
p = para._element
p.getparent().remove(p)
for para in template_section.footer.paragraphs:
new_para = section.footer.add_paragraph()
new_para.text = para.text
new_para.style = para.style
new_para.alignment = para.alignment
# Salva em memória
file_stream = BytesIO()
doc.save(file_stream)
file_stream.seek(0)
return file_stream
except ImportError:
logger.error("python-docx não está instalado")
return None
except Exception as e:
logger.error(f"Erro ao aplicar template a documento existente: {e}")
return None
Loading…
Cancel
Save