Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -28,7 +28,6 @@ except ImportError:
|
|
| 28 |
subprocess.run(["pip", "install", "fake-useragent"], check=True)
|
| 29 |
from fake_useragent import UserAgent
|
| 30 |
|
| 31 |
-
# --- URL DEL GOBIERNO ---
|
| 32 |
URL_LOGO_GOBIERNO = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Escudo_de_Colombia.svg/250px-Escudo_de_Colombia.svg.png"
|
| 33 |
|
| 34 |
def descargar_recurso(url, nombre_archivo):
|
|
@@ -79,17 +78,13 @@ def construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antigu
|
|
| 79 |
if tipo_slug == "local": tipo_fr = "locales"
|
| 80 |
elif tipo_slug == "edificio": tipo_fr = "edificios"
|
| 81 |
else: tipo_fr = tipo_slug + "s"
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
filtros_fr = ""; filtros_mc = ""
|
| 85 |
-
else:
|
| 86 |
-
filtros_fr = f"/{slug_park}/{slug_ant}"; filtros_mc = ""
|
| 87 |
|
| 88 |
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)}"
|
| 89 |
if ascensor and tipo_slug not in ["lote", "finca"]: url_fr_base += "/con-ascensor"
|
| 90 |
if piscina and tipo_slug in residenciales + ["finca"]: url_fr_base += "/con-piscina"
|
| 91 |
url_mc = f"https://www.metrocuadrado.com/{tipo_slug}/{op_slug}/{c_slug}/{b_slug}{filtros_mc}/?search=form"
|
| 92 |
-
|
| 93 |
return url_fr_base, url_mc
|
| 94 |
|
| 95 |
def extraer_precio(texto, operacion):
|
|
@@ -158,11 +153,10 @@ class PDF_SAE(FPDF):
|
|
| 158 |
|
| 159 |
|
| 160 |
# ==========================================
|
| 161 |
-
# FASE 1: BÚSQUEDA
|
| 162 |
# ==========================================
|
| 163 |
-
def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina):
|
| 164 |
-
|
| 165 |
-
|
| 166 |
resultados = []; urls_vistas = set(); precios_inversos = []
|
| 167 |
|
| 168 |
url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
|
|
@@ -170,8 +164,6 @@ def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo
|
|
| 170 |
url_inversa, _ = construir_urls_final(op_inversa, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
|
| 171 |
|
| 172 |
log_visible = f"✅ RUTAS GENERADAS:\n- FR: {url_fr}\n- MC: {url_mc}\n\n"
|
| 173 |
-
yield f"{log_visible}🚀 Levantando robot invisible...", pd.DataFrame(), gr.update(visible=False), gr.update(interactive=False), [], [], gr.update(interactive=False)
|
| 174 |
-
|
| 175 |
ua = UserAgent()
|
| 176 |
|
| 177 |
with sync_playwright() as p:
|
|
@@ -179,7 +171,7 @@ def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo
|
|
| 179 |
context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
|
| 180 |
|
| 181 |
# 1. Finca Raíz
|
| 182 |
-
|
| 183 |
try:
|
| 184 |
page = context.new_page()
|
| 185 |
page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
|
|
@@ -199,8 +191,7 @@ def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo
|
|
| 199 |
card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement")
|
| 200 |
if not card: continue
|
| 201 |
|
| 202 |
-
txt = card.inner_text()
|
| 203 |
-
precio = extraer_precio(txt, operacion)
|
| 204 |
if precio > 0:
|
| 205 |
img_url = ""
|
| 206 |
img_el = card.query_selector("img")
|
|
@@ -208,15 +199,13 @@ def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo
|
|
| 208 |
if img_url and img_url.startswith("/"): img_url = "https://www.fincaraiz.com.co" + img_url
|
| 209 |
|
| 210 |
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})
|
| 211 |
-
urls_vistas.add(full_url)
|
| 212 |
-
cont_fr += 1
|
| 213 |
except: continue
|
| 214 |
-
page.close()
|
| 215 |
-
|
| 216 |
-
except Exception as e: log_visible += f"⚠️ Error en FR: {e}\n"
|
| 217 |
|
| 218 |
# 2. Metrocuadrado
|
| 219 |
-
|
| 220 |
try:
|
| 221 |
page = context.new_page()
|
| 222 |
page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
|
|
@@ -236,8 +225,7 @@ def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo
|
|
| 236 |
card = el.evaluate_handle("el => el.closest('li') || el.closest('[class*=\"card\"]') || el.closest('[class*=\"property\"]') || el.parentElement.parentElement.parentElement")
|
| 237 |
if not card: continue
|
| 238 |
|
| 239 |
-
txt = card.inner_text()
|
| 240 |
-
precio = extraer_precio(txt, operacion)
|
| 241 |
if precio > 0:
|
| 242 |
img_url = ""
|
| 243 |
img_el = card.query_selector("img")
|
|
@@ -245,15 +233,13 @@ def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo
|
|
| 245 |
if img_url and img_url.startswith("/"): img_url = "https://www.metrocuadrado.com" + img_url
|
| 246 |
|
| 247 |
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})
|
| 248 |
-
urls_vistas.add(full_url)
|
| 249 |
-
cont_mc += 1
|
| 250 |
except: continue
|
| 251 |
-
page.close()
|
| 252 |
-
log_visible += f"✅ MC: {cont_mc} inmuebles extraídos.\n"
|
| 253 |
except Exception as e: log_visible += f"⚠️ Error en MC.\n"
|
| 254 |
|
| 255 |
# 3. Módulo Financiero
|
| 256 |
-
|
| 257 |
try:
|
| 258 |
page = context.new_page(); page.goto(url_inversa, wait_until="domcontentloaded", timeout=45000)
|
| 259 |
for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(1000)
|
|
@@ -266,54 +252,55 @@ def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo
|
|
| 266 |
if not es_inmueble_valido(href, "FR"): continue
|
| 267 |
card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement")
|
| 268 |
if not card: continue
|
| 269 |
-
txt = card.inner_text()
|
| 270 |
-
precio = extraer_precio(txt, op_inversa)
|
| 271 |
if precio > 0: precios_inversos.append(precio)
|
| 272 |
except: continue
|
| 273 |
-
page.close()
|
| 274 |
-
log_visible += f"✅ Finanzas: Capturados {len(precios_inversos)} valores de {op_inversa}.\n"
|
| 275 |
except Exception as ex: pass
|
| 276 |
-
|
| 277 |
browser.close()
|
| 278 |
|
|
|
|
|
|
|
| 279 |
if not resultados:
|
| 280 |
-
|
| 281 |
-
return
|
| 282 |
|
| 283 |
-
# ---
|
| 284 |
opciones_check = []
|
| 285 |
for idx, r in enumerate(resultados):
|
| 286 |
-
|
| 287 |
-
etiqueta = f"{idx+1}. [{r['Portal']}] ${r['Precio']:,.0f} - {r['Ubicacion'][:35]} 👉 {r['URL']}"
|
| 288 |
r['etiqueta_ui'] = etiqueta
|
| 289 |
opciones_check.append(etiqueta)
|
| 290 |
|
| 291 |
-
# --- PREPARACIÓN DE TABLA INTERACTIVA
|
| 292 |
df_mostrar = pd.DataFrame(resultados)[['Portal', 'Precio', 'Ubicacion', 'URL']].copy()
|
|
|
|
|
|
|
| 293 |
df_mostrar['Precio'] = df_mostrar['Precio'].apply(lambda x: f"${x:,.0f}")
|
| 294 |
-
|
| 295 |
-
df_mostrar
|
| 296 |
-
df_mostrar.rename(columns={'URL': 'Enlace'}, inplace=True)
|
| 297 |
|
| 298 |
-
log_visible += "\n🛑 EXTRACCIÓN FINALIZADA.\n👉
|
|
|
|
|
|
|
| 299 |
|
| 300 |
-
# Desbloqueamos botones
|
| 301 |
-
yield log_visible, df_mostrar, gr.update(choices=opciones_check, value=opciones_check, visible=True), gr.update(interactive=True), resultados, precios_inversos, gr.update(interactive=True)
|
| 302 |
|
| 303 |
# ==========================================
|
| 304 |
-
# FASE 2: CÁLCULO Y GENERACIÓN
|
| 305 |
# ==========================================
|
| 306 |
-
def fase_2_generar_dictamen(seleccionados_ui, data_cruda, precios_inversos, operacion, barrio, zona_especifica, ciudad, area, tipo, hab, ban, park):
|
| 307 |
-
yield None, "⏳ Procesando curaduría y estadísticas...", "<p>Cargando mapa...</p>", gr.update(interactive=False)
|
| 308 |
-
|
| 309 |
if not seleccionados_ui:
|
| 310 |
-
|
| 311 |
-
return
|
| 312 |
|
|
|
|
| 313 |
resultados_filtrados = [r for r in data_cruda if r['etiqueta_ui'] in seleccionados_ui]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
df_final = pd.DataFrame(resultados_filtrados)
|
| 315 |
df_final['V_Homogeneizado'] = df_final['Precio'] * 0.92
|
| 316 |
|
|
|
|
| 317 |
mediana_m2 = df_final['Precio_M2'].median()
|
| 318 |
minimo_zona = df_final['Precio'].min()
|
| 319 |
maximo_zona = df_final['Precio'].max()
|
|
@@ -335,10 +322,10 @@ def fase_2_generar_dictamen(seleccionados_ui, data_cruda, precios_inversos, oper
|
|
| 335 |
cap_rate_txt = f"Canon Arriendo Promedio: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual (Cap Rate): {rentabilidad:.2f}%"
|
| 336 |
except: pass
|
| 337 |
|
| 338 |
-
|
| 339 |
mapa_html, lat_mapa, lon_mapa = generar_mapa(barrio, ciudad)
|
| 340 |
|
| 341 |
-
|
| 342 |
preparar_entorno_pdf()
|
| 343 |
pdf_path = f"Dictamen_Pericial_SAE_{int(time.time())}.pdf"
|
| 344 |
pdf = PDF_SAE()
|
|
@@ -412,13 +399,14 @@ def fase_2_generar_dictamen(seleccionados_ui, data_cruda, precios_inversos, oper
|
|
| 412 |
pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(*COLOR_NEGRO)
|
| 413 |
pdf.multi_cell(0, 4, sanear_texto(r['Descripcion']))
|
| 414 |
pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(0, 102, 204)
|
| 415 |
-
pdf.cell(0, 4, f">> Ver testigo
|
| 416 |
pdf.set_text_color(*COLOR_NEGRO); y_end = pdf.get_y(); pdf.set_y(max(y_start + 35, y_end + 5))
|
| 417 |
pdf.set_draw_color(200, 200, 200); pdf.line(10, pdf.get_y(), 200, pdf.get_y()); pdf.ln(4)
|
| 418 |
if img_path and os.path.exists(img_path):
|
| 419 |
try: os.remove(img_path)
|
| 420 |
except: pass
|
| 421 |
|
|
|
|
| 422 |
pdf.output(pdf_path)
|
| 423 |
|
| 424 |
resumen = (
|
|
@@ -429,7 +417,7 @@ def fase_2_generar_dictamen(seleccionados_ui, data_cruda, precios_inversos, oper
|
|
| 429 |
f"📊 **Análisis de Inversión:**\n{cap_rate_txt}"
|
| 430 |
)
|
| 431 |
|
| 432 |
-
|
| 433 |
|
| 434 |
def adaptar_interfaz(tipo):
|
| 435 |
if tipo in ["Lote", "Finca"]: return [gr.update(visible=False)] * 6
|
|
@@ -441,7 +429,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 441 |
estado_datos_crudos = gr.State([])
|
| 442 |
estado_datos_financieros = gr.State([])
|
| 443 |
|
| 444 |
-
gr.Markdown("## 🏢 TramitIA Pro: Dictamen Pericial (Extracción
|
| 445 |
|
| 446 |
with gr.Row():
|
| 447 |
with gr.Column(scale=1):
|
|
@@ -467,27 +455,29 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 467 |
|
| 468 |
gr.Markdown("---")
|
| 469 |
gr.Markdown("### 📋 CURADURÍA (Selecciona los válidos)")
|
|
|
|
| 470 |
selector_testigos = gr.CheckboxGroup(label="Testigos encontrados", choices=[], visible=False)
|
| 471 |
btn_generar = gr.Button("📄 2. GENERAR DICTAMEN", variant="secondary", interactive=False)
|
| 472 |
|
| 473 |
with gr.Column(scale=2):
|
| 474 |
res_fin = gr.Markdown("### 💰 Resultado, Progreso y Rentabilidad...")
|
| 475 |
with gr.Tabs():
|
| 476 |
-
|
| 477 |
-
with gr.TabItem("
|
|
|
|
| 478 |
with gr.TabItem("Descargar Dictamen (PDF)"): out_pdf = gr.File()
|
| 479 |
with gr.TabItem("Mapa Geoespacial"): mapa_ui = gr.HTML("<p style='text-align:center; color:gray;'>El mapa aparecerá aquí...</p>")
|
| 480 |
|
| 481 |
btn_buscar.click(
|
| 482 |
fase_1_buscar_testigos,
|
| 483 |
inputs=[op, b, c, a, m2_min, m2_max, t, h, ban, p, e, ascensor, piscina],
|
| 484 |
-
outputs=[msg, out_df, selector_testigos, btn_generar, estado_datos_crudos, estado_datos_financieros
|
| 485 |
)
|
| 486 |
|
| 487 |
btn_generar.click(
|
| 488 |
fase_2_generar_dictamen,
|
| 489 |
inputs=[selector_testigos, estado_datos_crudos, estado_datos_financieros, op, b, z, c, a, t, h, ban, p],
|
| 490 |
-
outputs=[out_pdf, res_fin, mapa_ui
|
| 491 |
)
|
| 492 |
|
| 493 |
demo.launch(theme=gr.themes.Soft())
|
|
|
|
| 28 |
subprocess.run(["pip", "install", "fake-useragent"], check=True)
|
| 29 |
from fake_useragent import UserAgent
|
| 30 |
|
|
|
|
| 31 |
URL_LOGO_GOBIERNO = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Escudo_de_Colombia.svg/250px-Escudo_de_Colombia.svg.png"
|
| 32 |
|
| 33 |
def descargar_recurso(url, nombre_archivo):
|
|
|
|
| 78 |
if tipo_slug == "local": tipo_fr = "locales"
|
| 79 |
elif tipo_slug == "edificio": tipo_fr = "edificios"
|
| 80 |
else: tipo_fr = tipo_slug + "s"
|
| 81 |
+
if tipo_slug in ["lote", "finca"]: filtros_fr = ""; filtros_mc = ""
|
| 82 |
+
else: filtros_fr = f"/{slug_park}/{slug_ant}"; filtros_mc = ""
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
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)}"
|
| 85 |
if ascensor and tipo_slug not in ["lote", "finca"]: url_fr_base += "/con-ascensor"
|
| 86 |
if piscina and tipo_slug in residenciales + ["finca"]: url_fr_base += "/con-piscina"
|
| 87 |
url_mc = f"https://www.metrocuadrado.com/{tipo_slug}/{op_slug}/{c_slug}/{b_slug}{filtros_mc}/?search=form"
|
|
|
|
| 88 |
return url_fr_base, url_mc
|
| 89 |
|
| 90 |
def extraer_precio(texto, operacion):
|
|
|
|
| 153 |
|
| 154 |
|
| 155 |
# ==========================================
|
| 156 |
+
# FASE 1: BÚSQUEDA (CON PROGRESO NATIVO)
|
| 157 |
# ==========================================
|
| 158 |
+
def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina, progress=gr.Progress()):
|
| 159 |
+
progress(0.1, desc="Generando rutas y conectando al navegador...")
|
|
|
|
| 160 |
resultados = []; urls_vistas = set(); precios_inversos = []
|
| 161 |
|
| 162 |
url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
|
|
|
|
| 164 |
url_inversa, _ = construir_urls_final(op_inversa, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
|
| 165 |
|
| 166 |
log_visible = f"✅ RUTAS GENERADAS:\n- FR: {url_fr}\n- MC: {url_mc}\n\n"
|
|
|
|
|
|
|
| 167 |
ua = UserAgent()
|
| 168 |
|
| 169 |
with sync_playwright() as p:
|
|
|
|
| 171 |
context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
|
| 172 |
|
| 173 |
# 1. Finca Raíz
|
| 174 |
+
progress(0.3, desc="🏢 Escaneando Finca Raíz...")
|
| 175 |
try:
|
| 176 |
page = context.new_page()
|
| 177 |
page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
|
|
|
|
| 191 |
card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement")
|
| 192 |
if not card: continue
|
| 193 |
|
| 194 |
+
txt = card.inner_text(); precio = extraer_precio(txt, operacion)
|
|
|
|
| 195 |
if precio > 0:
|
| 196 |
img_url = ""
|
| 197 |
img_el = card.query_selector("img")
|
|
|
|
| 199 |
if img_url and img_url.startswith("/"): img_url = "https://www.fincaraiz.com.co" + img_url
|
| 200 |
|
| 201 |
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})
|
| 202 |
+
urls_vistas.add(full_url); cont_fr += 1
|
|
|
|
| 203 |
except: continue
|
| 204 |
+
page.close(); log_visible += f"✅ FR: {cont_fr} inmuebles extraídos.\n"
|
| 205 |
+
except Exception as e: log_visible += f"⚠️ Error en FR.\n"
|
|
|
|
| 206 |
|
| 207 |
# 2. Metrocuadrado
|
| 208 |
+
progress(0.6, desc="🏢 Escaneando Metrocuadrado...")
|
| 209 |
try:
|
| 210 |
page = context.new_page()
|
| 211 |
page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
|
|
|
|
| 225 |
card = el.evaluate_handle("el => el.closest('li') || el.closest('[class*=\"card\"]') || el.closest('[class*=\"property\"]') || el.parentElement.parentElement.parentElement")
|
| 226 |
if not card: continue
|
| 227 |
|
| 228 |
+
txt = card.inner_text(); precio = extraer_precio(txt, operacion)
|
|
|
|
| 229 |
if precio > 0:
|
| 230 |
img_url = ""
|
| 231 |
img_el = card.query_selector("img")
|
|
|
|
| 233 |
if img_url and img_url.startswith("/"): img_url = "https://www.metrocuadrado.com" + img_url
|
| 234 |
|
| 235 |
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})
|
| 236 |
+
urls_vistas.add(full_url); cont_mc += 1
|
|
|
|
| 237 |
except: continue
|
| 238 |
+
page.close(); log_visible += f"✅ MC: {cont_mc} inmuebles extraídos.\n"
|
|
|
|
| 239 |
except Exception as e: log_visible += f"⚠️ Error en MC.\n"
|
| 240 |
|
| 241 |
# 3. Módulo Financiero
|
| 242 |
+
progress(0.8, desc="📈 Calculando Cap Rate (Búsqueda Inversa)...")
|
| 243 |
try:
|
| 244 |
page = context.new_page(); page.goto(url_inversa, wait_until="domcontentloaded", timeout=45000)
|
| 245 |
for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(1000)
|
|
|
|
| 252 |
if not es_inmueble_valido(href, "FR"): continue
|
| 253 |
card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement")
|
| 254 |
if not card: continue
|
| 255 |
+
txt = card.inner_text(); precio = extraer_precio(txt, op_inversa)
|
|
|
|
| 256 |
if precio > 0: precios_inversos.append(precio)
|
| 257 |
except: continue
|
| 258 |
+
page.close(); log_visible += f"✅ Finanzas: Capturados {len(precios_inversos)} valores de {op_inversa}.\n"
|
|
|
|
| 259 |
except Exception as ex: pass
|
|
|
|
| 260 |
browser.close()
|
| 261 |
|
| 262 |
+
progress(1.0, desc="¡Búsqueda Finalizada!")
|
| 263 |
+
|
| 264 |
if not resultados:
|
| 265 |
+
return log_visible, pd.DataFrame(), gr.update(choices=[], value=[], visible=False), gr.update(interactive=False), [], []
|
|
|
|
| 266 |
|
| 267 |
+
# --- TEXTOS LIMPIOS PARA LAS CASILLAS (EVITA BUGS DE GRADIO) ---
|
| 268 |
opciones_check = []
|
| 269 |
for idx, r in enumerate(resultados):
|
| 270 |
+
etiqueta = f"ID {idx+1} | {r['Portal'][:2]} | ${r['Precio']:,.0f} | {r['Ubicacion'][:25]}"
|
|
|
|
| 271 |
r['etiqueta_ui'] = etiqueta
|
| 272 |
opciones_check.append(etiqueta)
|
| 273 |
|
| 274 |
+
# --- PREPARACIÓN DE TABLA INTERACTIVA CON LINKS CLICKEABLES ---
|
| 275 |
df_mostrar = pd.DataFrame(resultados)[['Portal', 'Precio', 'Ubicacion', 'URL']].copy()
|
| 276 |
+
# Insertamos la columna ID para que el usuario sepa cuál desmarcar
|
| 277 |
+
df_mostrar.insert(0, 'ID_Testigo', [f"ID {i+1}" for i in range(len(resultados))])
|
| 278 |
df_mostrar['Precio'] = df_mostrar['Precio'].apply(lambda x: f"${x:,.0f}")
|
| 279 |
+
df_mostrar['URL'] = df_mostrar['URL'].apply(lambda x: f"[🌐 CLIC PARA VER INMUEBLE]({x})")
|
| 280 |
+
df_mostrar.rename(columns={'URL': 'Enlace Origial'}, inplace=True)
|
|
|
|
| 281 |
|
| 282 |
+
log_visible += "\n🛑 EXTRACCIÓN FINALIZADA.\n👉 Revisa la tabla de enlaces. Luego desmarca los IDs inválidos en la lista izquierda y haz clic en 'Generar Dictamen'."
|
| 283 |
+
|
| 284 |
+
return log_visible, df_mostrar, gr.update(choices=opciones_check, value=opciones_check, visible=True), gr.update(interactive=True), resultados, precios_inversos
|
| 285 |
|
|
|
|
|
|
|
| 286 |
|
| 287 |
# ==========================================
|
| 288 |
+
# FASE 2: CÁLCULO Y GENERACIÓN (CON PROGRESO NATIVO)
|
| 289 |
# ==========================================
|
| 290 |
+
def fase_2_generar_dictamen(seleccionados_ui, data_cruda, precios_inversos, operacion, barrio, zona_especifica, ciudad, area, tipo, hab, ban, park, progress=gr.Progress()):
|
|
|
|
|
|
|
| 291 |
if not seleccionados_ui:
|
| 292 |
+
return None, "❌ ERROR: No dejaste seleccionado ningún testigo. Selecciona al menos 1.", "<p>Error</p>"
|
|
|
|
| 293 |
|
| 294 |
+
progress(0.1, desc="Filtrando testigos seleccionados...")
|
| 295 |
resultados_filtrados = [r for r in data_cruda if r['etiqueta_ui'] in seleccionados_ui]
|
| 296 |
+
|
| 297 |
+
if len(resultados_filtrados) == 0:
|
| 298 |
+
return None, "❌ ERROR: La lista de testigos válidos está vacía.", "<p>Error</p>"
|
| 299 |
+
|
| 300 |
df_final = pd.DataFrame(resultados_filtrados)
|
| 301 |
df_final['V_Homogeneizado'] = df_final['Precio'] * 0.92
|
| 302 |
|
| 303 |
+
progress(0.4, desc="Ejecutando cálculos estadísticos SAE...")
|
| 304 |
mediana_m2 = df_final['Precio_M2'].median()
|
| 305 |
minimo_zona = df_final['Precio'].min()
|
| 306 |
maximo_zona = df_final['Precio'].max()
|
|
|
|
| 322 |
cap_rate_txt = f"Canon Arriendo Promedio: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual (Cap Rate): {rentabilidad:.2f}%"
|
| 323 |
except: pass
|
| 324 |
|
| 325 |
+
progress(0.6, desc="Dibujando mapa geoespacial...")
|
| 326 |
mapa_html, lat_mapa, lon_mapa = generar_mapa(barrio, ciudad)
|
| 327 |
|
| 328 |
+
progress(0.8, desc="Maquetando documento PDF...")
|
| 329 |
preparar_entorno_pdf()
|
| 330 |
pdf_path = f"Dictamen_Pericial_SAE_{int(time.time())}.pdf"
|
| 331 |
pdf = PDF_SAE()
|
|
|
|
| 399 |
pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(*COLOR_NEGRO)
|
| 400 |
pdf.multi_cell(0, 4, sanear_texto(r['Descripcion']))
|
| 401 |
pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(0, 102, 204)
|
| 402 |
+
pdf.cell(0, 4, f">> Ver testigo original online", link=r['URL'], ln=True)
|
| 403 |
pdf.set_text_color(*COLOR_NEGRO); y_end = pdf.get_y(); pdf.set_y(max(y_start + 35, y_end + 5))
|
| 404 |
pdf.set_draw_color(200, 200, 200); pdf.line(10, pdf.get_y(), 200, pdf.get_y()); pdf.ln(4)
|
| 405 |
if img_path and os.path.exists(img_path):
|
| 406 |
try: os.remove(img_path)
|
| 407 |
except: pass
|
| 408 |
|
| 409 |
+
progress(1.0, desc="¡Dictamen Finalizado!")
|
| 410 |
pdf.output(pdf_path)
|
| 411 |
|
| 412 |
resumen = (
|
|
|
|
| 417 |
f"📊 **Análisis de Inversión:**\n{cap_rate_txt}"
|
| 418 |
)
|
| 419 |
|
| 420 |
+
return pdf_path, resumen, mapa_html
|
| 421 |
|
| 422 |
def adaptar_interfaz(tipo):
|
| 423 |
if tipo in ["Lote", "Finca"]: return [gr.update(visible=False)] * 6
|
|
|
|
| 429 |
estado_datos_crudos = gr.State([])
|
| 430 |
estado_datos_financieros = gr.State([])
|
| 431 |
|
| 432 |
+
gr.Markdown("## 🏢 TramitIA Pro: Dictamen Pericial (Extracción Curada)")
|
| 433 |
|
| 434 |
with gr.Row():
|
| 435 |
with gr.Column(scale=1):
|
|
|
|
| 455 |
|
| 456 |
gr.Markdown("---")
|
| 457 |
gr.Markdown("### 📋 CURADURÍA (Selecciona los válidos)")
|
| 458 |
+
# La caja de Checkboxes usará textos seguros
|
| 459 |
selector_testigos = gr.CheckboxGroup(label="Testigos encontrados", choices=[], visible=False)
|
| 460 |
btn_generar = gr.Button("📄 2. GENERAR DICTAMEN", variant="secondary", interactive=False)
|
| 461 |
|
| 462 |
with gr.Column(scale=2):
|
| 463 |
res_fin = gr.Markdown("### 💰 Resultado, Progreso y Rentabilidad...")
|
| 464 |
with gr.Tabs():
|
| 465 |
+
# Dataframe optimizado para mostrar Markdown clickeables
|
| 466 |
+
with gr.TabItem("Exploración Bruta (Links)"): out_df = gr.Dataframe(datatype=["str", "str", "str", "str", "markdown"])
|
| 467 |
+
with gr.TabItem("Log del Sistema"): msg = gr.Textbox(lines=12, label="Progreso del Robot")
|
| 468 |
with gr.TabItem("Descargar Dictamen (PDF)"): out_pdf = gr.File()
|
| 469 |
with gr.TabItem("Mapa Geoespacial"): mapa_ui = gr.HTML("<p style='text-align:center; color:gray;'>El mapa aparecerá aquí...</p>")
|
| 470 |
|
| 471 |
btn_buscar.click(
|
| 472 |
fase_1_buscar_testigos,
|
| 473 |
inputs=[op, b, c, a, m2_min, m2_max, t, h, ban, p, e, ascensor, piscina],
|
| 474 |
+
outputs=[msg, out_df, selector_testigos, btn_generar, estado_datos_crudos, estado_datos_financieros]
|
| 475 |
)
|
| 476 |
|
| 477 |
btn_generar.click(
|
| 478 |
fase_2_generar_dictamen,
|
| 479 |
inputs=[selector_testigos, estado_datos_crudos, estado_datos_financieros, op, b, z, c, a, t, h, ban, p],
|
| 480 |
+
outputs=[out_pdf, res_fin, mapa_ui]
|
| 481 |
)
|
| 482 |
|
| 483 |
demo.launch(theme=gr.themes.Soft())
|