Spaces:
Running
Running
| import sys | |
| import os | |
| import subprocess | |
| import pandas as pd | |
| import datetime | |
| import re | |
| from fpdf import FPDF | |
| import gradio as gr | |
| from playwright.sync_api import sync_playwright | |
| import time | |
| import random | |
| import requests | |
| from PIL import Image | |
| import io | |
| import traceback | |
| from geopy.geocoders import Nominatim | |
| import folium | |
| # Silenciador de consola | |
| def silenciador_errores_basura(unraisable): | |
| if unraisable.exc_type == ValueError and "Invalid file descriptor: -1" in str(unraisable.exc_value): pass | |
| else: sys.__unraisablehook__(unraisable) | |
| sys.unraisablehook = silenciador_errores_basura | |
| try: subprocess.run(["playwright", "install", "chromium"], check=True) | |
| except: pass | |
| try: from fake_useragent import UserAgent | |
| except ImportError: | |
| subprocess.run(["pip", "install", "fake-useragent"], check=True) | |
| from fake_useragent import UserAgent | |
| # --- URL DEL GOBIERNO --- | |
| URL_LOGO_GOBIERNO = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Escudo_de_Colombia.svg/250px-Escudo_de_Colombia.svg.png" | |
| def descargar_recurso(url, nombre_archivo): | |
| try: | |
| headers = {"User-Agent": "Mozilla/5.0"} | |
| r = requests.get(url, timeout=10, headers=headers) | |
| if r.status_code == 200: | |
| with open(nombre_archivo, 'wb') as f: f.write(r.content) | |
| except: pass | |
| def preparar_entorno_pdf(): | |
| if not os.path.exists("logo_gob.png"): descargar_recurso(URL_LOGO_GOBIERNO, "logo_gob.png") | |
| def sanear_texto(texto): | |
| if not isinstance(texto, str): return "" | |
| return texto.encode('latin-1', 'ignore').decode('latin-1').strip() | |
| def descargar_imagen(url, idx): | |
| if not url or len(url) < 5 or url.startswith("data:"): return None | |
| try: | |
| headers = {"User-Agent": "Mozilla/5.0", "Accept": "image/*", "Referer": "https://www.fincaraiz.com.co/"} | |
| r = requests.get(url, timeout=8, headers=headers) | |
| if r.status_code == 200: | |
| img = Image.open(io.BytesIO(r.content)) | |
| if img.mode != 'RGB': img = img.convert('RGB') | |
| path = f"temp_img_{idx}.jpg" | |
| img.save(path, format="JPEG") | |
| return path | |
| except: return None | |
| return None | |
| # --- CONSTRUCTOR DE URLS INTELIGENTE --- | |
| def construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina): | |
| mapa_ant = {"Menos de 1 año": "de-0-a-1-anos", "1 a 8 años": "de-1-a-8-anos", "9 a 15 años": "de-9-a-15-anos", "16 a 30 años": "de-16-a-30-anos", "Más de 30 años": "mas-de-30-anos"} | |
| slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anos") | |
| slug_park = f"{int(park)}-parqueadero" if int(park) == 1 else f"{int(park)}-parqueaderos" | |
| b_slug = barrio.lower().strip().replace(" ", "-") | |
| c_slug = ciudad.lower().strip().replace(" ", "-") | |
| op_slug = operacion.lower().strip() | |
| tipo_slug = tipo.lower().strip() | |
| residenciales = ["apartamento", "casa", "apartaestudio"] | |
| if tipo_slug in residenciales: | |
| tipo_fr = "casas-y-apartamentos-y-apartaestudios" | |
| filtros_fr = f"/{int(hab)}-o-mas-habitaciones/{int(ban)}-o-mas-banos/{slug_park}/{slug_ant}" | |
| filtros_mc = f"/{int(ban)}-banos-{int(hab)}-habitaciones" | |
| else: | |
| if tipo_slug == "local": tipo_fr = "locales" | |
| elif tipo_slug == "edificio": tipo_fr = "edificios" | |
| else: tipo_fr = tipo_slug + "s" | |
| if tipo_slug in ["lote", "finca"]: | |
| filtros_fr = "" | |
| filtros_mc = "" | |
| else: | |
| filtros_fr = f"/{slug_park}/{slug_ant}" | |
| filtros_mc = "" | |
| url_fr_base = f"https://www.fincaraiz.com.co/{op_slug}/{tipo_fr}/{b_slug}/{c_slug}{filtros_fr}/m2-desde-{int(m2_min)}/m2-hasta-{int(m2_max)}" | |
| if ascensor and tipo_slug not in ["lote", "finca"]: url_fr_base += "/con-ascensor" | |
| if piscina and tipo_slug in residenciales + ["finca"]: url_fr_base += "/con-piscina" | |
| url_mc = f"https://www.metrocuadrado.com/{tipo_slug}/{op_slug}/{c_slug}/{b_slug}{filtros_mc}/?search=form" | |
| return url_fr_base, url_mc | |
| def extraer_precio(texto, operacion): | |
| patron = r'\$\s?(\d{1,3}(?:[.,]\d{3})*)' | |
| coincidencias = re.findall(patron, texto) | |
| if coincidencias: | |
| precios = [int(p.replace('.', '').replace(',', '')) for p in coincidencias] | |
| precios_validos = [p for p in precios if (600000 <= p <= 40000000 if operacion == "Arriendo" else p >= 40000000)] | |
| if precios_validos: return precios_validos[0] | |
| return 0 | |
| def extraer_ubicacion(texto): | |
| lineas = [l.strip() for l in texto.split('\n') if len(l.strip()) > 4] | |
| for linea in lineas: | |
| if "$" in linea or re.search(r'(?i)(hab|baño|m2|m²)', linea): continue | |
| if "," in linea or " en " in linea.lower(): | |
| limpio = re.sub(r'(?i)(apartamento|casa|bodega|lote|oficina|local|consultorio|finca|edificio)\s+en\s+(arriendo|venta)\s+(en\s+)?', '', linea) | |
| return limpio[:60].strip() | |
| for linea in lineas[1:4]: | |
| if "$" not in linea and not re.search(r'\d', linea): return linea[:60] | |
| return "Ubicacion en la zona" | |
| def es_inmueble_valido(href, portal): | |
| if not href or "javascript" in href or "blog" in href or "proyectos" in href: return False | |
| if portal == "FR": | |
| if re.search(r'/\d{7,10}$', href) or "arriendo-en" in href or "venta-en" in href: return True | |
| elif portal == "MC": | |
| if "/inmueble/" in href or "-id-" in href: return True | |
| return False | |
| # --- MÓDULO GEOESPACIAL --- | |
| def generar_mapa(barrio, ciudad): | |
| try: | |
| geolocator = Nominatim(user_agent="tramitia_geo") | |
| location = geolocator.geocode(f"{barrio}, {ciudad}, Colombia") | |
| if location: | |
| lat, lon = location.latitude, location.longitude | |
| m = folium.Map(location=[lat, lon], zoom_start=15) | |
| folium.Marker([lat, lon], popup=f"Zona de Estudio: {barrio.title()}", icon=folium.Icon(color="red", icon="info-sign")).add_to(m) | |
| return m._repr_html_(), lat, lon | |
| except: pass | |
| return "<p>Mapa no disponible</p>", 10.9639, -74.7964 | |
| # --- CLASE PDF --- | |
| class PDF_SAE(FPDF): | |
| def header(self): | |
| if os.path.exists("logo_gob.png"): | |
| try: self.image("logo_gob.png", x=15, y=8, h=16) | |
| except: pass | |
| if os.path.exists("logo_sae.png"): | |
| try: self.image("logo_sae.png", x=155, y=8, w=40) | |
| except: pass | |
| self.ln(25) | |
| def footer(self): | |
| self.set_y(-25) | |
| try: | |
| self.set_font('Arial', '', 7) | |
| self.set_text_color(137, 137, 137) | |
| texto_pie = "Direccion General: Carrera 7 # 32-42 Centro Comercial San Martin Local 107 / PBX: 7431444\nLinea Gratuita Nacional: 01 8000 111612 - atencionalciudadano@saesas.gov.co - www.saesas.gov.co" | |
| self.multi_cell(0, 3, sanear_texto(texto_pie), align='C') | |
| except: pass | |
| self.set_y(-15); self.set_font('Arial', 'I', 8) | |
| self.cell(0, 10, f'Pagina {self.page_no()}', 0, 0, 'C') | |
| # --- MOTOR PRINCIPAL --- | |
| def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina): | |
| resultados = []; log_visible = ""; urls_vistas = set() | |
| precios_inversos = [] | |
| try: | |
| url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina) | |
| op_inversa = "Venta" if operacion == "Arriendo" else "Arriendo" | |
| url_inversa, _ = construir_urls_final(op_inversa, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina) | |
| log_visible = f"✅ INICIANDO EXTRACCIÓN PRINCIPAL:\nFR: {url_fr}\n\n" | |
| ua = UserAgent() | |
| with sync_playwright() as p: | |
| browser = p.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled', '--no-sandbox']) | |
| context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random) | |
| # 1. EXTRACCIÓN (FR) | |
| try: | |
| page = context.new_page(); page.goto(url_fr, wait_until="domcontentloaded", timeout=60000) | |
| for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000) | |
| elementos = page.query_selector_all("a"); cont_fr = 0 | |
| for el in elementos: | |
| if cont_fr >= 12: break | |
| try: | |
| href = el.get_attribute("href") | |
| if not es_inmueble_valido(href, "FR"): continue | |
| full_url = f"https://www.fincaraiz.com.co{href}" if href.startswith("/") else href | |
| if full_url in urls_vistas: continue | |
| card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement") | |
| if not card: continue | |
| txt = card.inner_text(); precio = extraer_precio(txt, operacion) | |
| if precio > 0: | |
| img_url = "" | |
| img_el = card.query_selector("img") | |
| if img_el: img_url = img_el.get_attribute("src") or img_el.get_attribute("data-src") or "" | |
| if img_url.startswith("/"): img_url = "https://www.fincaraiz.com.co" + img_url | |
| resultados.append({"Portal": "Finca Raiz", "Precio": precio, "Precio_M2": precio / area, "Ubicacion": extraer_ubicacion(txt), "Descripcion": txt.replace('\n', ' | ')[:120] + "...", "URL": full_url, "Imagen": img_url}) | |
| urls_vistas.add(full_url); cont_fr += 1 | |
| except: continue | |
| page.close(); log_visible += f"✅ FR: {cont_fr} inmuebles.\n" | |
| except Exception as e: log_visible += f"⚠️ Error FR: {e}\n" | |
| # 2. EXTRACCIÓN (MC) - ¡CORREGIDO! | |
| try: | |
| page = context.new_page(); page.goto(url_mc, wait_until="domcontentloaded", timeout=60000) | |
| for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000) | |
| elementos = page.query_selector_all("a"); cont_mc = 0 | |
| # AQUÍ ESTABA EL ERROR (elements por elementos) | |
| for el in elementos: | |
| if cont_mc >= 12: break | |
| try: | |
| href = el.get_attribute("href") | |
| if not es_inmueble_valido(href, "MC"): continue | |
| full_url = f"https://www.metrocuadrado.com{href}" if href.startswith("/") else href | |
| if full_url in urls_vistas: continue | |
| card = el.evaluate_handle("el => el.closest('li') || el.closest('[class*=\"card\"]') || el.closest('[class*=\"property\"]') || el.parentElement.parentElement.parentElement") | |
| if not card: continue | |
| txt = card.inner_text(); precio = extraer_precio(txt, operacion) | |
| if precio > 0: | |
| img_url = "" | |
| img_el = card.query_selector("img") | |
| if img_el: img_url = img_el.get_attribute("src") or img_el.get_attribute("data-src") or "" | |
| if img_url.startswith("/"): img_url = "https://www.metrocuadrado.com" + img_url | |
| resultados.append({"Portal": "Metrocuadrado", "Precio": precio, "Precio_M2": precio / area, "Ubicacion": extraer_ubicacion(txt), "Descripcion": txt.replace('\n', ' | ')[:120] + "...", "URL": full_url, "Imagen": img_url}) | |
| urls_vistas.add(full_url); cont_mc += 1 | |
| except: continue | |
| page.close(); log_visible += f"✅ MC: {cont_mc} inmuebles.\n" | |
| except Exception as e: log_visible += f"⚠️ Error MC: {e}\n" | |
| # 3. MÓDULO FINANCIERO | |
| log_visible += f"\n🔄 Ejecutando Módulo Financiero (Buscando {op_inversa} para Cap Rate)...\n" | |
| try: | |
| page = context.new_page(); page.goto(url_inversa, wait_until="domcontentloaded", timeout=45000) | |
| for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(1500) | |
| elementos = page.query_selector_all("a") | |
| for el in elementos: | |
| if len(precios_inversos) >= 8: break | |
| try: | |
| href = el.get_attribute("href") | |
| if not es_inmueble_valido(href, "FR"): continue | |
| card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement") | |
| if not card: continue | |
| txt = card.inner_text(); precio = extraer_precio(txt, op_inversa) | |
| if precio > 0: precios_inversos.append(precio) | |
| except: continue | |
| page.close() | |
| log_visible += f"✅ Finanzas: Capturados {len(precios_inversos)} valores de {op_inversa} en la zona.\n" | |
| except Exception as ex: pass | |
| browser.close() | |
| if not resultados: return f"{log_visible}\n❌ NO HAY DATOS. Intenta ampliar los M2 o quitar filtros.", pd.DataFrame(), None, "---", "Sin Mapa" | |
| # --- LIMPIEZA DE DATOS --- | |
| df_final_completo = pd.DataFrame(resultados) | |
| df_fr = df_final_completo[df_final_completo['Portal'] == 'Finca Raiz'].head(6) | |
| df_mc = df_final_completo[df_final_completo['Portal'] == 'Metrocuadrado'].head(6) | |
| df_final = pd.concat([df_fr, df_mc]).reset_index(drop=True) | |
| # --- CÁLCULOS TÉCNICOS --- | |
| df_final['V_Homogeneizado'] = df_final['Precio'] * 0.92 | |
| mediana_m2 = df_final['Precio_M2'].median() | |
| minimo_zona = df_final['Precio'].min() | |
| maximo_zona = df_final['Precio'].max() | |
| valor_tecnico_m2 = mediana_m2 * 0.92 | |
| valor_total_sugerido = valor_tecnico_m2 * area | |
| valor_maximo_neg = (mediana_m2 * area) * 0.95 | |
| valor_minimo_neg = (mediana_m2 * area) * 0.90 | |
| # --- CAP RATE --- | |
| cap_rate_txt = "Datos insuficientes en la zona para cruzar la rentabilidad." | |
| if len(precios_inversos) > 0: | |
| mediana_inversa = pd.Series(precios_inversos).median() | |
| try: | |
| if operacion == "Arriendo": | |
| rentabilidad = ((valor_total_sugerido * 12) / mediana_inversa) * 100 | |
| cap_rate_txt = f"Valor Comercial de Venta Promedio en la zona: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual Estimada (Cap Rate): {rentabilidad:.2f}%" | |
| else: | |
| rentabilidad = ((mediana_inversa * 12) / valor_total_sugerido) * 100 | |
| cap_rate_txt = f"Canon de Arriendo Mensual Promedio en la zona: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual Estimada (Cap Rate): {rentabilidad:.2f}%" | |
| except: pass | |
| # --- MAPA Y PDF --- | |
| mapa_html, lat_mapa, lon_mapa = generar_mapa(barrio, ciudad) | |
| preparar_entorno_pdf() | |
| pdf_path = f"Dictamen_Pericial_SAE_{int(time.time())}.pdf" | |
| pdf = PDF_SAE() | |
| COLOR_ROSADO = (254, 25, 120); COLOR_GRIS = (137, 137, 137); COLOR_NEGRO = (0, 0, 0) | |
| # PÁGINA 1 | |
| pdf.add_page() | |
| pdf.set_font("Arial", 'B', 18); pdf.set_text_color(*COLOR_ROSADO) | |
| pdf.cell(0, 15, sanear_texto("DICTAMEN COMERCIAL Y FINANCIERO"), ln=True, align='C') | |
| pdf.set_font("Arial", 'B', 14); pdf.set_text_color(*COLOR_GRIS) | |
| pdf.cell(0, 8, sanear_texto(f"METODOLOGIA COMPARATIVA DE MERCADO"), ln=True, align='C') | |
| pdf.line(20, 65, 190, 65); pdf.ln(15) | |
| pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_ROSADO) | |
| pdf.cell(0, 10, sanear_texto("1. DATOS GEOESPACIALES Y DEL ACTIVO"), ln=True) | |
| pdf.set_font("Arial", '', 11); pdf.set_text_color(*COLOR_NEGRO) | |
| pdf.cell(0, 6, sanear_texto(f"- Operacion: {operacion.capitalize()}"), ln=True) | |
| pdf.cell(0, 6, sanear_texto(f"- Tipo de Inmueble: {tipo.capitalize()}"), ln=True) | |
| pdf.cell(0, 6, sanear_texto(f"- Ubicacion: Barrio {barrio.title()}, {ciudad.title()}"), ln=True) | |
| pdf.cell(0, 6, sanear_texto(f"- Coordenadas de Influencia: Lat {lat_mapa:.4f}, Lon {lon_mapa:.4f}"), ln=True) | |
| pdf.cell(0, 6, sanear_texto(f"- Area Construida: {area} m2"), ln=True) | |
| pdf.ln(5) | |
| pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_ROSADO) | |
| pdf.cell(0, 10, sanear_texto("2. RENTABILIDAD Y DESEMPEÑO FINANCIERO (CAP RATE)"), ln=True) | |
| pdf.set_font("Arial", '', 11); pdf.set_text_color(*COLOR_NEGRO) | |
| pdf.multi_cell(0, 6, sanear_texto(f"Basado en el cruce de datos de la zona, el activo presenta el siguiente comportamiento:\n{cap_rate_txt}")) | |
| pdf.ln(5) | |
| # PÁGINA 2 | |
| pdf.add_page() | |
| pdf.set_font("Arial", 'B', 12); pdf.set_fill_color(*COLOR_ROSADO); pdf.set_text_color(255, 255, 255) | |
| pdf.cell(0, 10, sanear_texto(" 3. MATRIZ DE HOMOGENEIZACION Y RANGOS"), ln=True, fill=True) | |
| pdf.set_text_color(*COLOR_NEGRO); pdf.ln(5); pdf.set_font("Arial", '', 11) | |
| pdf.cell(80, 8, sanear_texto("Valor Mínimo (Oferta Zona):"), border=1) | |
| pdf.cell(50, 8, sanear_texto(f"${minimo_zona:,.0f}"), border=1, ln=True, align='C') | |
| pdf.cell(80, 8, sanear_texto("Valor Máximo (Oferta Zona):"), border=1) | |
| pdf.cell(50, 8, sanear_texto(f"${maximo_zona:,.0f}"), border=1, ln=True, align='C') | |
| pdf.cell(80, 8, sanear_texto("Factor Comercialización (Castigo):"), border=1) | |
| pdf.cell(50, 8, sanear_texto("-8.00%"), border=1, ln=True, align='C') | |
| pdf.ln(10) | |
| pdf.set_font("Arial", 'B', 14); pdf.set_text_color(*COLOR_ROSADO) | |
| pdf.cell(0, 10, sanear_texto("RANGOS DE NEGOCIACION AUTORIZADOS (CIERRE):"), ln=True) | |
| pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_GRIS) | |
| pdf.cell(0, 8, sanear_texto(f"TECHO MAXIMO (-5%): ${valor_maximo_neg:,.0f} COP"), ln=True) | |
| pdf.set_font("Arial", 'B', 16); pdf.set_text_color(0, 128, 0) | |
| pdf.cell(0, 12, sanear_texto(f"VALOR OPTIMO SUGERIDO (-8%): ${valor_total_sugerido:,.0f} COP"), ln=True) | |
| pdf.set_font("Arial", 'B', 12); pdf.set_text_color(200, 0, 0) | |
| pdf.cell(0, 8, sanear_texto(f"PISO MINIMO ACEPTABLE (-10%): ${valor_minimo_neg:,.0f} COP"), ln=True) | |
| # PÁGINA 3+ | |
| pdf.add_page() | |
| pdf.set_font("Arial", 'B', 12); pdf.set_fill_color(*COLOR_GRIS); pdf.set_text_color(255, 255, 255) | |
| pdf.cell(0, 10, sanear_texto(" 4. ANEXO TECNICO: TESTIGOS COMPARABLES"), ln=True, fill=True) | |
| pdf.set_text_color(*COLOR_NEGRO); pdf.ln(5) | |
| for idx, r in df_final.iterrows(): | |
| if pdf.get_y() > 240: pdf.add_page() | |
| y_start = pdf.get_y(); img_path = descargar_imagen(r['Imagen'], idx); text_x = 10 | |
| if img_path and os.path.exists(img_path): | |
| try: pdf.image(img_path, x=10, y=y_start, w=45, h=30); text_x = 60 | |
| except: pass | |
| pdf.set_xy(text_x, y_start); pdf.set_font("Arial", 'B', 11) | |
| pdf.cell(0, 6, f"Oferta: ${r['Precio']:,.0f} | Homogeneizado: ${r['V_Homogeneizado']:,.0f}", ln=True) | |
| pdf.set_x(text_x); pdf.set_font("Arial", 'B', 8); pdf.set_text_color(*COLOR_ROSADO) | |
| pdf.cell(0, 4, f"Ubicacion: {sanear_texto(r['Ubicacion'])}", ln=True) | |
| pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(*COLOR_NEGRO) | |
| pdf.multi_cell(0, 4, sanear_texto(r['Descripcion'])) | |
| pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(0, 102, 204) | |
| pdf.cell(0, 4, f">> Ver testigo en {sanear_texto(r['Portal'])}", link=r['URL'], ln=True) | |
| pdf.set_text_color(*COLOR_NEGRO); y_end = pdf.get_y(); pdf.set_y(max(y_start + 35, y_end + 5)) | |
| pdf.set_draw_color(200, 200, 200); pdf.line(10, pdf.get_y(), 200, pdf.get_y()); pdf.ln(4) | |
| if img_path and os.path.exists(img_path): | |
| try: os.remove(img_path) | |
| except: pass | |
| pdf.output(pdf_path) | |
| resumen = ( | |
| f"🏢 **DICTAMEN PERICIAL Y FINANCIERO**\n" | |
| f"📈 **Techo Máximo:** ${valor_maximo_neg:,.0f}\n" | |
| f"✅ **VALOR ÓPTIMO:** ${valor_total_sugerido:,.0f}\n" | |
| f"🛑 **Piso Mínimo:** ${valor_minimo_neg:,.0f}\n\n" | |
| f"📊 **Análisis de Inversión:**\n{cap_rate_txt}" | |
| ) | |
| df_mostrar = df_final[['Portal', 'Precio', 'V_Homogeneizado', 'Precio_M2', 'URL']].copy() | |
| df_mostrar['Precio'] = df_mostrar['Precio'].apply(lambda x: f"${x:,.0f}") | |
| df_mostrar['V_Homogeneizado'] = df_mostrar['V_Homogeneizado'].apply(lambda x: f"${x:,.0f}") | |
| df_mostrar['Precio_M2'] = df_mostrar['Precio_M2'].apply(lambda x: f"${x:,.0f}") | |
| return f"{log_visible}\n✅ Dictamen Maestro Generado.", df_mostrar, pdf_path, resumen, mapa_html | |
| except Exception as error_fatal: | |
| return f"❌ ERROR GRAVE:\n{str(error_fatal)}", pd.DataFrame(), None, "⚠️ Falló la ejecución", "<p>Error en mapa</p>" | |
| # --- FUNCIÓN DE OCULTAMIENTO INTELIGENTE UI --- | |
| def adaptar_interfaz(tipo): | |
| if tipo in ["Lote", "Finca"]: | |
| return [gr.update(visible=False)] * 6 | |
| elif tipo in ["Bodega", "Local", "Oficina", "Consultorio", "Edificio"]: | |
| return [gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)] | |
| else: | |
| return [gr.update(visible=True)] * 6 | |
| # --- INTERFAZ GRÁFICA --- | |
| with gr.Blocks() as demo: | |
| gr.Markdown("## 🏢 TramitIA Pro: Dictamen Pericial Máster (IGAC/SAE)") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| op = gr.Radio(["Arriendo", "Venta"], label="Tipo de Operación", value="Arriendo") | |
| c = gr.Textbox(label="Ciudad", value="Barranquilla") | |
| b = gr.Textbox(label="Barrio (Ej: La Concepcion)", value="La Concepcion") | |
| with gr.Row(): | |
| t = gr.Dropdown(["Apartamento", "Casa", "Apartaestudio", "Bodega", "Local", "Oficina", "Consultorio", "Lote", "Finca", "Edificio"], label="Tipo de Inmueble", value="Apartamento") | |
| a = gr.Number(label="Área M2 a tasar", value=70) | |
| with gr.Row(): | |
| m2_min = gr.Number(label="M2 Mínimo", value=10) | |
| m2_max = gr.Number(label="M2 Máximo", value=200) | |
| with gr.Row(): | |
| ascensor = gr.Checkbox(label="Con Ascensor") | |
| piscina = gr.Checkbox(label="Con Piscina") | |
| with gr.Row(): | |
| h = gr.Number(label="Hab", value=3) | |
| ban = gr.Number(label="Baños", value=2) | |
| p = gr.Number(label="Park", value=1) | |
| e = gr.Dropdown(["Menos de 1 año", "1 a 8 años", "9 a 15 años", "16 a 30 años", "Más de 30 años"], label="Antigüedad", value="1 a 8 años") | |
| t.change(adaptar_interfaz, inputs=t, outputs=[h, ban, p, e, ascensor, piscina]) | |
| btn = gr.Button("GENERAR DICTAMEN INTEGRAL", variant="primary") | |
| with gr.Column(scale=2): | |
| res_fin = gr.Markdown("### 💰 Resultado y Rentabilidad (Cap Rate)...") | |
| with gr.Tabs(): | |
| with gr.TabItem("Mapa Geoespacial"): mapa_ui = gr.HTML("<p style='text-align:center; color:gray;'>El mapa aparecerá aquí...</p>") | |
| with gr.TabItem("Descargar Dictamen (PDF)"): out_pdf = gr.File() | |
| with gr.TabItem("Tabla de Homogeneización"): out_df = gr.Dataframe() | |
| with gr.TabItem("Log de Sistema"): msg = gr.Textbox(lines=10) | |
| btn.click(motor_tramitia_visual, [op, b, c, a, m2_min, m2_max, t, h, ban, p, e, ascensor, piscina], [msg, out_df, out_pdf, res_fin, mapa_ui]) | |
| demo.launch(theme=gr.themes.Soft()) |