mirror of https://github.com/interlegis/sapl.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
364 lines
13 KiB
364 lines
13 KiB
"""
|
|
Utilitários para aplicar templates de documentos aos novos documentos
|
|
criados via OnlyOffice.
|
|
"""
|
|
import logging
|
|
import os
|
|
from io import BytesIO
|
|
|
|
from django.core.files.base import ContentFile
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _forcar_fonte_estilo(style):
|
|
"""Força Arial em todos os slots de fonte de um estilo,
|
|
sobrescrevendo qualquer fonte de tema."""
|
|
from docx.oxml.ns import qn
|
|
|
|
style.font.name = 'Arial'
|
|
rPr = style.element.get_or_add_rPr()
|
|
rFonts = rPr.get_or_add_rFonts()
|
|
rFonts.set(qn('w:ascii'), 'Arial')
|
|
rFonts.set(qn('w:hAnsi'), 'Arial')
|
|
rFonts.set(qn('w:eastAsia'), 'Arial')
|
|
rFonts.set(qn('w:cs'), 'Arial')
|
|
|
|
|
|
def criar_documento_padrao():
|
|
"""
|
|
Cria um Document python-docx com fonte Arial como padrão.
|
|
Configura o estilo 'Normal', 'Title' e os estilos de heading para usar Arial,
|
|
forçando em todos os slots de fonte (ascii, hAnsi, eastAsia, cs).
|
|
"""
|
|
from docx import Document
|
|
from docx.shared import Pt
|
|
|
|
doc = Document()
|
|
|
|
# Define Arial como fonte padrão no estilo Normal
|
|
style = doc.styles['Normal']
|
|
style.font.size = Pt(12)
|
|
_forcar_fonte_estilo(style)
|
|
|
|
# Define Arial no estilo Title (usado por add_heading(text, 0))
|
|
if 'Title' in doc.styles:
|
|
_forcar_fonte_estilo(doc.styles['Title'])
|
|
|
|
# Define Arial nos estilos de heading
|
|
for i in range(1, 10):
|
|
heading_style_name = f'Heading {i}'
|
|
if heading_style_name in doc.styles:
|
|
_forcar_fonte_estilo(doc.styles[heading_style_name])
|
|
|
|
return doc
|
|
|
|
|
|
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
|
|
|
|
# Substitui placeholders no corpo do documento
|
|
# Placeholders suportados: {titulo}, {ementa}, {descricao}, {tipo}, {data}
|
|
titulo = dados.get('titulo', 'Documento')
|
|
descricao = dados.get('descricao', '')
|
|
tipo_display = dados.get('tipo_display', tipo_conteudo.title())
|
|
|
|
from datetime import date
|
|
data_atual = date.today().strftime('%d/%m/%Y')
|
|
|
|
placeholders = {
|
|
'{titulo}': titulo,
|
|
'{ementa}': descricao,
|
|
'{descricao}': descricao,
|
|
'{tipo}': tipo_display,
|
|
'{data}': data_atual,
|
|
}
|
|
|
|
# Substitui placeholders nos parágrafos
|
|
for paragraph in doc.paragraphs:
|
|
for placeholder, valor in placeholders.items():
|
|
if placeholder in paragraph.text:
|
|
paragraph.text = paragraph.text.replace(placeholder, valor)
|
|
|
|
# Substitui placeholders nas tabelas
|
|
for table in doc.tables:
|
|
for row in table.rows:
|
|
for cell in row.cells:
|
|
for paragraph in cell.paragraphs:
|
|
for placeholder, valor in placeholders.items():
|
|
if placeholder in paragraph.text:
|
|
paragraph.text = paragraph.text.replace(placeholder, valor)
|
|
|
|
# Substitui placeholders no cabeçalho e rodapé
|
|
for section in doc.sections:
|
|
for header_para in section.header.paragraphs:
|
|
for placeholder, valor in placeholders.items():
|
|
if placeholder in header_para.text:
|
|
header_para.text = header_para.text.replace(placeholder, valor)
|
|
for footer_para in section.footer.paragraphs:
|
|
for placeholder, valor in placeholders.items():
|
|
if placeholder in footer_para.text:
|
|
footer_para.text = footer_para.text.replace(placeholder, valor)
|
|
|
|
# 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 há 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 io import BytesIO
|
|
|
|
doc = criar_documento_padrao()
|
|
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 já 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
|
|
|
|
|
|
def adicionar_cabecalho_materia(materia):
|
|
"""
|
|
Adiciona cabeçalho com identificação da matéria no início do documento.
|
|
|
|
Adiciona um parágrafo no início do documento com o formato:
|
|
"TIPO NÚMERO / ANO" (ex: "INDICAÇÃO 10 / 2026")
|
|
|
|
Args:
|
|
materia: objeto MateriaLegislativa com texto_original preenchido
|
|
|
|
Returns:
|
|
True se o cabeçalho foi adicionado com sucesso, False caso contrário
|
|
"""
|
|
if not materia.texto_original:
|
|
logger.warning(f"Matéria {materia.pk} não possui texto_original")
|
|
return False
|
|
|
|
# Verifica se é um arquivo .docx
|
|
arquivo_path = materia.texto_original.path
|
|
extensao = os.path.splitext(arquivo_path)[1].lower()
|
|
|
|
if extensao not in ['.docx', '.doc']:
|
|
logger.info(f"Arquivo {arquivo_path} não é .docx, pulando adição de cabeçalho")
|
|
return False
|
|
|
|
try:
|
|
from docx import Document
|
|
from docx.shared import Pt, Inches
|
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
from docx.oxml.ns import qn
|
|
|
|
def _forcar_fonte_arial(run):
|
|
"""Força Arial em todos os slots de fonte do run,
|
|
sobrescrevendo qualquer fonte de tema do documento."""
|
|
run.bold = True
|
|
run.font.name = 'Arial'
|
|
run.font.size = Pt(14)
|
|
rPr = run._element.get_or_add_rPr()
|
|
rFonts = rPr.get_or_add_rFonts()
|
|
rFonts.set(qn('w:ascii'), 'Arial')
|
|
rFonts.set(qn('w:hAnsi'), 'Arial')
|
|
rFonts.set(qn('w:eastAsia'), 'Arial')
|
|
rFonts.set(qn('w:cs'), 'Arial')
|
|
|
|
# Carrega o documento
|
|
doc = Document(arquivo_path)
|
|
|
|
# Monta o texto do cabeçalho: "TIPO NÚMERO / ANO"
|
|
tipo_nome = str(materia.tipo).upper()
|
|
cabecalho_texto = f"{tipo_nome} {materia.numero} / {materia.ano}"
|
|
|
|
# Insere o cabeçalho no início do documento
|
|
# Precisamos inserir antes do primeiro parágrafo
|
|
if doc.paragraphs:
|
|
primeiro_paragrafo = doc.paragraphs[0]
|
|
|
|
# Cria novo parágrafo antes do primeiro
|
|
novo_paragrafo = primeiro_paragrafo.insert_paragraph_before(cabecalho_texto)
|
|
|
|
# Formata o cabeçalho
|
|
novo_paragrafo.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
for run in novo_paragrafo.runs:
|
|
_forcar_fonte_arial(run)
|
|
|
|
# Adiciona linha em branco após o cabeçalho
|
|
primeiro_paragrafo.insert_paragraph_before('')
|
|
else:
|
|
# Documento vazio, adiciona o cabeçalho como primeiro parágrafo
|
|
paragrafo = doc.add_paragraph(cabecalho_texto)
|
|
paragrafo.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
for run in paragrafo.runs:
|
|
_forcar_fonte_arial(run)
|
|
doc.add_paragraph('')
|
|
|
|
# Salva o documento modificado
|
|
file_stream = BytesIO()
|
|
doc.save(file_stream)
|
|
file_stream.seek(0)
|
|
|
|
# Atualiza o arquivo da matéria
|
|
nome_arquivo = os.path.basename(arquivo_path)
|
|
materia.texto_original.save(nome_arquivo, ContentFile(file_stream.read()), save=True)
|
|
|
|
logger.info(f"Cabeçalho adicionado à matéria {materia.pk}: {cabecalho_texto}")
|
|
return True
|
|
|
|
except ImportError:
|
|
logger.error("python-docx não está instalado")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Erro ao adicionar cabeçalho à matéria {materia.pk}: {e}")
|
|
return False
|
|
|