Spaces:
Sleeping
Sleeping
| """ | |
| utils.py | |
| Módulo de utilitários e funções acessórias. | |
| Contém: | |
| - Formatadores de texto e moeda | |
| - Extrator de dados do PDF do SIAT | |
| """ | |
| import re | |
| import json | |
| from typing import Tuple, List, Dict | |
| from pypdf import PdfReader | |
| # ============================================================================ | |
| # FORMATADORES (Antigo formatters.py) | |
| # ============================================================================ | |
| def numero_para_extenso(valor: float) -> str: | |
| """Converte um valor numérico para sua representação por extenso em português.""" | |
| if valor == 0: | |
| return "zero reais" | |
| unidades = ['', 'um', 'dois', 'três', 'quatro', 'cinco', 'seis', 'sete', 'oito', 'nove'] | |
| especiais = ['dez', 'onze', 'doze', 'treze', 'quatorze', 'quinze', 'dezesseis', 'dezessete', 'dezoito', 'dezenove'] | |
| dezenas = ['', '', 'vinte', 'trinta', 'quarenta', 'cinquenta', 'sessenta', 'setenta', 'oitenta', 'noventa'] | |
| centenas = ['', 'cento', 'duzentos', 'trezentos', 'quatrocentos', 'quinhentos', 'seiscentos', 'setecentos', 'oitocentos', 'novecentos'] | |
| def converter_grupo(n): | |
| if n == 0: | |
| return '' | |
| if n == 100: | |
| return 'cem' | |
| resultado = '' | |
| if n >= 100: | |
| resultado += centenas[n // 100] | |
| n %= 100 | |
| if n > 0: | |
| resultado += ' e ' | |
| if n >= 20: | |
| resultado += dezenas[n // 10] | |
| n %= 10 | |
| if n > 0: | |
| resultado += ' e ' + unidades[n] | |
| elif n >= 10: | |
| resultado += especiais[n - 10] | |
| elif n > 0: | |
| resultado += unidades[n] | |
| return resultado | |
| parte_inteira = int(valor) | |
| centavos = round((valor - parte_inteira) * 100) | |
| resultado = '' | |
| if parte_inteira >= 1000000: | |
| milhoes = parte_inteira // 1000000 | |
| if milhoes == 1: | |
| resultado += 'um milhão' | |
| else: | |
| resultado += converter_grupo(milhoes) + ' milhões' | |
| parte_inteira %= 1000000 | |
| if parte_inteira > 0: | |
| resultado += ' e ' | |
| if parte_inteira >= 1000: | |
| milhares = parte_inteira // 1000 | |
| if milhares == 1: | |
| resultado += 'mil' | |
| else: | |
| resultado += converter_grupo(milhares) + ' mil' | |
| parte_inteira %= 1000 | |
| if parte_inteira > 0: | |
| resultado += ' e ' | |
| if parte_inteira > 0: | |
| resultado += converter_grupo(parte_inteira) | |
| if resultado: | |
| if int(valor) == 1: | |
| resultado += ' real' | |
| else: | |
| resultado += ' reais' | |
| if centavos > 0: | |
| if resultado: | |
| resultado += ' e ' | |
| resultado += converter_grupo(centavos) | |
| if centavos == 1: | |
| resultado += ' centavo' | |
| else: | |
| resultado += ' centavos' | |
| return resultado | |
| def parse_valor_monetario(valor_str: str) -> float: | |
| """Converte string de valor monetário para float.""" | |
| if not valor_str: | |
| return 0.0 | |
| valor_limpo = re.sub(r'[^\d,.]', '', valor_str) | |
| valor_limpo = valor_limpo.replace('.', '').replace(',', '.') | |
| try: | |
| return float(valor_limpo) | |
| except ValueError: | |
| return 0.0 | |
| def formatar_valor_monetario(valor_str: str) -> Tuple[str, str]: | |
| """Formata valor monetário e retorna tupla (valor_formatado, extenso).""" | |
| valor_float = parse_valor_monetario(valor_str) | |
| if valor_float == 0: | |
| return valor_str, "" | |
| valor_formatado = f"R$ {valor_float:,.2f}".replace(',', 'X').replace('.', ',').replace('X', '.') | |
| extenso = numero_para_extenso(valor_float) | |
| return valor_formatado, extenso | |
| def aplicar_mascara_monetaria(valor: str) -> str: | |
| """Aplica máscara monetária brasileira a um valor.""" | |
| if not valor: | |
| return "" | |
| valor = valor.strip() | |
| if re.match(r'^R\$\s*[\d\.]+,\d{2}$', valor): | |
| return valor | |
| if re.search(r'[a-zA-ZáéíóúãõâêîôûàèìòùçÁÉÍÓÚÃÕÂÊÎÔÛÀÈÌÒÙÇ]', valor): | |
| return valor | |
| valor_limpo = re.sub(r'R\$\s*', '', valor) | |
| if ',' in valor_limpo and '.' in valor_limpo: | |
| valor_limpo = valor_limpo.replace('.', '').replace(',', '.') | |
| elif ',' in valor_limpo: | |
| valor_limpo = valor_limpo.replace(',', '.') | |
| try: | |
| valor_float = float(valor_limpo) | |
| except ValueError: | |
| return valor | |
| return f"R$ {valor_float:,.2f}".replace(',', 'X').replace('.', ',').replace('X', '.') | |
| def formatar_motivos_para_documento(motivos: List[str]) -> str: | |
| """Formata lista de motivos para inclusão no documento.""" | |
| motivos_limpos = [m.strip() for m in motivos if m and m.strip()] | |
| if not motivos_limpos: | |
| return "" | |
| return "\n".join([f"- {m};" for m in motivos_limpos]) | |
| def formatar_documentacao_para_documento(docs: List[str]) -> str: | |
| """Formata lista de documentos para inclusão no documento final.""" | |
| docs_limpos = [d.strip() for d in docs if d and d.strip()] | |
| if not docs_limpos: | |
| return "" | |
| return "\n".join([f"- {d};" for d in docs_limpos]) | |
| def formatar_valores_mercado_para_documento(anos: List[str], valores: List[str], com_extenso: bool = True) -> str: | |
| """Formata lista de valores de mercado por ano para inclusão no documento.""" | |
| linhas = [] | |
| for ano, valor in zip(anos, valores): | |
| if ano and valor: | |
| try: | |
| ano_str = str(int(float(ano))) if ano else "" | |
| except (ValueError, TypeError): | |
| ano_str = str(ano) if ano else "" | |
| if com_extenso: | |
| valor_float = parse_valor_monetario(valor) | |
| if valor_float > 0: | |
| extenso = numero_para_extenso(valor_float) | |
| linhas.append(f"{ano_str} - {valor} ({extenso})") | |
| else: | |
| linhas.append(f"{ano_str} - {valor}") | |
| else: | |
| linhas.append(f"{ano_str}: {valor}") | |
| if not linhas: | |
| return "" | |
| return "\n".join(linhas) | |
| def formatar_motivos_desvalorizantes(motivos: List[Dict]) -> Dict: | |
| """Formata a lista unificada de motivos desvalorizantes.""" | |
| if not motivos: | |
| return { | |
| 'alegados': [], | |
| 'confirmados': [], | |
| 'todos': [], | |
| 'alegados_texto': '', | |
| 'confirmados_texto': '', | |
| 'secoes': [] | |
| } | |
| alegados = [] | |
| confirmados = [] | |
| todos = [] | |
| secoes = [] | |
| for i, motivo in enumerate(motivos, 1): | |
| descricao = motivo.get('descricao', '').strip() | |
| foi_alegado = motivo.get('alegado', False) | |
| foi_confirmado = motivo.get('confirmado', False) | |
| if not descricao: | |
| continue | |
| item = { | |
| 'numero': i, | |
| 'descricao': descricao, | |
| 'alegado': foi_alegado, | |
| 'confirmado': foi_confirmado | |
| } | |
| todos.append(item) | |
| if foi_alegado: | |
| alegados.append(descricao) | |
| if foi_confirmado: | |
| confirmados.append(descricao) | |
| status = [] | |
| if foi_alegado: | |
| status.append("alegado pelo contribuinte") | |
| else: | |
| status.append("não alegado pelo contribuinte") | |
| if foi_confirmado: | |
| status.append("e confirmado na análise") | |
| else: | |
| status.append("e não confirmado na análise") | |
| secoes.append({ | |
| 'numero': i, | |
| 'titulo': descricao, | |
| 'status': ", ".join(status) if status else "identificado na vistoria", | |
| 'confirmado': foi_confirmado | |
| }) | |
| alegados_texto = formatar_motivos_para_documento(alegados) | |
| confirmados_texto = formatar_motivos_para_documento(confirmados) | |
| return { | |
| 'alegados': alegados, | |
| 'confirmados': confirmados, | |
| 'todos': todos, | |
| 'alegados_texto': alegados_texto, | |
| 'confirmados_texto': confirmados_texto, | |
| 'secoes': secoes | |
| } | |
| # ============================================================================ | |
| # EXTRATOR DE PDF (Antigo pdf_extractor.py) | |
| # ============================================================================ | |
| def extrair_dados_pdf(pdf_file) -> dict: | |
| """Extrai dados do PDF do SIAT.""" | |
| if pdf_file is None: | |
| return None | |
| try: | |
| reader = PdfReader(pdf_file) | |
| text = "" | |
| for page in reader.pages: | |
| page_text = page.extract_text() | |
| if page_text: | |
| text += page_text | |
| dados = { | |
| "inscricao_imovel": None, | |
| "endereco_imovel": None, | |
| "bairro_imovel": None, | |
| "setor_imovel": None, | |
| "quarteirao_imovel": None, | |
| "lote_imovel": None, | |
| "finalidade_imovel": None, | |
| "area_territorial": None, | |
| "construcoes_imovel": "Sem construções cadastradas", | |
| "valores_venais": None | |
| } | |
| # INSCRIÇÃO | |
| m = re.search(r'(\d{0,10})Inscriç', text) | |
| if m: | |
| dados["inscricao_imovel"] = m.group(1) | |
| # BAIRRO | |
| pattern_bairro = re.compile(r"Quadra \/ Lote(.*?)Bairro", re.DOTALL) | |
| bairros = [b.strip() for b in pattern_bairro.findall(text)] | |
| if bairros: | |
| dados["bairro_imovel"] = list(set(bairros))[-1] | |
| # ENDEREÇO | |
| pattern_endereco = re.compile( | |
| r"([^\n]*? - Porto Alegre/RS - \d{5}-\d{3})", | |
| re.IGNORECASE | |
| ) | |
| matches_end = pattern_endereco.findall(text) | |
| if matches_end: | |
| endereco_completo = matches_end[-1] | |
| if dados["bairro_imovel"]: | |
| dados["endereco_imovel"] = endereco_completo.split(f" - {dados['bairro_imovel']}")[0].strip() | |
| else: | |
| dados["endereco_imovel"] = re.split(r"\s-\s(?:PORTO ALEGRE|Porto Alegre)/RS", endereco_completo)[0].strip() | |
| # SETOR | |
| pattern_setor = re.compile(r"Ratei(.*?)Setor", re.DOTALL) | |
| setores = [re.findall(r"(?<!\d)\d{5}(?![,\d])", m)[-1] for m in pattern_setor.findall(text)] | |
| if setores: | |
| dados["setor_imovel"] = list(set(setores))[-1] | |
| # QUARTEIRÃO | |
| pattern_quadra = re.compile(r"Setor(.*?)Quadra", re.DOTALL) | |
| quadras = [re.findall(r"(?<!\d)\d{4}(?![,\d])", m)[-1] for m in pattern_quadra.findall(text)] | |
| if quadras: | |
| dados["quarteirao_imovel"] = list(set(quadras))[-1] | |
| # LOTE | |
| pattern_lote = re.compile(r"Lote Fiscal(.*?)Quarteir", re.DOTALL) | |
| lotes = [re.findall(r"(\d{1}\.\d{8}\.\d{4})", m)[-1] for m in pattern_lote.findall(text)] | |
| if lotes: | |
| dados["lote_imovel"] = list(set(lotes))[-1] | |
| # FINALIDADE | |
| pattern_finalidade = re.compile(r"Uso(.*?)Finalidad", re.DOTALL) | |
| fins = [f.strip() for f in pattern_finalidade.findall(text)] | |
| if fins: | |
| dados["finalidade_imovel"] = list(set(fins))[-1] | |
| # VALOR VENAL | |
| pattern_valor = re.compile(r'Tributação(.*?)Planta', re.DOTALL) | |
| matches_valor = pattern_valor.findall(text) | |
| valores_ano = [] | |
| for m in matches_valor: | |
| try: | |
| ano = re.search(r'(\d{4})', m).group(1) | |
| valores = re.findall(r'(\d{1,3}(?:\.\d{3})*,\d{2})', m) | |
| valor_venal = valores[2] | |
| valores_ano.append(f"{ano}: R$ {valor_venal}") | |
| except: | |
| continue | |
| if valores_ano: | |
| dados["valores_venais"] = " | ".join(valores_ano) | |
| # ÁREA TERRITORIAL | |
| match_area_real = re.search(r"Área real total\s+(\d{1,5},\d{2})\s*m²", text, re.IGNORECASE) | |
| area_real = match_area_real.group(1) if match_area_real else "0,00" | |
| matches_corr = re.findall(r'territorial corrigida(.*?)Última', text, re.DOTALL | re.IGNORECASE) | |
| corrigidas = [] | |
| for m in matches_corr: | |
| valores = re.findall(r"(\d{1,5},\d{2})", m) | |
| if valores: | |
| corrigidas.extend(valores) | |
| corrigidas_unicas = list(set(corrigidas)) | |
| area_corrigida = corrigidas_unicas[-1] if corrigidas_unicas else "0,00" | |
| dados["area_territorial"] = f"{area_real} m² / {area_corrigida} m²" | |
| # ÁREA CONSTRUÍDA | |
| pattern_area_construida = re.compile( | |
| r"Área Construída(.*?)Documento de Origem", | |
| re.DOTALL | re.IGNORECASE | |
| ) | |
| match_area = pattern_area_construida.search(text) | |
| construcoes = [] | |
| if match_area: | |
| bloco = match_area.group(1) | |
| pattern_const = re.compile( | |
| r"^\s*\d+\s+(?P<area>\d{1,4},\d{2})\s+(?P<ano>\d{4})\s+\d{4}\s+(?P<tipo>.*?)\s+\d{1,2}-\d{1,2}%", | |
| re.MULTILINE | |
| ) | |
| for m in pattern_const.finditer(bloco): | |
| area = m.group("area") | |
| ano = m.group("ano") | |
| tipo = m.group("tipo").strip() | |
| construcoes.append(f"{area} m² / {ano} / {tipo}") | |
| if construcoes: | |
| dados["construcoes_imovel"] = "\n".join(construcoes) | |
| return dados | |
| except Exception as e: | |
| print(f"Erro ao extrair dados do PDF: {e}") | |
| return None | |
| def processar_upload_pdf(pdf_file): | |
| """Processa upload de PDF e retorna valores para preencher formulário.""" | |
| if pdf_file is None: | |
| return [""] * 10 | |
| dados = extrair_dados_pdf(pdf_file) | |
| if dados is None: | |
| return [""] * 10 | |
| setor_quarteirao = f"{dados['setor_imovel']} / {dados['quarteirao_imovel']}" | |
| return [ | |
| dados["inscricao_imovel"], | |
| dados["endereco_imovel"], | |
| dados["bairro_imovel"], | |
| setor_quarteirao, | |
| dados["lote_imovel"], | |
| dados["finalidade_imovel"], | |
| dados["area_territorial"], | |
| dados["construcoes_imovel"], | |
| dados["valores_venais"], | |
| json.dumps(dados.get("valores_mercado_extraidos", [])) | |
| ] |