Spaces:
Sleeping
Sleeping
| """ | |
| Gerador principal de laudos de avaliação. | |
| """ | |
| import os | |
| import re | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| from docx import Document | |
| from docx.shared import Pt, Inches, Cm | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| from docx.enum.table import WD_TABLE_ALIGNMENT | |
| from .numbering import NumeradorSecoes | |
| from .formatters import ( | |
| criar_paragrafo_formatado, | |
| add_body_text, | |
| add_placeholder_text, | |
| add_section_title, | |
| add_subsection_title, | |
| add_subsubsection_title, | |
| add_simple_table, | |
| configurar_linha_tabela_altura, | |
| criar_celula_cabecalho_tabela, | |
| criar_celula_dados_tabela, | |
| iniciar_secao_paisagem, | |
| iniciar_secao_retrato, | |
| ) | |
| from config.settings import TEMPLATES_DIR, TEXTOS_DIR, OUTPUT_DIR | |
| from models import get_model | |
| from utils.docx_loader import ler_texto_docx, mesclar_documento_fotos | |
| from .anexos import ( | |
| gerar_anexo_metodologia, | |
| gerar_anexo_banco_dados, | |
| gerar_anexo_planilha, | |
| gerar_anexo_graficos, | |
| gerar_anexo_calculo, | |
| ) | |
| class LaudoGenerator: | |
| """Gerador de laudos de avaliação imobiliária.""" | |
| def __init__(self): | |
| self.header_path = TEMPLATES_DIR / "header.docx" | |
| self.output_dir = OUTPUT_DIR | |
| def gerar(self, dados: Dict, motivos_formatados: Dict) -> Optional[Path]: | |
| """ | |
| Gera um laudo completo. | |
| Args: | |
| dados: Dicionário com todos os dados do laudo | |
| motivos_formatados: Dicionário com motivos desvalorizantes formatados | |
| Returns: | |
| Path do arquivo gerado ou None se falhar | |
| """ | |
| try: | |
| # Criar documento | |
| doc = None | |
| if self.header_path.exists(): | |
| try: | |
| doc = Document(str(self.header_path)) | |
| self._substituir_processo_header(doc, dados.get('numero_processo', '')) | |
| except Exception: | |
| # Header inválido ou corrompido, criar documento vazio | |
| doc = None | |
| if doc is None: | |
| doc = Document() | |
| self._configurar_margens(doc) | |
| # Atualizar dados com motivos formatados | |
| dados['motivos_alegados'] = motivos_formatados.get('alegados_texto', '') | |
| dados['motivos_existentes'] = motivos_formatados.get('confirmados_texto', '') | |
| # Gerar seções | |
| self._gerar_cabecalho(doc, dados) | |
| self._gerar_corpo(doc, dados, motivos_formatados) | |
| self._gerar_assinatura(doc, dados) | |
| self._gerar_anexos(doc, dados) | |
| # Mesclar documento de registro fotográfico se fornecido | |
| registro_foto_path = dados.get('registro_fotografico_path') | |
| if registro_foto_path: | |
| doc = mesclar_documento_fotos(doc, Path(registro_foto_path)) | |
| # Salvar | |
| output_path = self._gerar_nome_arquivo(dados) | |
| doc.save(str(output_path)) | |
| return output_path | |
| except Exception as e: | |
| print(f"Erro ao gerar laudo: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return None | |
| def _configurar_margens(self, doc: Document) -> None: | |
| """Configura margens do documento.""" | |
| for section in doc.sections: | |
| section.top_margin = Cm(2.5) | |
| section.bottom_margin = Cm(2.5) | |
| section.left_margin = Cm(3) | |
| section.right_margin = Cm(2) | |
| def _substituir_processo_header(self, doc: Document, numero_processo: str) -> None: | |
| """Substitui número do processo no header.""" | |
| if not numero_processo: | |
| return | |
| for section in doc.sections: | |
| if section.header: | |
| for paragraph in section.header.paragraphs: | |
| if "XXX" in paragraph.text: | |
| new_text = paragraph.text.replace("XXX", numero_processo) | |
| for run in paragraph.runs: | |
| run.clear() | |
| run = paragraph.add_run(new_text) | |
| run.bold = True | |
| run.font.name = 'Arial' | |
| run.font.size = Pt(10) | |
| def _gerar_cabecalho(self, doc: Document, dados: Dict) -> None: | |
| """Gera o cabeçalho/tabela inicial do laudo.""" | |
| # Título | |
| criar_paragrafo_formatado( | |
| doc, "LAUDO DE AVALIAÇÃO", negrito=True, sublinhado=True, tamanho=14, | |
| alinhamento=WD_ALIGN_PARAGRAPH.CENTER | |
| ) | |
| # Número do laudo | |
| criar_paragrafo_formatado( | |
| doc, dados.get('numero_laudo', 'LA_XXX_XXXX'), tamanho=12, | |
| alinhamento=WD_ALIGN_PARAGRAPH.CENTER, espaco_depois=12 | |
| ) | |
| # Tabela de dados | |
| table = doc.add_table(rows=0, cols=2) | |
| table.style = 'Table Grid' | |
| table.alignment = WD_TABLE_ALIGNMENT.CENTER | |
| def add_section_header(text): | |
| row = table.add_row() | |
| row.height = Cm(1.2) | |
| cell = row.cells[0] | |
| cell.merge(row.cells[1]) | |
| criar_celula_cabecalho_tabela(cell, text) | |
| def add_row(label, value): | |
| row = table.add_row() | |
| configurar_linha_tabela_altura(row, 0.6) | |
| criar_celula_dados_tabela(row.cells[0], label, is_label=True) | |
| criar_celula_dados_tabela(row.cells[1], str(value) if value else "") | |
| # Definição das seções | |
| secoes = [ | |
| ("SOLICITAÇÃO", [ | |
| ("Unidade demandante:", 'unidade_demandante'), | |
| ("Requerimento:", 'dados_requerimento'), | |
| ("Requerente/Representante legal:", 'representante_legal'), | |
| ("Motivos desvalorizantes alegados:", 'motivos_alegados'), | |
| ("Valor venal proposto contribuinte:", 'valor_proposto'), | |
| ("Documentação Padrão:", 'documentacao_padrao'), | |
| ("Documentação Específica:", 'documentacao_especifica'), | |
| ]), | |
| ("IMÓVEL OBJETO", [ | |
| ("Endereço:", 'endereco_imovel'), | |
| ("Bairro (Setor/Quarteirão):", 'bairro_imovel'), | |
| ("Lote Fiscal:", 'lote_imovel'), | |
| ("Inscrição:", 'inscricao_imovel'), | |
| ("Finalidade Imóvel:", 'finalidade_imovel'), | |
| ("Área Territorial Total / Privativa:", 'area_territorial'), | |
| ("Área construída:", 'construcoes_imovel'), | |
| ("Valores Venais Guias IPTU:", 'valores_venais'), | |
| ]), | |
| ("ANÁLISE VALOR DE MERCADO", [ | |
| ("Unidade responsável:", 'unidade_responsavel'), | |
| ("Técnico responsável:", 'tecnico_responsavel'), | |
| ("Método de Avaliação:", 'metodo_avaliacao'), | |
| ("Modelo de Avaliação utilizado:", 'modelo_avaliacao'), | |
| ]), | |
| ("CONCLUSÃO TÉCNICA", [ | |
| ("Valores de Mercado:", 'valores_mercado'), | |
| ("Referências:", 'datas_referencia'), | |
| ("Motivos desvalorizantes existentes:", 'motivos_existentes'), | |
| ]), | |
| ] | |
| for header, rows in secoes: | |
| add_section_header(header) | |
| for label, key in rows: | |
| add_row(label, dados.get(key, '')) | |
| for row in table.rows: | |
| row.cells[0].width = Inches(2.5) | |
| row.cells[1].width = Inches(4.0) | |
| def _gerar_corpo(self, doc: Document, dados: Dict, motivos_formatados: Dict) -> None: | |
| """Gera o corpo principal do laudo.""" | |
| num = NumeradorSecoes() | |
| # 1. SOLICITAÇÃO | |
| add_section_title(doc, num.secao(), "SOLICITAÇÃO") | |
| add_subsection_title(doc, num.subsecao(), "CONSIDERAÇÕES INICIAIS") | |
| # Carregar texto de considerações iniciais do template | |
| texto_consideracoes = ler_texto_docx(TEXTOS_DIR / "CONSIDERACOES_INICIAIS.docx") | |
| if texto_consideracoes: | |
| for paragrafo in texto_consideracoes.split("\n\n"): | |
| if paragrafo.strip(): | |
| add_body_text(doc, paragrafo.strip()) | |
| else: | |
| add_placeholder_text(doc, "[Inserir considerações iniciais]") | |
| add_subsection_title(doc, num.subsecao(), "DOCUMENTAÇÃO APRESENTADA") | |
| add_placeholder_text(doc) | |
| # 2. IMÓVEL OBJETO | |
| add_section_title(doc, num.secao(), "IMÓVEL OBJETO") | |
| add_subsection_title(doc, num.subsecao(), "DESCRIÇÃO DO IMÓVEL") | |
| add_placeholder_text(doc) | |
| add_subsection_title(doc, num.subsecao(), "CARACTERÍSTICAS PARTICULARMENTE DESVALORIZANTES") | |
| secoes_motivos = motivos_formatados.get('secoes', []) | |
| if not secoes_motivos: | |
| add_placeholder_text(doc) | |
| else: | |
| for secao in secoes_motivos: | |
| add_subsubsection_title(doc, num.subsubsecao(), secao['titulo']) | |
| p = doc.add_paragraph() | |
| p.add_run(f"Status: {secao['status']}").italic = True | |
| p.paragraph_format.left_indent = Inches(0.5) | |
| add_placeholder_text(doc) | |
| # 3. ANÁLISE DE MERCADO | |
| add_section_title(doc, num.secao(), "ANÁLISE VALOR DE MERCADO") | |
| add_subsection_title(doc, num.subsecao(), "DIAGNÓSTICO DE MERCADO") | |
| add_placeholder_text(doc, "[Inserir diagnóstico de mercado]") | |
| add_subsection_title(doc, num.subsecao(), "METODOLOGIA DE AVALIAÇÃO") | |
| # Tentar gerar metodologia a partir do modelo | |
| modelo_nome = dados.get('modelo_avaliacao', '') | |
| modelo = get_model(modelo_nome) if modelo_nome else None | |
| if modelo: | |
| gerar_anexo_metodologia(doc, modelo, num=num) | |
| else: | |
| add_placeholder_text(doc, "[Inserir metodologia - selecione um modelo]") | |
| # 4. ESPECIFICAÇÃO | |
| add_section_title(doc, num.secao(), "ESPECIFICAÇÃO DA AVALIAÇÃO") | |
| add_body_text(doc, f"Método: {dados.get('metodo_avaliacao', '')}") | |
| add_placeholder_text(doc) | |
| # 5. CONCLUSÃO | |
| add_section_title(doc, num.secao(), "CONCLUSÃO TÉCNICA") | |
| add_body_text(doc, "Em face do acima exposto, esta EAV concluiu que:") | |
| add_subsection_title(doc, num.subsecao(), "SOBRE AS CARACTERÍSTICAS PARTICULARMENTE DESVALORIZANTES") | |
| if not secoes_motivos: | |
| add_body_text(doc, "- não foram identificados motivos desvalorizantes;") | |
| else: | |
| for i, secao in enumerate(secoes_motivos, 1): | |
| if secao['confirmado']: | |
| texto = f"- {secao['titulo'].lower()}, conforme item 2.2.{i};" | |
| else: | |
| texto = f"- quanto ao motivo \"{secao['titulo'].lower()}\", não foi confirmado;" | |
| add_body_text(doc, texto) | |
| add_subsection_title(doc, num.subsecao(), "SOBRE O VALOR DE MERCADO") | |
| valores_mercado_lista = dados.get('valores_mercado_lista', []) | |
| if valores_mercado_lista: | |
| self._gerar_tabela_valores_mercado(doc, valores_mercado_lista) | |
| else: | |
| add_placeholder_text(doc) | |
| add_subsection_title(doc, num.subsecao(), "OBSERVAÇÕES COMPLEMENTARES") | |
| add_placeholder_text(doc, "[Inserir observações]") | |
| # 6. ANEXOS (lista) | |
| add_section_title(doc, num.secao(), "ANEXOS") | |
| for anexo in ["I. BANCO DE DADOS", "II. PLANILHA DE CÁLCULO", "III. GRÁFICOS", | |
| "IV. CÁLCULO DO VALOR", "V. DADOS DO IMÓVEL", "VI. REGISTRO FOTOGRÁFICO"]: | |
| add_body_text(doc, anexo) | |
| def _gerar_tabela_valores_mercado(self, doc: Document, valores_lista: List[Dict]) -> None: | |
| """Gera tabela de valores de mercado.""" | |
| dados_tabela = [ | |
| ["Exercício IPTU", "Ano Base", "VTOTAL Imóvel"] | |
| ] | |
| for item in valores_lista: | |
| ano = item.get('ano', '') | |
| valor = item.get('valor', '') | |
| valor_extenso = item.get('valor_extenso', '') | |
| if ano and valor: | |
| exercicio = str(ano) | |
| ano_base = str(int(ano) - 1) if ano else '' | |
| if valor_extenso: | |
| vtotal = f"R$ {valor} ({valor_extenso})" | |
| else: | |
| vtotal = f"R$ {valor}" | |
| dados_tabela.append([exercicio, ano_base, vtotal]) | |
| if len(dados_tabela) > 1: | |
| add_simple_table(doc, dados_tabela, header_row=True, largura_colunas=[1, 1, 8]) | |
| def _gerar_assinatura(self, doc: Document, dados: Dict) -> None: | |
| """Gera a seção de assinatura.""" | |
| doc.add_paragraph() | |
| data_laudo = dados.get('data_laudo', datetime.now().strftime('%d de %B de %Y')) | |
| criar_paragrafo_formatado( | |
| doc, f"Porto Alegre, {data_laudo}.", | |
| tamanho=11, alinhamento=WD_ALIGN_PARAGRAPH.RIGHT | |
| ) | |
| doc.add_paragraph() | |
| doc.add_paragraph() | |
| p = doc.add_paragraph() | |
| p.paragraph_format.first_line_indent = Inches(0) | |
| p.paragraph_format.left_indent = Inches(0) | |
| p.add_run("_" * 40) | |
| p.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| if dados.get('tecnico_responsavel'): | |
| criar_paragrafo_formatado( | |
| doc, dados.get('tecnico_responsavel'), tamanho=11, | |
| alinhamento=WD_ALIGN_PARAGRAPH.CENTER | |
| ) | |
| p = doc.add_paragraph() | |
| p.paragraph_format.first_line_indent = Inches(0) | |
| p.paragraph_format.left_indent = Inches(0) | |
| run = p.add_run("Equipe de Avaliações\nDAI-RM-SMF") | |
| run.bold = True | |
| run.font.size = Pt(11) | |
| run.font.name = 'Arial' | |
| p.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| def _criar_titulo_anexo(self, doc: Document, titulo: str) -> None: | |
| """Cria título de anexo com formatação padrão (tamanho 10, centralizado).""" | |
| criar_paragrafo_formatado( | |
| doc, titulo, negrito=True, sublinhado=True, tamanho=10, | |
| espaco_antes=18, espaco_depois=12, alinhamento=WD_ALIGN_PARAGRAPH.CENTER | |
| ) | |
| def _gerar_anexos(self, doc: Document, dados: Dict) -> None: | |
| """Gera as páginas de anexos a partir dos dados do modelo.""" | |
| # Tentar carregar o modelo selecionado | |
| modelo_nome = dados.get('modelo_avaliacao', '') | |
| modelo = get_model(modelo_nome) if modelo_nome else None | |
| # ANEXO I - BANCO DE DADOS (orientação paisagem) | |
| iniciar_secao_paisagem(doc) | |
| self._criar_titulo_anexo(doc, "ANEXO I – BANCO DE DADOS") | |
| gerar_anexo_banco_dados(doc, modelo) | |
| # ANEXO II - PLANILHA DE CÁLCULO (volta para retrato) | |
| iniciar_secao_retrato(doc) | |
| self._criar_titulo_anexo(doc, "ANEXO II – PLANILHA DE CÁLCULO E RESULTADOS ESTATÍSTICOS") | |
| gerar_anexo_planilha(doc, modelo) | |
| # ANEXO III - GRÁFICOS | |
| doc.add_page_break() | |
| self._criar_titulo_anexo(doc, "ANEXO III – GRÁFICOS") | |
| gerar_anexo_graficos(doc, modelo) | |
| # ANEXO IV - CÁLCULO DO VALOR | |
| doc.add_page_break() | |
| self._criar_titulo_anexo(doc, "ANEXO IV – CÁLCULO DO VALOR") | |
| gerar_anexo_calculo(doc, modelo) | |
| # ANEXO V - DADOS E LOCALIZAÇÃO (placeholder manual) | |
| doc.add_page_break() | |
| self._criar_titulo_anexo(doc, "ANEXO V – DADOS E LOCALIZAÇÃO DO IMÓVEL") | |
| add_placeholder_text(doc, "[Inserir dados e localização do imóvel]") | |
| # ANEXO VI - REGISTRO FOTOGRÁFICO | |
| # Se houver documento de fotos, não gerar título nem placeholder | |
| # (o documento já contém o título do anexo) | |
| registro_foto_path = dados.get('registro_fotografico_path') | |
| if not registro_foto_path: | |
| doc.add_page_break() | |
| self._criar_titulo_anexo(doc, "ANEXO VI – REGISTRO FOTOGRÁFICO") | |
| add_placeholder_text(doc, "[Inserir registro fotográfico]") | |
| def _gerar_nome_arquivo(self, dados: Dict) -> Path: | |
| """Gera nome do arquivo de saída.""" | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| inscricao = dados.get('inscricao_imovel', 'sem_inscricao') | |
| numero = dados.get('numero_laudo', 'sem_numero') | |
| inscricao_safe = re.sub(r'[^\w\-]', '_', inscricao) | |
| numero_safe = re.sub(r'[^\w\-]', '_', numero) | |
| filename = f"Laudo_{numero_safe}_{inscricao_safe}_{timestamp}.docx" | |
| return self.output_dir / filename | |