gui-sparim's picture
Update app.py
f88a241 verified
"""
app.py
Aplicação Principal - Sistema de Geração de Laudos
Inclui interface gráfica e lógica de geração de documentos DOCX.
"""
import gradio as gr
import os
import re
import shutil
from datetime import datetime
from docx import Document
from docx.shared import Pt, Inches, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
# Importar formatadores DOCX
from docx_formatters import (
NumeradorSecoes,
aplicar_cor_run,
set_cell_shading,
criar_paragrafo_formatado,
add_heading_custom,
add_section_title,
add_subsection_title,
add_subsubsection_title,
add_subsubsubsection_title,
add_body_text,
add_placeholder_text,
add_bullet_text,
formatar_celula_tabela,
add_simple_table,
configurar_linha_tabela_altura,
criar_celula_cabecalho_tabela,
criar_celula_dados_tabela,
inserir_imagem_de_documento,
add_image_placeholder,
)
# Importar funções do módulo utils
from utils import (
aplicar_mascara_monetaria,
processar_upload_pdf,
formatar_valor_monetario,
formatar_documentacao_para_documento,
formatar_valores_mercado_para_documento,
formatar_motivos_desvalorizantes
)
# Importar valores
from valores import (
TEXTO_CONSIDERACOES_INICIAIS,
TEXTO_DIAGNOSTICO_MERCADO,
TEXTO_OBSERVACOES_COMPLEMENTARES,
UNIDADES_DEMANDANTES,
TIPOS_REQUERIMENTO,
UNIDADES_RESPONSAVEIS,
TECNICOS_RESPONSAVEIS,
METODOS_AVALIACAO,
MODELOS_AVALIACAO,
MAX_MOTIVOS_DESVALORIZANTES,
MAX_VALORES_MERCADO,
MAX_DOCUMENTACAO_PADRAO,
MAX_DOCUMENTACAO_ESPECIFICA
)
# Importar carregador de modelos
from modelos_loader import (
modelo_tem_arquivos,
get_status_modelo,
carregar_conteudo_metodologia,
carregar_conteudo_anexos
)
# ============================================================================
# CONFIGURAÇÕES E CONSTANTES
# ============================================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_PATH = os.path.join(SCRIPT_DIR, "modelo_laudo_tributario.docx")
HEADER_PATH = os.path.join(SCRIPT_DIR, "header.docx")
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "laudos_gerados")
os.makedirs(OUTPUT_DIR, exist_ok=True)
WD_ALIGN_VERTICAL = WD_CELL_VERTICAL_ALIGNMENT
# ============================================================================
# INSERÇÃO DE CONTEÚDO DE METODOLOGIA E ANEXOS
# ============================================================================
def inserir_conteudo_metodologia(doc, elementos, numerador):
"""
Insere o conteúdo da metodologia no documento.
Args:
doc: Documento python-docx
elementos: Lista de elementos da metodologia
numerador: Instância de NumeradorSecoes para manter numeração
"""
for elem in elementos:
tipo = elem['tipo']
conteudo = elem.get('conteudo', '')
cor = elem.get('cor')
if tipo == 'secao':
p = add_section_title(doc, numerador.secao(), conteudo)
if cor:
for run in p.runs:
aplicar_cor_run(run, cor)
elif tipo == 'subsecao':
p = add_subsection_title(doc, numerador.subsecao(), conteudo.strip())
if cor:
for run in p.runs:
aplicar_cor_run(run, cor)
elif tipo == 'subsubsecao':
p = add_subsubsection_title(doc, numerador.subsubsecao(), conteudo.strip())
if cor:
for run in p.runs:
aplicar_cor_run(run, cor)
elif tipo == 'subsubsubsecao':
p = add_subsubsubsection_title(doc, numerador.subsubsubsecao(), conteudo.strip())
if cor:
for run in p.runs:
aplicar_cor_run(run, cor)
elif tipo == 'texto' and conteudo:
p = add_body_text(doc, conteudo, indent=True)
if cor:
for run in p.runs:
aplicar_cor_run(run, cor)
elif tipo == 'variavel' and conteudo:
p = add_bullet_text(doc, conteudo)
if cor:
for run in p.runs:
aplicar_cor_run(run, cor)
elif tipo == 'tabela' and conteudo:
add_simple_table(doc, conteudo, header_row=True)
def inserir_conteudo_anexos(doc, elementos):
"""Insere o conteúdo dos anexos no documento."""
if not elementos:
return False
for elem in elementos:
tipo = elem['tipo']
conteudo = elem.get('conteudo', '')
alinhamento = elem.get('alinhamento', 'left')
cor = elem.get('cor')
conteudo_texto = conteudo if isinstance(conteudo, str) else ''
align = WD_ALIGN_PARAGRAPH.CENTER if alinhamento == 'center' else WD_ALIGN_PARAGRAPH.LEFT
if tipo == 'anexo_titulo' or (conteudo_texto and conteudo_texto.upper().startswith('ANEXO')):
doc.add_page_break()
criar_paragrafo_formatado(
doc, conteudo_texto, negrito=True, tamanho=12, cor=cor,
espaco_antes=12, espaco_depois=12, alinhamento=WD_ALIGN_PARAGRAPH.CENTER
)
elif tipo == 'subtitulo_centralizado' or (tipo == 'texto' and alinhamento == 'center' and conteudo_texto):
criar_paragrafo_formatado(
doc, conteudo_texto, negrito=True, tamanho=11, cor=cor,
espaco_antes=10, espaco_depois=6, alinhamento=WD_ALIGN_PARAGRAPH.CENTER
)
elif tipo == 'imagem':
imagem_data = elem.get('imagem_data')
if not inserir_imagem_de_documento(doc, imagem_data):
add_image_placeholder(doc)
elif tipo in ['secao', 'subsecao', 'subsubsecao']:
criar_paragrafo_formatado(
doc, conteudo_texto.strip(), negrito=True, tamanho=11, cor=cor,
espaco_antes=10, espaco_depois=6, alinhamento=align
)
elif tipo == 'texto' and conteudo_texto:
p = add_body_text(doc, conteudo_texto, indent=False)
if alinhamento == 'center':
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
if cor:
for run in p.runs:
aplicar_cor_run(run, cor)
elif tipo == 'variavel' and conteudo_texto:
p = add_bullet_text(doc, conteudo_texto)
if cor:
for run in p.runs:
aplicar_cor_run(run, cor)
elif tipo == 'tabela' and conteudo:
add_simple_table(doc, conteudo, header_row=True)
return True
# ============================================================================
# GERADORES DE SEÇÕES DO LAUDO
# ============================================================================
def gerar_cabecalho(doc, dados):
"""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 e seus campos
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_laudo(doc, dados, motivos_formatados, nome_modelo=None):
"""Gera o corpo principal do laudo de avaliação com numeração automática."""
num = NumeradorSecoes() # Numerador automático
# 1. SOLICITAÇÃO
add_section_title(doc, num.secao(), "SOLICITAÇÃO")
add_subsection_title(doc, num.subsecao(), "CONSIDERAÇÕES INICIAIS")
for paragrafo in TEXTO_CONSIDERACOES_INICIAIS.strip().split('\n\n'):
add_body_text(doc, paragrafo.strip(), indent=True)
add_subsection_title(doc, num.subsecao(), "DOCUMENTAÇÃO APRESENTADA")
add_placeholder_text(doc, indent=True)
# 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, indent=True)
add_subsection_title(doc, num.subsecao(), "CARACTERÍSTICAS PARTICULARMENTE DESVALORIZANTES")
secoes_motivos = motivos_formatados.get('secoes', [])
if not secoes_motivos:
add_placeholder_text(doc, indent=True)
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, indent=True)
# 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")
for paragrafo in TEXTO_DIAGNOSTICO_MERCADO.strip().split('\n\n'):
add_body_text(doc, paragrafo.strip(), indent=True)
# 3.2 METODOLOGIA - inserir conteúdo do modelo se disponível
metodologia_inserida = False
if nome_modelo and nome_modelo != "Outros":
elementos_metodologia = carregar_conteudo_metodologia(nome_modelo)
if elementos_metodologia:
# Passa o numerador para manter a sequência
inserir_conteudo_metodologia(doc, elementos_metodologia, num)
metodologia_inserida = True
print(f"Metodologia do modelo '{nome_modelo}' inserida.")
if not metodologia_inserida:
add_subsection_title(doc, num.subsecao(), "METODOLOGIA DE AVALIAÇÃO")
add_placeholder_text(doc, indent=True)
add_subsection_title(doc, num.subsecao(), "AVALIAÇÃO DO TERRENO")
add_placeholder_text(doc, indent=True)
# 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', '')}", indent=True)
add_placeholder_text(doc, indent=True)
# CONCLUSÃO (numeração automática continua de onde parou)
add_section_title(doc, num.secao(), "CONCLUSÃO TÉCNICA")
add_body_text(doc, "Em face do acima exposto, esta EAV concluiu que:", indent=True)
# Guardar número da seção de conclusão para referências
secao_conclusao = num.numero_atual(0)
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;", indent=True)
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, indent=True)
add_subsection_title(doc, num.subsecao(), "SOBRE O VALOR DE MERCADO")
# Gerar tabela de valores de mercado
valores_mercado_lista = dados.get('valores_mercado_lista', [])
if valores_mercado_lista:
_gerar_tabela_valores_mercado(doc, valores_mercado_lista)
else:
add_placeholder_text(doc, indent=True)
add_subsection_title(doc, num.subsecao(), "OBSERVAÇÕES COMPLEMENTARES")
texto_obs = TEXTO_OBSERVACOES_COMPLEMENTARES.replace("{{num_paginas}}", "[XX]")
texto_obs = texto_obs.replace("{{modelo_avaliacao}}", dados.get('modelo_avaliacao', '[preencher modelo de avaliação]'))
texto_obs = texto_obs.replace("{{datas_referencia}}", dados.get('datas_referencia', '[preencher datas de referência]'))
for linha in texto_obs.strip().split('\n\n'):
add_body_text(doc, linha.strip(), cor=RGBColor(255, 0, 0), indent=True)
# ANEXOS
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, indent=True)
def _gerar_tabela_valores_mercado(doc, valores_lista):
"""
Gera tabela de valores de mercado no estilo:
| Exercício IPTU | Ano Base | VTOTAL Imóvel |
Args:
doc: Documento python-docx
valores_lista: Lista de dicts com 'ano' e 'valor'
"""
# Cabeçalho
dados_tabela = [
["Exercício IPTU", "Ano Base", "VTOTAL Imóvel"]
]
# Dados
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 ''
# Formatar valor com extenso
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: # Tem dados além do cabeçalho
add_simple_table(doc, dados_tabela, header_row=True, largura_colunas=[1,1,8])
def gerar_assinatura(doc, dados):
"""Gera a seção de assinatura do laudo."""
doc.add_paragraph()
criar_paragrafo_formatado(
doc, f"Porto Alegre, {dados.get('data_laudo', datetime.now().strftime('%d de %B de %Y'))}.",
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 gerar_paginas_anexos(doc, nome_modelo=None):
"""Gera as páginas de anexos do laudo."""
anexos_inseridos = False
if nome_modelo and nome_modelo != "Outros":
elementos_anexos = carregar_conteudo_anexos(nome_modelo)
if elementos_anexos:
anexos_inseridos = inserir_conteudo_anexos(doc, elementos_anexos)
if anexos_inseridos:
print(f"Anexos do modelo '{nome_modelo}' inseridos.")
if not anexos_inseridos:
titulos = [
"ANEXO I – BANCO DE DADOS",
"ANEXO II – PLANILHA DE CÁLCULO E RESULTADOS ESTATÍSTICOS",
"ANEXO III – GRÁFICOS",
"ANEXO IV – CÁLCULO DO VALOR",
"ANEXO V – DADOS E LOCALIZAÇÃO DO IMÓVEL",
"ANEXO VI – REGISTRO FOTOGRÁFICO",
]
for titulo in titulos:
doc.add_page_break()
add_heading_custom(doc, titulo, level=1)
add_placeholder_text(doc)
# ============================================================================
# GERAÇÃO DO LAUDO COMPLETO
# ============================================================================
def gerar_laudo_completo(output_path: str, dados: dict, motivos_desvalorizantes: list, incluir_anexos: bool = True) -> bool:
"""Gera o documento de laudo completo."""
try:
using_template = os.path.exists(HEADER_PATH)
doc = Document(HEADER_PATH) if using_template else Document()
if using_template:
print(f"Utilizando template de cabeçalho: {HEADER_PATH}")
else:
print("Template de cabeçalho não encontrado. Usando documento em branco.")
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)
# Substituir processo no header
numero_processo = dados.get('numero_processo', '')
if using_template and numero_processo:
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)
# Verificar arquivos do modelo
modelo_avaliacao = dados.get('modelo_avaliacao', '')
if modelo_avaliacao and modelo_avaliacao != "Outros":
if modelo_tem_arquivos(modelo_avaliacao):
print(f"Modelo '{modelo_avaliacao}' possui arquivos de metodologia/anexos.")
else:
print(f"Modelo '{modelo_avaliacao}' não possui arquivos específicos.")
motivos_formatados = formatar_motivos_desvalorizantes(motivos_desvalorizantes)
dados['motivos_alegados'] = motivos_formatados['alegados_texto']
dados['motivos_existentes'] = motivos_formatados['confirmados_texto']
gerar_cabecalho(doc, dados)
gerar_corpo_laudo(doc, dados, motivos_formatados, modelo_avaliacao)
gerar_assinatura(doc, dados)
if incluir_anexos:
gerar_paginas_anexos(doc, modelo_avaliacao)
doc.save(output_path)
return True
except Exception as e:
print(f"Erro ao gerar laudo: {e}")
import traceback
traceback.print_exc()
return False
def substituir_placeholders_docx(template_path: str, output_path: str, replacements: dict) -> bool:
"""Substitui placeholders em um template DOCX."""
try:
shutil.copy2(template_path, output_path)
doc = Document(output_path)
def replace_in_paragraph(paragraph, replacements):
for placeholder, value in replacements.items():
if placeholder in paragraph.text:
new_text = paragraph.text.replace(placeholder, str(value) if value else '')
if paragraph.runs:
first_run = paragraph.runs[0]
font_name = first_run.font.name
font_size = first_run.font.size
bold = first_run.font.bold
italic = first_run.font.italic
for run in paragraph.runs:
run.text = ''
first_run.text = new_text
if font_name: first_run.font.name = font_name
if font_size: first_run.font.size = font_size
if bold is not None: first_run.font.bold = bold
if italic is not None: first_run.font.italic = italic
for paragraph in doc.paragraphs:
replace_in_paragraph(paragraph, replacements)
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
replace_in_paragraph(paragraph, replacements)
for section in doc.sections:
if section.header:
for p in section.header.paragraphs:
replace_in_paragraph(p, replacements)
if section.footer:
for p in section.footer.paragraphs:
replace_in_paragraph(p, replacements)
doc.save(output_path)
return True
except Exception as e:
print(f"Erro ao substituir placeholders: {e}")
return False
# ============================================================================
# FUNÇÃO PRINCIPAL DE GERAÇÃO
# ============================================================================
def _criar_lista_valores_mercado(anos, valores):
"""
Cria lista estruturada de valores de mercado para a tabela.
Args:
anos: Lista de anos
valores: Lista de valores monetários
Returns:
Lista de dicts com 'ano', 'valor' e 'valor_extenso'
"""
lista = []
for ano, valor in zip(anos, valores):
ano_str = str(ano).strip() if ano else ''
valor_str = str(valor).strip() if valor else ''
if ano_str and valor_str:
try:
valor_formatado, valor_extenso = formatar_valor_monetario(valor_str)
lista.append({
'ano': ano_str,
'valor': valor_formatado,
'valor_extenso': valor_extenso or ''
})
except Exception as e:
# Se falhar a formatação, usa o valor como está
print(f"Aviso: erro ao formatar valor '{valor_str}': {e}")
lista.append({
'ano': ano_str,
'valor': valor_str,
'valor_extenso': ''
})
return lista
def gerar_documento(
template_path: str, output_dir: str, numero_laudo: str, numero_processo: str, data_laudo: str,
unidade_demandante: str, unidade_demandante_custom: str,
dados_requerimento: str, dados_requerimento_custom: str,
representante_legal: str, valor_proposto: str, ano_proposto: str,
documentacao_padrao: list, documentacao_especifica: list,
motivos_desvalorizantes: list,
inscricao_imovel: str, endereco_imovel: str, bairro_imovel: str,
setor_quarteirao: str, lote_imovel: str, finalidade_imovel: str,
area_territorial: str, construcoes_imovel: str, valores_venais: str,
unidade_responsavel: str, unidade_responsavel_custom: str,
tecnico_responsavel: str, tecnico_responsavel_custom: str,
metodo_avaliacao: str, metodo_avaliacao_custom: str,
modelo_avaliacao: str, modelo_avaliacao_custom: str,
valores_mercado_anos: list, valores_mercado_valores: list,
datas_referencia: str,
gerar_laudo_completo_flag: bool = True
) -> tuple:
"""Função principal que orquestra a geração do documento."""
if not gerar_laudo_completo_flag and not os.path.exists(template_path):
return f"Erro: Template não encontrado em {template_path}", None
def get_valor(dropdown, custom):
return custom if dropdown == "Outros" and custom else dropdown
valor_formatado, valor_extenso = formatar_valor_monetario(valor_proposto)
valor_proposto_completo = f"{valor_formatado} ({valor_extenso})" if valor_extenso else valor_formatado
setor_parts = setor_quarteirao.split(" / ") if " / " in setor_quarteirao else [setor_quarteirao, ""]
setor, quarteirao = (setor_parts[0], setor_parts[1]) if len(setor_parts) > 1 else (setor_parts[0], "")
bairro_completo = f"{bairro_imovel} ({setor} / {quarteirao})"
ano_proposto_str = str(ano_proposto).strip() if ano_proposto else ""
# Criar lista estruturada de valores de mercado para a tabela
valores_mercado_lista = _criar_lista_valores_mercado(valores_mercado_anos, valores_mercado_valores)
dados = {
'numero_laudo': numero_laudo,
'numero_processo': numero_processo,
'data_laudo': data_laudo,
'unidade_demandante': get_valor(unidade_demandante, unidade_demandante_custom),
'dados_requerimento': get_valor(dados_requerimento, dados_requerimento_custom),
'representante_legal': representante_legal,
'valor_proposto': f"{valor_proposto_completo} (referente ao IPTU {ano_proposto_str})" if valor_proposto_completo else valor_proposto_completo,
'documentacao_padrao': formatar_documentacao_para_documento(documentacao_padrao),
'documentacao_especifica': formatar_documentacao_para_documento(documentacao_especifica),
'endereco_imovel': endereco_imovel,
'bairro_imovel': bairro_completo,
'setor_imovel': setor,
'quarteirao_imovel': quarteirao,
'lote_imovel': lote_imovel,
'inscricao_imovel': inscricao_imovel,
'finalidade_imovel': finalidade_imovel,
'area_territorial': area_territorial,
'construcoes_imovel': construcoes_imovel,
'valores_venais': valores_venais,
'unidade_responsavel': get_valor(unidade_responsavel, unidade_responsavel_custom),
'tecnico_responsavel': get_valor(tecnico_responsavel, tecnico_responsavel_custom),
'metodo_avaliacao': get_valor(metodo_avaliacao, metodo_avaliacao_custom),
'modelo_avaliacao': get_valor(modelo_avaliacao, modelo_avaliacao_custom),
'valores_mercado': formatar_valores_mercado_para_documento(valores_mercado_anos, valores_mercado_valores),
'valores_mercado_lista': valores_mercado_lista, # Lista estruturada para tabela
'datas_referencia': datas_referencia,
'motivos_alegados': formatar_motivos_desvalorizantes(motivos_desvalorizantes)['alegados_texto'],
'motivos_existentes': formatar_motivos_desvalorizantes(motivos_desvalorizantes)['confirmados_texto']
}
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
inscricao_safe = re.sub(r'[^\w\-]', '_', inscricao_imovel) if inscricao_imovel else "sem_inscricao"
numero_safe = re.sub(r'[^\w\-]', '_', numero_laudo) if numero_laudo else "sem_numero"
if gerar_laudo_completo_flag:
filename = f"Laudo_{numero_safe}_{inscricao_safe}_{timestamp}.docx"
path = os.path.join(output_dir, filename)
success = gerar_laudo_completo(path, dados, motivos_desvalorizantes, incluir_anexos=True)
else:
filename = f"Cabecalho_{numero_safe}_{inscricao_safe}_{timestamp}.docx"
path = os.path.join(output_dir, filename)
success = substituir_placeholders_docx(template_path, path, dados)
if success:
return f"✅ Documento gerado: {filename}\nLocal: {path}", path
else:
return "❌ Erro ao gerar documento", None
# ============================================================================
# INTERFACE GRÁFICA - CALLBACKS
# ============================================================================
def toggle_custom_field(dropdown_value):
"""Callback para mostrar/esconder campo customizado."""
return gr.update(visible=(dropdown_value == "Outros"))
def adicionar_campo(count, max_count):
"""Adiciona um campo dinâmico."""
new = min(count + 1, max_count)
return [new] + [gr.update(visible=(i < new)) for i in range(max_count)]
def remover_campo(count, max_count):
"""Remove um campo dinâmico."""
new = max(count - 1, 1)
return [new] + [gr.update(visible=(i < new)) for i in range(max_count)]
def gerar_campos_valores_mercado(ano_ini, ano_fim):
"""Gera campos de valores de mercado para intervalo de anos."""
try:
ano_ini, ano_fim = int(ano_ini), int(ano_fim)
except:
return [gr.update(visible=False, value=""), gr.update(value=""), gr.update(value="")] * MAX_VALORES_MERCADO + [gr.update(value="")]
if ano_fim < ano_ini:
ano_ini, ano_fim = ano_fim, ano_ini
anos = list(range(ano_ini, ano_fim + 1))
updates = []
for i in range(MAX_VALORES_MERCADO):
if i < min(len(anos), MAX_VALORES_MERCADO):
updates.extend([gr.update(visible=True), gr.update(value=str(anos[i])), gr.update(value="")])
else:
updates.extend([gr.update(visible=False), gr.update(value=""), gr.update(value="")])
referencias_texto = ", ".join([f"Dezembro/{ano-1}" for ano in anos])
updates.append(gr.update(value=referencias_texto))
return updates
def limpar_campos_valores_mercado():
"""Limpa todos os campos de valores de mercado."""
return [gr.update(visible=False, value=""), gr.update(value=""), gr.update(value="")] * MAX_VALORES_MERCADO + [gr.update(value="")]
# ============================================================================
# INTERFACE GRÁFICA - WRAPPER DE GERAÇÃO
# ============================================================================
def gerar_documento_wrapper(*args):
"""Wrapper que extrai argumentos da interface e chama gerar_documento."""
idx = 0
numero_laudo = args[idx]; idx += 1
numero_processo = args[idx]; idx += 1
data_laudo = args[idx]; idx += 1
unidade_demandante = args[idx]; idx += 1
unidade_demandante_custom = args[idx]; idx += 1
dados_requerimento = args[idx]; idx += 1
dados_requerimento_custom = args[idx]; idx += 1
representante_legal = args[idx]; idx += 1
valor_proposto = args[idx]; idx += 1
ano_proposto = args[idx]; idx += 1
documentacao_padrao = [args[idx+i] for i in range(MAX_DOCUMENTACAO_PADRAO)]
idx += MAX_DOCUMENTACAO_PADRAO
documentacao_especifica = [args[idx+i] for i in range(MAX_DOCUMENTACAO_ESPECIFICA)]
idx += MAX_DOCUMENTACAO_ESPECIFICA
motivos_desvalorizantes = []
for _ in range(MAX_MOTIVOS_DESVALORIZANTES):
desc = args[idx]; idx += 1
aleg = args[idx]; idx += 1
conf = args[idx]; idx += 1
if desc and desc.strip():
motivos_desvalorizantes.append({'descricao': desc.strip(), 'alegado': aleg, 'confirmado': conf})
inscricao_imovel = args[idx]; idx += 1
endereco_imovel = args[idx]; idx += 1
bairro_imovel = args[idx]; idx += 1
setor_quarteirao = args[idx]; idx += 1
lote_imovel = args[idx]; idx += 1
finalidade_imovel = args[idx]; idx += 1
area_territorial = args[idx]; idx += 1
construcoes_imovel = args[idx]; idx += 1
valores_venais = args[idx]; idx += 1
unidade_responsavel = args[idx]; idx += 1
unidade_responsavel_custom = args[idx]; idx += 1
tecnico_responsavel = args[idx]; idx += 1
tecnico_responsavel_custom = args[idx]; idx += 1
metodo_avaliacao = args[idx]; idx += 1
metodo_avaliacao_custom = args[idx]; idx += 1
modelo_avaliacao = args[idx]; idx += 1
modelo_avaliacao_custom = args[idx]; idx += 1
valores_mercado_anos = []
valores_mercado_valores = []
for _ in range(MAX_VALORES_MERCADO):
valores_mercado_anos.append(args[idx]); idx += 1
valores_mercado_valores.append(args[idx]); idx += 1
datas_referencia = args[idx]; idx += 1
return gerar_documento(
TEMPLATE_PATH, OUTPUT_DIR, numero_laudo, numero_processo, data_laudo,
unidade_demandante, unidade_demandante_custom,
dados_requerimento, dados_requerimento_custom,
representante_legal, valor_proposto, ano_proposto,
documentacao_padrao, documentacao_especifica,
motivos_desvalorizantes,
inscricao_imovel, endereco_imovel, bairro_imovel,
setor_quarteirao, lote_imovel, finalidade_imovel,
area_territorial, construcoes_imovel, valores_venais,
unidade_responsavel, unidade_responsavel_custom,
tecnico_responsavel, tecnico_responsavel_custom,
metodo_avaliacao, metodo_avaliacao_custom,
modelo_avaliacao, modelo_avaliacao_custom,
valores_mercado_anos, valores_mercado_valores,
datas_referencia
)
# ============================================================================
# INTERFACE GRÁFICA - CRIAÇÃO
# ============================================================================
def _get_data_atual_formatada():
"""Retorna a data atual formatada em português."""
meses = {
"January": "janeiro", "February": "fevereiro", "March": "março",
"April": "abril", "May": "maio", "June": "junho",
"July": "julho", "August": "agosto", "September": "setembro",
"October": "outubro", "November": "novembro", "December": "dezembro"
}
data = datetime.now().strftime("%d de %B de %Y")
for en, pt in meses.items():
data = data.replace(en, pt)
return data
def criar_interface():
"""Cria a interface Gradio completa."""
with gr.Blocks(title="Sistema de Avaliação de Imóvel", theme=gr.themes.Soft(primary_hue="blue")) as app:
gr.Markdown("# 📋 Sistema de Geração de Documentos")
with gr.Blocks():
gr.Markdown("### Dados gerais:")
with gr.Row():
numero_laudo = gr.Textbox(label="Número do Laudo", placeholder="LA_XXX_2025")
numero_processo = gr.Textbox(label="Processo Administrativo", placeholder="000.000000.00.0")
data_laudo = gr.Textbox(label="Data do Laudo", value=_get_data_atual_formatada())
with gr.Row():
u_demand = gr.Dropdown(UNIDADES_DEMANDANTES, label="Unidade Demandante", value=UNIDADES_DEMANDANTES[0])
u_demand_custom = gr.Textbox(label="Especifique", visible=False)
u_demand.change(toggle_custom_field, u_demand, u_demand_custom)
with gr.Row():
req_tipo = gr.Dropdown(TIPOS_REQUERIMENTO, label="Requerimento", value=TIPOS_REQUERIMENTO[0])
req_custom = gr.Textbox(label="Especifique", visible=False)
req_tipo.change(toggle_custom_field, req_tipo, req_custom)
rep_legal = gr.Textbox(label="Requerente/Representante Legal")
with gr.Blocks():
gr.Markdown("### Valor Proposto pelo contribuinte (preencher somente se houver)")
with gr.Row():
v_proposto = gr.Textbox(label="Valor Proposto (R$)", placeholder="Digite e saia do campo")
a_proposto = gr.Textbox(label="Ano Ref.", value="2025")
v_proposto.blur(aplicar_mascara_monetaria, v_proposto, v_proposto)
# Docs Padrão
with gr.Blocks():
gr.Markdown("### Documentação Padrão")
docs_padrao_inputs = []
docs_padrao_rows = []
for i in range(MAX_DOCUMENTACAO_PADRAO):
with gr.Row(visible=(i==0)) as row:
docs_padrao_inputs.append(gr.Textbox(label=f"Documento {i+1}"))
docs_padrao_rows.append(row)
docs_padrao_count = gr.State(1)
with gr.Row():
gr.Button("➕ Adicionar").click(lambda c: adicionar_campo(c, MAX_DOCUMENTACAO_PADRAO), docs_padrao_count, [docs_padrao_count] + docs_padrao_rows)
gr.Button("➖ Remover").click(lambda c: remover_campo(c, MAX_DOCUMENTACAO_PADRAO), docs_padrao_count, [docs_padrao_count] + docs_padrao_rows)
with gr.Blocks():
# Docs Específica
gr.Markdown("### Documentação Específica")
docs_esp_inputs = []
docs_esp_rows = []
for i in range(MAX_DOCUMENTACAO_ESPECIFICA):
with gr.Row(visible=(i==0)) as row:
docs_esp_inputs.append(gr.Textbox(label=f"Doc Específico {i+1}"))
docs_esp_rows.append(row)
docs_esp_count = gr.State(1)
with gr.Row():
gr.Button("➕ Adicionar").click(lambda c: adicionar_campo(c, MAX_DOCUMENTACAO_ESPECIFICA), docs_esp_count, [docs_esp_count] + docs_esp_rows)
gr.Button("➖ Remover").click(lambda c: remover_campo(c, MAX_DOCUMENTACAO_ESPECIFICA), docs_esp_count, [docs_esp_count] + docs_esp_rows)
with gr.Blocks():
gr.Markdown("### Motivos desvalorizantes")
motivos_rows = []
motivos_desc = []
motivos_aleg = []
motivos_conf = []
for i in range(MAX_MOTIVOS_DESVALORIZANTES):
with gr.Row(visible=(i==0)) as row:
d = gr.Textbox(show_label=False, scale=3, placeholder=f"Motivo Desvalorizante {i+1}")
a = gr.Checkbox(label="Alegado pelo contribuinte")
c = gr.Checkbox(label="Confirmado pelo avaliador")
motivos_desc.append(d); motivos_aleg.append(a); motivos_conf.append(c)
motivos_rows.append(row)
motivos_count = gr.State(1)
with gr.Row():
gr.Button("➕ Adicionar").click(lambda c: adicionar_campo(c, MAX_MOTIVOS_DESVALORIZANTES), motivos_count, [motivos_count] + motivos_rows)
gr.Button("➖ Remover").click(lambda c: remover_campo(c, MAX_MOTIVOS_DESVALORIZANTES), motivos_count, [motivos_count] + motivos_rows)
# --- SEÇÃO 3: IMÓVEL ---
with gr.Blocks():
gr.Markdown("### Imóvel Objeto")
with gr.Row():
pdf_up = gr.File(label="PDF SIAT", type="filepath")
with gr.Row():
btn_pdf = gr.Button("🔍 Processar PDF")
inscricao = gr.Textbox(label="Inscrição", placeholder="Aguardando processamento do PDF ou preenchimento manual")
with gr.Row():
endereco = gr.Textbox(label="Endereço", placeholder="Aguardando processamento do PDF ou preenchimento manual")
bairro = gr.Textbox(label="Bairro", placeholder="Aguardando processamento do PDF ou preenchimento manual")
with gr.Row():
setor_q = gr.Textbox(label="Setor / Quarteirão", placeholder="Aguardando processamento do PDF ou preenchimento manual")
lote = gr.Textbox(label="Lote Fiscal", placeholder="Aguardando processamento do PDF ou preenchimento manual")
with gr.Row():
finalidade = gr.Textbox(label="Finalidade", placeholder="Aguardando processamento do PDF ou preenchimento manual")
area_ter = gr.Textbox(label="Área Territorial", placeholder="Aguardando processamento do PDF ou preenchimento manual")
construcoes = gr.Textbox(label="Construções", lines=2, placeholder="Aguardando processamento do PDF ou preenchimento manual")
val_venais = gr.Textbox(label="Valores Venais", lines=2, placeholder="Aguardando processamento do PDF ou preenchimento manual")
vm_extraidos = gr.Textbox(visible=False)
btn_pdf.click(processar_upload_pdf, pdf_up, [inscricao, endereco, bairro, setor_q, lote, finalidade, area_ter, construcoes, val_venais, vm_extraidos])
# --- SEÇÃO 4: ANÁLISE ---
with gr.Blocks():
gr.Markdown("### Dados do avaliador e do modelo")
with gr.Row():
u_resp = gr.Dropdown(UNIDADES_RESPONSAVEIS, label="Unidade Resp.", value=UNIDADES_RESPONSAVEIS[0])
u_resp_custom = gr.Textbox(label="Esp.", visible=False)
u_resp.change(toggle_custom_field, u_resp, u_resp_custom)
with gr.Row():
tec_resp = gr.Dropdown(TECNICOS_RESPONSAVEIS, label="Técnico", value=TECNICOS_RESPONSAVEIS[0])
tec_resp_custom = gr.Textbox(label="Esp.", visible=False)
tec_resp.change(toggle_custom_field, tec_resp, tec_resp_custom)
with gr.Row():
metodo = gr.Dropdown(METODOS_AVALIACAO, label="Método", value=METODOS_AVALIACAO[0])
metodo_custom = gr.Textbox(label="Esp.", visible=False)
metodo.change(toggle_custom_field, metodo, metodo_custom)
with gr.Row():
modelo = gr.Dropdown(MODELOS_AVALIACAO, label="Modelo", value=MODELOS_AVALIACAO[0])
modelo_custom = gr.Textbox(label="Esp.", visible=False)
modelo_status = gr.Markdown(value=get_status_modelo(MODELOS_AVALIACAO[0]))
def atualizar_status_modelo(modelo_selecionado):
custom_visible = modelo_selecionado == "Outros"
status = get_status_modelo(modelo_selecionado)
return gr.update(visible=custom_visible), status
modelo.change(atualizar_status_modelo, modelo, [modelo_custom, modelo_status])
# --- SEÇÃO 5: CONCLUSÃO ---
with gr.Blocks():
gr.Markdown("### Valores de Mercado (selecione de que ano até que ano haverá análise)")
with gr.Row():
ano_ini = gr.Number(value=2020, label="Ano Inicial")
ano_fim = gr.Number(value=2025, label="Ano Final")
with gr.Column():
btn_gerar_anos = gr.Button("📅 Gerar Campos")
btn_limpar_anos = gr.Button("🗑️ Limpar")
vm_rows = []
vm_anos = []
vm_vals = []
vm_outputs_flat = []
for i in range(MAX_VALORES_MERCADO):
with gr.Row(visible=False) as row:
a = gr.Textbox(label="Ano", interactive=False, scale=1)
v = gr.Textbox(label="Valor", scale=2)
v.blur(aplicar_mascara_monetaria, v, v)
vm_anos.append(a); vm_vals.append(v)
vm_rows.append(row)
vm_outputs_flat.extend([row, a, v])
ref_datas = gr.Textbox(label="Referências (datas-base)")
btn_gerar_anos.click(gerar_campos_valores_mercado, [ano_ini, ano_fim], vm_outputs_flat + [ref_datas])
btn_limpar_anos.click(limpar_campos_valores_mercado, [], vm_outputs_flat + [ref_datas])
# --- GERAÇÃO ---
gr.Markdown("---")
btn_gerar = gr.Button("📄 Gerar Documento", variant="primary", size="lg")
res_md = gr.Markdown()
res_file = gr.File(label="Download")
# Coleta de Inputs
all_inputs = [numero_laudo, numero_processo, data_laudo, u_demand, u_demand_custom, req_tipo, req_custom, rep_legal, v_proposto, a_proposto]
all_inputs.extend(docs_padrao_inputs)
all_inputs.extend(docs_esp_inputs)
for d, a, c in zip(motivos_desc, motivos_aleg, motivos_conf):
all_inputs.extend([d, a, c])
all_inputs.extend([inscricao, endereco, bairro, setor_q, lote, finalidade, area_ter, construcoes, val_venais])
all_inputs.extend([u_resp, u_resp_custom, tec_resp, tec_resp_custom, metodo, metodo_custom, modelo, modelo_custom])
for a, v in zip(vm_anos, vm_vals):
all_inputs.extend([a, v])
all_inputs.append(ref_datas)
btn_gerar.click(gerar_documento_wrapper, all_inputs, [res_md, res_file])
return app
if __name__ == "__main__":
app = criar_interface()
app.launch(server_name="0.0.0.0", server_port=7860, share=False)