Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -8,6 +8,7 @@ from fpdf import FPDF
|
|
| 8 |
import gradio as gr
|
| 9 |
from playwright.sync_api import sync_playwright
|
| 10 |
import time
|
|
|
|
| 11 |
import requests
|
| 12 |
from PIL import Image
|
| 13 |
import io
|
|
@@ -115,7 +116,6 @@ def generar_mapa(barrio, ciudad):
|
|
| 115 |
folium.Marker([lat, lon], popup=f"Zona de Estudio: {barrio.title()}", icon=folium.Icon(color="red", icon="info-sign")).add_to(m)
|
| 116 |
return m._repr_html_(), lat, lon
|
| 117 |
except: pass
|
| 118 |
-
# Coordenadas por defecto (Centro Barranquilla)
|
| 119 |
return "<p>Mapa no disponible</p>", 10.9639, -74.7964
|
| 120 |
|
| 121 |
# --- CLASE PDF ---
|
|
@@ -143,12 +143,12 @@ class PDF_SAE(FPDF):
|
|
| 143 |
# --- MOTOR PRINCIPAL ---
|
| 144 |
def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina):
|
| 145 |
resultados = []; log_visible = ""; urls_vistas = set()
|
| 146 |
-
precios_inversos = []
|
| 147 |
|
| 148 |
try:
|
| 149 |
url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
|
| 150 |
|
| 151 |
-
# URL INVERSA
|
| 152 |
op_inversa = "Venta" if operacion == "Arriendo" else "Arriendo"
|
| 153 |
url_inversa, _ = construir_urls_final(op_inversa, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
|
| 154 |
|
|
@@ -159,7 +159,7 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
|
|
| 159 |
browser = p.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled', '--no-sandbox'])
|
| 160 |
context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
|
| 161 |
|
| 162 |
-
# 1. EXTRACCIÓN PRINCIPAL (
|
| 163 |
try:
|
| 164 |
page = context.new_page(); page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
|
| 165 |
for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
|
|
@@ -185,7 +185,7 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
|
|
| 185 |
page.close(); log_visible += f"✅ FR: {cont_fr} inmuebles.\n"
|
| 186 |
except Exception as e: log_visible += f"⚠️ Error FR.\n"
|
| 187 |
|
| 188 |
-
# 2. EXTRACCIÓN PRINCIPAL (
|
| 189 |
try:
|
| 190 |
page = context.new_page(); page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
|
| 191 |
for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
|
|
@@ -211,17 +211,37 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
|
|
| 211 |
page.close(); log_visible += f"✅ MC: {cont_mc} inmuebles.\n"
|
| 212 |
except Exception as e: log_visible += f"⚠️ Error MC.\n"
|
| 213 |
|
| 214 |
-
# 3. EXTRACCIÓN INVERSA SILENCIOSA (MÓDULO FINANCIERO)
|
| 215 |
-
log_visible += f"\n🔄 Ejecutando Módulo Financiero (Buscando {op_inversa})...\n"
|
| 216 |
try:
|
| 217 |
-
page = context.new_page(); page.goto(url_inversa, wait_until="domcontentloaded", timeout=
|
| 218 |
-
page.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
elementos = page.query_selector_all("a")
|
| 220 |
-
for el in elementos
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
page.close()
|
| 224 |
-
|
|
|
|
|
|
|
| 225 |
|
| 226 |
browser.close()
|
| 227 |
|
|
@@ -234,7 +254,6 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
|
|
| 234 |
df_final = pd.concat([df_fr, df_mc]).reset_index(drop=True)
|
| 235 |
|
| 236 |
# --- MÓDULO 1: HOMOGENEIZACIÓN MATEMÁTICA ---
|
| 237 |
-
# Aplicamos factor de comercialización a todos los testigos
|
| 238 |
df_final['V_Homogeneizado'] = df_final['Precio'] * 0.92
|
| 239 |
|
| 240 |
mediana_m2 = df_final['Precio_M2'].median()
|
|
@@ -248,22 +267,26 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
|
|
| 248 |
valor_minimo_neg = (mediana_m2 * area) * 0.90
|
| 249 |
|
| 250 |
# --- MÓDULO 2: INTELIGENCIA FINANCIERA (CAP RATE) ---
|
| 251 |
-
cap_rate_txt = "Datos insuficientes para
|
| 252 |
-
if precios_inversos:
|
| 253 |
mediana_inversa = pd.Series(precios_inversos).median()
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
# --- MÓDULO 3: MAPA GEOESPACIAL ---
|
| 264 |
mapa_html, lat_mapa, lon_mapa = generar_mapa(barrio, ciudad)
|
| 265 |
|
| 266 |
-
# --- GENERACIÓN DEL PDF
|
| 267 |
preparar_entorno_pdf()
|
| 268 |
pdf_path = f"Dictamen_Pericial_SAE_{int(time.time())}.pdf"
|
| 269 |
pdf = PDF_SAE()
|
|
@@ -285,6 +308,7 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
|
|
| 285 |
pdf.cell(0, 10, sanear_texto("1. DATOS GEOESPACIALES Y DEL ACTIVO"), ln=True)
|
| 286 |
pdf.set_font("Arial", '', 11); pdf.set_text_color(*COLOR_NEGRO)
|
| 287 |
pdf.cell(0, 6, sanear_texto(f"- Operacion: {operacion.capitalize()}"), ln=True)
|
|
|
|
| 288 |
pdf.cell(0, 6, sanear_texto(f"- Ubicacion: Barrio {barrio.title()}, {ciudad.title()}"), ln=True)
|
| 289 |
pdf.cell(0, 6, sanear_texto(f"- Coordenadas de Influencia: Lat {lat_mapa:.4f}, Lon {lon_mapa:.4f}"), ln=True)
|
| 290 |
pdf.cell(0, 6, sanear_texto(f"- Area Construida: {area} m2"), ln=True)
|
|
@@ -333,7 +357,6 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
|
|
| 333 |
try: pdf.image(img_path, x=10, y=y_start, w=45, h=30); text_x = 60
|
| 334 |
except: pass
|
| 335 |
|
| 336 |
-
# Mostrar precio Oferta y Homogeneizado
|
| 337 |
pdf.set_xy(text_x, y_start); pdf.set_font("Arial", 'B', 11)
|
| 338 |
pdf.cell(0, 6, f"Oferta: ${r['Precio']:,.0f} | Homogeneizado: ${r['V_Homogeneizado']:,.0f}", ln=True)
|
| 339 |
|
|
@@ -398,7 +421,7 @@ with gr.Blocks() as demo:
|
|
| 398 |
res_fin = gr.Markdown("### 💰 Resultado y Rentabilidad (Cap Rate)...")
|
| 399 |
with gr.Tabs():
|
| 400 |
with gr.TabItem("Mapa Geoespacial"):
|
| 401 |
-
mapa_ui = gr.HTML("<p style='text-align:center; color:gray;'>El mapa
|
| 402 |
with gr.TabItem("Descargar Dictamen (PDF)"): out_pdf = gr.File()
|
| 403 |
with gr.TabItem("Tabla de Homogeneización"): out_df = gr.Dataframe()
|
| 404 |
with gr.TabItem("Log de Sistema"): msg = gr.Textbox(lines=10)
|
|
|
|
| 8 |
import gradio as gr
|
| 9 |
from playwright.sync_api import sync_playwright
|
| 10 |
import time
|
| 11 |
+
import random
|
| 12 |
import requests
|
| 13 |
from PIL import Image
|
| 14 |
import io
|
|
|
|
| 116 |
folium.Marker([lat, lon], popup=f"Zona de Estudio: {barrio.title()}", icon=folium.Icon(color="red", icon="info-sign")).add_to(m)
|
| 117 |
return m._repr_html_(), lat, lon
|
| 118 |
except: pass
|
|
|
|
| 119 |
return "<p>Mapa no disponible</p>", 10.9639, -74.7964
|
| 120 |
|
| 121 |
# --- CLASE PDF ---
|
|
|
|
| 143 |
# --- MOTOR PRINCIPAL ---
|
| 144 |
def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina):
|
| 145 |
resultados = []; log_visible = ""; urls_vistas = set()
|
| 146 |
+
precios_inversos = []
|
| 147 |
|
| 148 |
try:
|
| 149 |
url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
|
| 150 |
|
| 151 |
+
# URL INVERSA
|
| 152 |
op_inversa = "Venta" if operacion == "Arriendo" else "Arriendo"
|
| 153 |
url_inversa, _ = construir_urls_final(op_inversa, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
|
| 154 |
|
|
|
|
| 159 |
browser = p.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled', '--no-sandbox'])
|
| 160 |
context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
|
| 161 |
|
| 162 |
+
# 1. EXTRACCIÓN PRINCIPAL (FR)
|
| 163 |
try:
|
| 164 |
page = context.new_page(); page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
|
| 165 |
for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
|
|
|
|
| 185 |
page.close(); log_visible += f"✅ FR: {cont_fr} inmuebles.\n"
|
| 186 |
except Exception as e: log_visible += f"⚠️ Error FR.\n"
|
| 187 |
|
| 188 |
+
# 2. EXTRACCIÓN PRINCIPAL (MC)
|
| 189 |
try:
|
| 190 |
page = context.new_page(); page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
|
| 191 |
for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
|
|
|
|
| 211 |
page.close(); log_visible += f"✅ MC: {cont_mc} inmuebles.\n"
|
| 212 |
except Exception as e: log_visible += f"⚠️ Error MC.\n"
|
| 213 |
|
| 214 |
+
# 3. EXTRACCIÓN INVERSA SILENCIOSA (MÓDULO FINANCIERO MEJORADO)
|
| 215 |
+
log_visible += f"\n🔄 Ejecutando Módulo Financiero (Buscando {op_inversa} para Cap Rate)...\n"
|
| 216 |
try:
|
| 217 |
+
page = context.new_page(); page.goto(url_inversa, wait_until="domcontentloaded", timeout=45000)
|
| 218 |
+
try: page.wait_for_load_state("networkidle", timeout=5000)
|
| 219 |
+
except: pass
|
| 220 |
+
|
| 221 |
+
# Hacemos scroll para asegurar que carguen los elementos
|
| 222 |
+
for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(1500)
|
| 223 |
+
|
| 224 |
elementos = page.query_selector_all("a")
|
| 225 |
+
for el in elementos:
|
| 226 |
+
if len(precios_inversos) >= 8: break # Con atrapar 8 datos es suficiente para una mediana robusta
|
| 227 |
+
try:
|
| 228 |
+
href = el.get_attribute("href")
|
| 229 |
+
if not es_inmueble_valido(href, "FR"): continue
|
| 230 |
+
|
| 231 |
+
# Usamos el mismo radar preciso de la búsqueda principal
|
| 232 |
+
card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement")
|
| 233 |
+
if not card: continue
|
| 234 |
+
|
| 235 |
+
txt = card.inner_text()
|
| 236 |
+
precio = extraer_precio(txt, op_inversa)
|
| 237 |
+
|
| 238 |
+
if precio > 0:
|
| 239 |
+
precios_inversos.append(precio)
|
| 240 |
+
except: continue
|
| 241 |
page.close()
|
| 242 |
+
log_visible += f"✅ Módulo Financiero: Capturados {len(precios_inversos)} valores de {op_inversa} en la zona.\n"
|
| 243 |
+
except Exception as ex:
|
| 244 |
+
log_visible += f"⚠️ Módulo Financiero falló: {ex}\n"
|
| 245 |
|
| 246 |
browser.close()
|
| 247 |
|
|
|
|
| 254 |
df_final = pd.concat([df_fr, df_mc]).reset_index(drop=True)
|
| 255 |
|
| 256 |
# --- MÓDULO 1: HOMOGENEIZACIÓN MATEMÁTICA ---
|
|
|
|
| 257 |
df_final['V_Homogeneizado'] = df_final['Precio'] * 0.92
|
| 258 |
|
| 259 |
mediana_m2 = df_final['Precio_M2'].median()
|
|
|
|
| 267 |
valor_minimo_neg = (mediana_m2 * area) * 0.90
|
| 268 |
|
| 269 |
# --- MÓDULO 2: INTELIGENCIA FINANCIERA (CAP RATE) ---
|
| 270 |
+
cap_rate_txt = "Datos insuficientes en la zona para cruzar la rentabilidad."
|
| 271 |
+
if len(precios_inversos) > 0:
|
| 272 |
mediana_inversa = pd.Series(precios_inversos).median()
|
| 273 |
+
|
| 274 |
+
# Para evitar rentabilidades rotas si el portal cruza mal los datos (ej: Venta de lote en vez de apto)
|
| 275 |
+
try:
|
| 276 |
+
if operacion == "Arriendo":
|
| 277 |
+
# Tasa Cap = (Arriendo Anual Sugerido / Valor de Venta Promedio de la zona)
|
| 278 |
+
rentabilidad = ((valor_total_sugerido * 12) / mediana_inversa) * 100
|
| 279 |
+
cap_rate_txt = f"Valor Comercial de Venta Promedio en la zona: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual Estimada (Cap Rate): {rentabilidad:.2f}%"
|
| 280 |
+
else: # Operación = Venta
|
| 281 |
+
# Tasa Cap = (Arriendo Anual Promedio de la zona / Valor de Venta Sugerido)
|
| 282 |
+
rentabilidad = ((mediana_inversa * 12) / valor_total_sugerido) * 100
|
| 283 |
+
cap_rate_txt = f"Canon de Arriendo Mensual Promedio en la zona: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual Estimada (Cap Rate): {rentabilidad:.2f}%"
|
| 284 |
+
except: pass
|
| 285 |
|
| 286 |
# --- MÓDULO 3: MAPA GEOESPACIAL ---
|
| 287 |
mapa_html, lat_mapa, lon_mapa = generar_mapa(barrio, ciudad)
|
| 288 |
|
| 289 |
+
# --- GENERACIÓN DEL PDF ---
|
| 290 |
preparar_entorno_pdf()
|
| 291 |
pdf_path = f"Dictamen_Pericial_SAE_{int(time.time())}.pdf"
|
| 292 |
pdf = PDF_SAE()
|
|
|
|
| 308 |
pdf.cell(0, 10, sanear_texto("1. DATOS GEOESPACIALES Y DEL ACTIVO"), ln=True)
|
| 309 |
pdf.set_font("Arial", '', 11); pdf.set_text_color(*COLOR_NEGRO)
|
| 310 |
pdf.cell(0, 6, sanear_texto(f"- Operacion: {operacion.capitalize()}"), ln=True)
|
| 311 |
+
pdf.cell(0, 6, sanear_texto(f"- Tipo de Inmueble: {tipo.capitalize()}"), ln=True)
|
| 312 |
pdf.cell(0, 6, sanear_texto(f"- Ubicacion: Barrio {barrio.title()}, {ciudad.title()}"), ln=True)
|
| 313 |
pdf.cell(0, 6, sanear_texto(f"- Coordenadas de Influencia: Lat {lat_mapa:.4f}, Lon {lon_mapa:.4f}"), ln=True)
|
| 314 |
pdf.cell(0, 6, sanear_texto(f"- Area Construida: {area} m2"), ln=True)
|
|
|
|
| 357 |
try: pdf.image(img_path, x=10, y=y_start, w=45, h=30); text_x = 60
|
| 358 |
except: pass
|
| 359 |
|
|
|
|
| 360 |
pdf.set_xy(text_x, y_start); pdf.set_font("Arial", 'B', 11)
|
| 361 |
pdf.cell(0, 6, f"Oferta: ${r['Precio']:,.0f} | Homogeneizado: ${r['V_Homogeneizado']:,.0f}", ln=True)
|
| 362 |
|
|
|
|
| 421 |
res_fin = gr.Markdown("### 💰 Resultado y Rentabilidad (Cap Rate)...")
|
| 422 |
with gr.Tabs():
|
| 423 |
with gr.TabItem("Mapa Geoespacial"):
|
| 424 |
+
mapa_ui = gr.HTML("<p style='text-align:center; color:gray;'>El mapa aparecerá aquí...</p>")
|
| 425 |
with gr.TabItem("Descargar Dictamen (PDF)"): out_pdf = gr.File()
|
| 426 |
with gr.TabItem("Tabla de Homogeneización"): out_df = gr.Dataframe()
|
| 427 |
with gr.TabItem("Log de Sistema"): msg = gr.Textbox(lines=10)
|