jcalbornoz commited on
Commit
43088bd
·
verified ·
1 Parent(s): 4e0b97c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -57
app.py CHANGED
@@ -8,7 +8,6 @@ from fpdf import FPDF
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
@@ -61,20 +60,48 @@ def descargar_imagen(url, idx):
61
  except: return None
62
  return None
63
 
 
64
  def construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina):
65
  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"}
66
  slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anos")
67
  slug_park = f"{int(park)}-parqueadero" if int(park) == 1 else f"{int(park)}-parqueaderos"
 
68
  b_slug = barrio.lower().strip().replace(" ", "-")
69
  c_slug = ciudad.lower().strip().replace(" ", "-")
70
  op_slug = operacion.lower().strip()
71
  tipo_slug = tipo.lower().strip()
72
- tipo_fr = "casas-y-apartamentos-y-apartaestudios" if tipo_slug in ["apartamento", "casa"] else tipo_slug + "s"
73
 
74
- url_fr_base = f"https://www.fincaraiz.com.co/{op_slug}/{tipo_fr}/{b_slug}/{c_slug}/{int(hab)}-o-mas-habitaciones/{int(ban)}-o-mas-banos/{slug_park}/{slug_ant}/m2-desde-{int(m2_min)}/m2-hasta-{int(m2_max)}"
75
- if ascensor: url_fr_base += "/con-ascensor"
76
- if piscina: url_fr_base += "/con-piscina"
77
- url_mc = f"https://www.metrocuadrado.com/{tipo_slug}-casa-oficina/{op_slug}/{c_slug}/{b_slug}/{int(ban)}-banos-{int(hab)}-habitaciones/?search=form"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  return url_fr_base, url_mc
79
 
80
  def extraer_precio(texto, operacion):
@@ -91,7 +118,7 @@ def extraer_ubicacion(texto):
91
  for linea in lineas:
92
  if "$" in linea or re.search(r'(?i)(hab|baño|m2|m²)', linea): continue
93
  if "," in linea or " en " in linea.lower():
94
- limpio = re.sub(r'(?i)(apartamento|casa|bodega|lote|oficina)\s+en\s+(arriendo|venta)\s+(en\s+)?', '', linea)
95
  return limpio[:60].strip()
96
  for linea in lineas[1:4]:
97
  if "$" not in linea and not re.search(r'\d', linea): return linea[:60]
@@ -159,7 +186,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 (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,12 +212,12 @@ 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 (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)
192
  elementos = page.query_selector_all("a"); cont_mc = 0
193
- for el in elementos:
194
  if cont_mc >= 12: break
195
  try:
196
  href = el.get_attribute("href")
@@ -211,41 +238,29 @@ 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 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
 
248
- if not resultados: return f"{log_visible}\n❌ NO HAY DATOS.", pd.DataFrame(), None, "---", "Sin Mapa"
249
 
250
  # --- LIMPIEZA DE DATOS ---
251
  df_final_completo = pd.DataFrame(resultados)
@@ -253,9 +268,8 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
253
  df_mc = df_final_completo[df_final_completo['Portal'] == 'Metrocuadrado'].head(6)
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()
260
  minimo_zona = df_final['Precio'].min()
261
  maximo_zona = df_final['Precio'].max()
@@ -266,34 +280,25 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
266
  valor_maximo_neg = (mediana_m2 * area) * 0.95
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()
293
-
294
- COLOR_ROSADO = (254, 25, 120)
295
- COLOR_GRIS = (137, 137, 137)
296
- COLOR_NEGRO = (0, 0, 0)
297
 
298
  # PÁGINA 1
299
  pdf.add_page()
@@ -301,8 +306,7 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
301
  pdf.cell(0, 15, sanear_texto("DICTAMEN COMERCIAL Y FINANCIERO"), ln=True, align='C')
302
  pdf.set_font("Arial", 'B', 14); pdf.set_text_color(*COLOR_GRIS)
303
  pdf.cell(0, 8, sanear_texto(f"METODOLOGIA COMPARATIVA DE MERCADO"), ln=True, align='C')
304
- pdf.line(20, 65, 190, 65)
305
- pdf.ln(15)
306
 
307
  pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_ROSADO)
308
  pdf.cell(0, 10, sanear_texto("1. DATOS GEOESPACIALES Y DEL ACTIVO"), ln=True)
@@ -320,7 +324,7 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
320
  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}"))
321
  pdf.ln(5)
322
 
323
- # PÁGINA 2: HOMOGENEIZACIÓN Y BANDAS
324
  pdf.add_page()
325
  pdf.set_font("Arial", 'B', 12); pdf.set_fill_color(*COLOR_ROSADO); pdf.set_text_color(255, 255, 255)
326
  pdf.cell(0, 10, sanear_texto(" 3. MATRIZ DE HOMOGENEIZACION Y RANGOS"), ln=True, fill=True)
@@ -334,7 +338,6 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
334
  pdf.cell(50, 8, sanear_texto("-8.00%"), border=1, ln=True, align='C')
335
  pdf.ln(10)
336
 
337
- # BANDAS
338
  pdf.set_font("Arial", 'B', 14); pdf.set_text_color(*COLOR_ROSADO)
339
  pdf.cell(0, 10, sanear_texto("RANGOS DE NEGOCIACION AUTORIZADOS (CIERRE):"), ln=True)
340
  pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_GRIS)
@@ -344,7 +347,7 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
344
  pdf.set_font("Arial", 'B', 12); pdf.set_text_color(200, 0, 0)
345
  pdf.cell(0, 8, sanear_texto(f"PISO MINIMO ACEPTABLE (-10%): ${valor_minimo_neg:,.0f} COP"), ln=True)
346
 
347
- # PÁGINA 3+: ANEXOS
348
  pdf.add_page()
349
  pdf.set_font("Arial", 'B', 12); pdf.set_fill_color(*COLOR_GRIS); pdf.set_text_color(255, 255, 255)
350
  pdf.cell(0, 10, sanear_texto(" 4. ANEXO TECNICO: TESTIGOS COMPARABLES"), ln=True, fill=True)
@@ -356,10 +359,8 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
356
  if img_path and os.path.exists(img_path):
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
-
363
  pdf.set_x(text_x); pdf.set_font("Arial", 'B', 8); pdf.set_text_color(*COLOR_ROSADO)
364
  pdf.cell(0, 4, f"Ubicacion: {sanear_texto(r['Ubicacion'])}", ln=True)
365
  pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(*COLOR_NEGRO)
@@ -374,7 +375,6 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
374
 
375
  pdf.output(pdf_path)
376
 
377
- # --- CÁLCULOS INTERFAZ ---
378
  resumen = (
379
  f"🏢 **DICTAMEN PERICIAL Y FINANCIERO**\n"
380
  f"📈 **Techo Máximo:** ${valor_maximo_neg:,.0f}\n"
@@ -391,9 +391,17 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
391
  return f"{log_visible}\n✅ Dictamen Maestro Generado.", df_mostrar, pdf_path, resumen, mapa_html
392
 
393
  except Exception as error_fatal:
394
- traza = traceback.format_exc()
395
  return f"❌ ERROR GRAVE:\n{str(error_fatal)}", pd.DataFrame(), None, "⚠️ Falló la ejecución", "<p>Error en mapa</p>"
396
 
 
 
 
 
 
 
 
 
 
397
  # --- INTERFAZ GRÁFICA ---
398
  with gr.Blocks() as demo:
399
  gr.Markdown("## 🏢 TramitIA Pro: Dictamen Pericial Máster (IGAC/SAE)")
@@ -403,25 +411,35 @@ with gr.Blocks() as demo:
403
  op = gr.Radio(["Arriendo", "Venta"], label="Tipo de Operación", value="Arriendo")
404
  c = gr.Textbox(label="Ciudad", value="Barranquilla")
405
  b = gr.Textbox(label="Barrio (Ej: La Concepcion)", value="La Concepcion")
 
406
  with gr.Row():
407
- t = gr.Dropdown(["Apartamento", "Casa", "Bodega", "Lote", "Oficina"], label="Tipo de Inmueble", value="Apartamento")
408
  a = gr.Number(label="Área M2 a tasar", value=70)
 
409
  with gr.Row():
410
  m2_min = gr.Number(label="M2 Mínimo", value=10)
411
  m2_max = gr.Number(label="M2 Máximo", value=200)
 
 
412
  with gr.Row():
413
  ascensor = gr.Checkbox(label="Con Ascensor")
414
  piscina = gr.Checkbox(label="Con Piscina")
415
  with gr.Row():
416
- h = gr.Number(label="Hab", value=3); ban = gr.Number(label="Baños", value=2); p = gr.Number(label="Park", value=1)
 
 
 
417
  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")
 
 
 
 
418
  btn = gr.Button("GENERAR DICTAMEN INTEGRAL", variant="primary")
419
 
420
  with gr.Column(scale=2):
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)
 
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
 
60
  except: return None
61
  return None
62
 
63
+ # --- CONSTRUCTOR DE URLS INTELIGENTE (ACTUALIZADO) ---
64
  def construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina):
65
  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"}
66
  slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anos")
67
  slug_park = f"{int(park)}-parqueadero" if int(park) == 1 else f"{int(park)}-parqueaderos"
68
+
69
  b_slug = barrio.lower().strip().replace(" ", "-")
70
  c_slug = ciudad.lower().strip().replace(" ", "-")
71
  op_slug = operacion.lower().strip()
72
  tipo_slug = tipo.lower().strip()
 
73
 
74
+ residenciales = ["apartamento", "casa", "apartaestudio"]
75
+
76
+ # 1. Ajuste de Estructura de Filtros según Inmueble
77
+ if tipo_slug in residenciales:
78
+ tipo_fr = "casas-y-apartamentos-y-apartaestudios"
79
+ filtros_fr = f"/{int(hab)}-o-mas-habitaciones/{int(ban)}-o-mas-banos/{slug_park}/{slug_ant}"
80
+ filtros_mc = f"/{int(ban)}-banos-{int(hab)}-habitaciones"
81
+ else:
82
+ # Es Comercial o Terreno (Bodega, Local, Oficina, Lote, Finca)
83
+ if tipo_slug == "local": tipo_fr = "locales"
84
+ elif tipo_slug == "edificio": tipo_fr = "edificios"
85
+ else: tipo_fr = tipo_slug + "s"
86
+
87
+ # Lotes y Fincas no llevan ni baños, ni cuartos, ni antigüedad ni parqueaderos en la ruta
88
+ if tipo_slug in ["lote", "finca"]:
89
+ filtros_fr = ""
90
+ filtros_mc = ""
91
+ else:
92
+ # Oficinas, Bodegas, Locales sí llevan parqueaderos y antigüedad, pero NO cuartos ni baños
93
+ filtros_fr = f"/{slug_park}/{slug_ant}"
94
+ filtros_mc = ""
95
+
96
+ # 2. Generación Final de Rutas
97
+ 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)}"
98
+
99
+ # Excepciones lógicas de amenidades
100
+ if ascensor and tipo_slug not in ["lote", "finca"]: url_fr_base += "/con-ascensor"
101
+ if piscina and tipo_slug in residenciales + ["finca"]: url_fr_base += "/con-piscina"
102
+
103
+ url_mc = f"https://www.metrocuadrado.com/{tipo_slug}/{op_slug}/{c_slug}/{b_slug}{filtros_mc}/?search=form"
104
+
105
  return url_fr_base, url_mc
106
 
107
  def extraer_precio(texto, operacion):
 
118
  for linea in lineas:
119
  if "$" in linea or re.search(r'(?i)(hab|baño|m2|m²)', linea): continue
120
  if "," in linea or " en " in linea.lower():
121
+ limpio = re.sub(r'(?i)(apartamento|casa|bodega|lote|oficina|local|consultorio|finca|edificio)\s+en\s+(arriendo|venta)\s+(en\s+)?', '', linea)
122
  return limpio[:60].strip()
123
  for linea in lineas[1:4]:
124
  if "$" not in linea and not re.search(r'\d', linea): return linea[:60]
 
186
  browser = p.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled', '--no-sandbox'])
187
  context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
188
 
189
+ # 1. EXTRACCIÓN (FR)
190
  try:
191
  page = context.new_page(); page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
192
  for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
 
212
  page.close(); log_visible += f"✅ FR: {cont_fr} inmuebles.\n"
213
  except Exception as e: log_visible += f"⚠️ Error FR.\n"
214
 
215
+ # 2. EXTRACCIÓN (MC)
216
  try:
217
  page = context.new_page(); page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
218
  for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
219
  elementos = page.query_selector_all("a"); cont_mc = 0
220
+ for el in elements:
221
  if cont_mc >= 12: break
222
  try:
223
  href = el.get_attribute("href")
 
238
  page.close(); log_visible += f"✅ MC: {cont_mc} inmuebles.\n"
239
  except Exception as e: log_visible += f"⚠️ Error MC.\n"
240
 
241
+ # 3. MÓDULO FINANCIERO
242
  log_visible += f"\n🔄 Ejecutando Módulo Financiero (Buscando {op_inversa} para Cap Rate)...\n"
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(1500)
 
246
  elementos = page.query_selector_all("a")
247
  for el in elementos:
248
+ if len(precios_inversos) >= 8: break
249
  try:
250
  href = el.get_attribute("href")
251
  if not es_inmueble_valido(href, "FR"): continue
 
 
252
  card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement")
253
  if not card: continue
254
+ txt = card.inner_text(); precio = extraer_precio(txt, op_inversa)
255
+ if precio > 0: precios_inversos.append(precio)
 
 
 
 
256
  except: continue
257
  page.close()
258
+ log_visible += f"✅ Finanzas: Capturados {len(precios_inversos)} valores de {op_inversa} en la zona.\n"
259
+ except Exception as ex: pass
 
260
 
261
  browser.close()
262
 
263
+ if not resultados: return f"{log_visible}\n❌ NO HAY DATOS. Intenta ampliar los M2 o quitar filtros.", pd.DataFrame(), None, "---", "Sin Mapa"
264
 
265
  # --- LIMPIEZA DE DATOS ---
266
  df_final_completo = pd.DataFrame(resultados)
 
268
  df_mc = df_final_completo[df_final_completo['Portal'] == 'Metrocuadrado'].head(6)
269
  df_final = pd.concat([df_fr, df_mc]).reset_index(drop=True)
270
 
271
+ # --- CÁLCULOS TÉCNICOS ---
272
  df_final['V_Homogeneizado'] = df_final['Precio'] * 0.92
 
273
  mediana_m2 = df_final['Precio_M2'].median()
274
  minimo_zona = df_final['Precio'].min()
275
  maximo_zona = df_final['Precio'].max()
 
280
  valor_maximo_neg = (mediana_m2 * area) * 0.95
281
  valor_minimo_neg = (mediana_m2 * area) * 0.90
282
 
283
+ # --- CAP RATE ---
284
  cap_rate_txt = "Datos insuficientes en la zona para cruzar la rentabilidad."
285
  if len(precios_inversos) > 0:
286
  mediana_inversa = pd.Series(precios_inversos).median()
 
 
287
  try:
288
  if operacion == "Arriendo":
 
289
  rentabilidad = ((valor_total_sugerido * 12) / mediana_inversa) * 100
290
  cap_rate_txt = f"Valor Comercial de Venta Promedio en la zona: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual Estimada (Cap Rate): {rentabilidad:.2f}%"
291
+ else:
 
292
  rentabilidad = ((mediana_inversa * 12) / valor_total_sugerido) * 100
293
  cap_rate_txt = f"Canon de Arriendo Mensual Promedio en la zona: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual Estimada (Cap Rate): {rentabilidad:.2f}%"
294
  except: pass
295
 
296
+ # --- MAPA Y PDF ---
297
  mapa_html, lat_mapa, lon_mapa = generar_mapa(barrio, ciudad)
 
 
298
  preparar_entorno_pdf()
299
  pdf_path = f"Dictamen_Pericial_SAE_{int(time.time())}.pdf"
300
  pdf = PDF_SAE()
301
+ COLOR_ROSADO = (254, 25, 120); COLOR_GRIS = (137, 137, 137); COLOR_NEGRO = (0, 0, 0)
 
 
 
302
 
303
  # PÁGINA 1
304
  pdf.add_page()
 
306
  pdf.cell(0, 15, sanear_texto("DICTAMEN COMERCIAL Y FINANCIERO"), ln=True, align='C')
307
  pdf.set_font("Arial", 'B', 14); pdf.set_text_color(*COLOR_GRIS)
308
  pdf.cell(0, 8, sanear_texto(f"METODOLOGIA COMPARATIVA DE MERCADO"), ln=True, align='C')
309
+ pdf.line(20, 65, 190, 65); pdf.ln(15)
 
310
 
311
  pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_ROSADO)
312
  pdf.cell(0, 10, sanear_texto("1. DATOS GEOESPACIALES Y DEL ACTIVO"), ln=True)
 
324
  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}"))
325
  pdf.ln(5)
326
 
327
+ # PÁGINA 2
328
  pdf.add_page()
329
  pdf.set_font("Arial", 'B', 12); pdf.set_fill_color(*COLOR_ROSADO); pdf.set_text_color(255, 255, 255)
330
  pdf.cell(0, 10, sanear_texto(" 3. MATRIZ DE HOMOGENEIZACION Y RANGOS"), ln=True, fill=True)
 
338
  pdf.cell(50, 8, sanear_texto("-8.00%"), border=1, ln=True, align='C')
339
  pdf.ln(10)
340
 
 
341
  pdf.set_font("Arial", 'B', 14); pdf.set_text_color(*COLOR_ROSADO)
342
  pdf.cell(0, 10, sanear_texto("RANGOS DE NEGOCIACION AUTORIZADOS (CIERRE):"), ln=True)
343
  pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_GRIS)
 
347
  pdf.set_font("Arial", 'B', 12); pdf.set_text_color(200, 0, 0)
348
  pdf.cell(0, 8, sanear_texto(f"PISO MINIMO ACEPTABLE (-10%): ${valor_minimo_neg:,.0f} COP"), ln=True)
349
 
350
+ # PÁGINA 3+
351
  pdf.add_page()
352
  pdf.set_font("Arial", 'B', 12); pdf.set_fill_color(*COLOR_GRIS); pdf.set_text_color(255, 255, 255)
353
  pdf.cell(0, 10, sanear_texto(" 4. ANEXO TECNICO: TESTIGOS COMPARABLES"), ln=True, fill=True)
 
359
  if img_path and os.path.exists(img_path):
360
  try: pdf.image(img_path, x=10, y=y_start, w=45, h=30); text_x = 60
361
  except: pass
 
362
  pdf.set_xy(text_x, y_start); pdf.set_font("Arial", 'B', 11)
363
  pdf.cell(0, 6, f"Oferta: ${r['Precio']:,.0f} | Homogeneizado: ${r['V_Homogeneizado']:,.0f}", ln=True)
 
364
  pdf.set_x(text_x); pdf.set_font("Arial", 'B', 8); pdf.set_text_color(*COLOR_ROSADO)
365
  pdf.cell(0, 4, f"Ubicacion: {sanear_texto(r['Ubicacion'])}", ln=True)
366
  pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(*COLOR_NEGRO)
 
375
 
376
  pdf.output(pdf_path)
377
 
 
378
  resumen = (
379
  f"🏢 **DICTAMEN PERICIAL Y FINANCIERO**\n"
380
  f"📈 **Techo Máximo:** ${valor_maximo_neg:,.0f}\n"
 
391
  return f"{log_visible}\n✅ Dictamen Maestro Generado.", df_mostrar, pdf_path, resumen, mapa_html
392
 
393
  except Exception as error_fatal:
 
394
  return f"❌ ERROR GRAVE:\n{str(error_fatal)}", pd.DataFrame(), None, "⚠️ Falló la ejecución", "<p>Error en mapa</p>"
395
 
396
+ # --- FUNCIÓN DE OCULTAMIENTO INTELIGENTE UI ---
397
+ def adaptar_interfaz(tipo):
398
+ if tipo in ["Lote", "Finca"]:
399
+ return [gr.update(visible=False)] * 6
400
+ elif tipo in ["Bodega", "Local", "Oficina", "Consultorio", "Edificio"]:
401
+ 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)]
402
+ else:
403
+ return [gr.update(visible=True)] * 6
404
+
405
  # --- INTERFAZ GRÁFICA ---
406
  with gr.Blocks() as demo:
407
  gr.Markdown("## 🏢 TramitIA Pro: Dictamen Pericial Máster (IGAC/SAE)")
 
411
  op = gr.Radio(["Arriendo", "Venta"], label="Tipo de Operación", value="Arriendo")
412
  c = gr.Textbox(label="Ciudad", value="Barranquilla")
413
  b = gr.Textbox(label="Barrio (Ej: La Concepcion)", value="La Concepcion")
414
+
415
  with gr.Row():
416
+ t = gr.Dropdown(["Apartamento", "Casa", "Apartaestudio", "Bodega", "Local", "Oficina", "Consultorio", "Lote", "Finca", "Edificio"], label="Tipo de Inmueble", value="Apartamento")
417
  a = gr.Number(label="Área M2 a tasar", value=70)
418
+
419
  with gr.Row():
420
  m2_min = gr.Number(label="M2 Mínimo", value=10)
421
  m2_max = gr.Number(label="M2 Máximo", value=200)
422
+
423
+ # Se declaran las variables primero para poder esconderlas
424
  with gr.Row():
425
  ascensor = gr.Checkbox(label="Con Ascensor")
426
  piscina = gr.Checkbox(label="Con Piscina")
427
  with gr.Row():
428
+ h = gr.Number(label="Hab", value=3)
429
+ ban = gr.Number(label="Baños", value=2)
430
+ p = gr.Number(label="Park", value=1)
431
+
432
  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")
433
+
434
+ # Evento que oculta cosas al cambiar el tipo
435
+ t.change(adaptar_interfaz, inputs=t, outputs=[h, ban, p, e, ascensor, piscina])
436
+
437
  btn = gr.Button("GENERAR DICTAMEN INTEGRAL", variant="primary")
438
 
439
  with gr.Column(scale=2):
440
  res_fin = gr.Markdown("### 💰 Resultado y Rentabilidad (Cap Rate)...")
441
  with gr.Tabs():
442
+ with gr.TabItem("Mapa Geoespacial"): mapa_ui = gr.HTML("<p style='text-align:center; color:gray;'>El mapa aparecerá aquí...</p>")
 
443
  with gr.TabItem("Descargar Dictamen (PDF)"): out_pdf = gr.File()
444
  with gr.TabItem("Tabla de Homogeneización"): out_df = gr.Dataframe()
445
  with gr.TabItem("Log de Sistema"): msg = gr.Textbox(lines=10)