Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,7 +13,7 @@ try:
|
|
| 13 |
subprocess.run(["playwright", "install", "chromium"], check=True)
|
| 14 |
except: pass
|
| 15 |
|
| 16 |
-
# --- 1. GENERADOR DE URLS
|
| 17 |
def construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad):
|
| 18 |
# Mapeo Antigüedad
|
| 19 |
mapa_ant = {
|
|
@@ -24,11 +24,7 @@ def construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad):
|
|
| 24 |
"Más de 30 años": "mas-de-30-anos"
|
| 25 |
}
|
| 26 |
slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anios")
|
| 27 |
-
|
| 28 |
-
# Lógica singular/plural
|
| 29 |
slug_park = f"{int(park)}-parqueadero" if int(park) == 1 else f"{int(park)}-parqueaderos"
|
| 30 |
-
|
| 31 |
-
# Searchstring
|
| 32 |
query_geo = f"{zona.lower().replace(' ', '-')}-{ciudad.lower().replace(' ', '-')}"
|
| 33 |
|
| 34 |
# URL FINCA RAÍZ
|
|
@@ -40,55 +36,71 @@ def construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad):
|
|
| 40 |
|
| 41 |
return url_fr, url_mc
|
| 42 |
|
| 43 |
-
# --- 2. GENERADOR PDF CON
|
| 44 |
-
def
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
pdf = FPDF()
|
| 47 |
pdf.add_page()
|
| 48 |
|
| 49 |
-
#
|
| 50 |
-
pdf.set_font("Arial", 'B',
|
| 51 |
-
pdf.cell(0, 10, f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
pdf.set_font("Arial", 'B', 8)
|
| 55 |
pdf.set_text_color(100, 100, 100)
|
| 56 |
-
pdf.cell(0, 5, "
|
| 57 |
pdf.set_font("Courier", '', 7)
|
| 58 |
-
pdf.multi_cell(0, 4, f"FR: {url_fr}")
|
| 59 |
-
pdf.multi_cell(0, 4, f"MC: {url_mc}")
|
| 60 |
pdf.ln(5)
|
| 61 |
|
| 62 |
-
#
|
| 63 |
pdf.set_text_color(0, 0, 0)
|
| 64 |
-
pdf.set_font("Arial", '',
|
|
|
|
| 65 |
|
|
|
|
| 66 |
for _, r in df.iterrows():
|
| 67 |
-
pdf.set_fill_color(
|
| 68 |
-
|
| 69 |
-
pdf.set_font("Arial", 'B', 10)
|
| 70 |
-
pdf.cell(0, 8, f"{r['Portal']} - ${r['Precio']:,.0f}", ln=True, fill=True)
|
| 71 |
-
# Detalle
|
| 72 |
-
pdf.set_font("Arial", '', 9)
|
| 73 |
-
pdf.multi_cell(0, 5, f"Desc: {r['Descripcion']}")
|
| 74 |
pdf.set_font("Arial", 'U', 8)
|
| 75 |
pdf.set_text_color(0, 0, 255)
|
| 76 |
-
pdf.cell(0, 5, f"
|
| 77 |
pdf.set_text_color(0, 0, 0)
|
| 78 |
pdf.ln(2)
|
| 79 |
|
| 80 |
pdf.output(pdf_path)
|
| 81 |
-
return pdf_path
|
| 82 |
|
| 83 |
# --- 3. MOTOR DE EXTRACCIÓN ---
|
| 84 |
-
def
|
| 85 |
resultados = []
|
| 86 |
-
|
| 87 |
-
# 1. Generamos URLs
|
| 88 |
url_fr, url_mc = construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad)
|
| 89 |
-
|
| 90 |
-
# INICIAMOS EL LOG VISIBLE
|
| 91 |
-
log_visible = f"✅ URLs GENERADAS POR EL SISTEMA:\n\n🌍 FINCA RAÍZ:\n{url_fr}\n\n🌍 METROCUADRADO:\n{url_mc}\n\n--- PROCESANDO ---\n"
|
| 92 |
|
| 93 |
with sync_playwright() as p:
|
| 94 |
browser = p.chromium.launch(headless=True)
|
|
@@ -113,13 +125,11 @@ def motor_tramitia_debug(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
|
|
| 113 |
"Portal": "Finca Raiz",
|
| 114 |
"Precio": max(precios),
|
| 115 |
"Precio_M2": max(precios) / area,
|
| 116 |
-
"Descripcion": txt.split('\n')[0][:60],
|
| 117 |
"URL": href if href else url_fr
|
| 118 |
})
|
| 119 |
except: continue
|
| 120 |
page.close()
|
| 121 |
-
except Exception as e:
|
| 122 |
-
log_visible += f"⚠️ Error FR: {str(e)}\n"
|
| 123 |
|
| 124 |
# --- METROCUADRADO ---
|
| 125 |
try:
|
|
@@ -127,7 +137,6 @@ def motor_tramitia_debug(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
|
|
| 127 |
page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
|
| 128 |
for _ in range(3): page.mouse.wheel(0, 800); time.sleep(1)
|
| 129 |
|
| 130 |
-
# Selector Detail específico que pediste
|
| 131 |
details = page.query_selector_all("div.property-card__detail")
|
| 132 |
for d in details[:5]:
|
| 133 |
try:
|
|
@@ -135,7 +144,6 @@ def motor_tramitia_debug(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
|
|
| 135 |
precios = [int(s) for s in txt.replace('.', '').replace('$', '').split() if s.isdigit() and len(s) >= 6]
|
| 136 |
if not precios: continue
|
| 137 |
|
| 138 |
-
# Búsqueda de link hacia arriba
|
| 139 |
href = d.evaluate("""el => {
|
| 140 |
let card = el.closest('li') || el.closest('div[class*="card"]');
|
| 141 |
return card && card.querySelector('a') ? card.querySelector('a').href : '';
|
|
@@ -145,35 +153,40 @@ def motor_tramitia_debug(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
|
|
| 145 |
"Portal": "Metrocuadrado",
|
| 146 |
"Precio": max(precios),
|
| 147 |
"Precio_M2": max(precios) / area,
|
| 148 |
-
"Descripcion": txt.split('\n')[0][:60],
|
| 149 |
"URL": href if href else url_mc
|
| 150 |
})
|
| 151 |
except: continue
|
| 152 |
page.close()
|
| 153 |
-
except Exception as e:
|
| 154 |
-
log_visible += f"⚠️ Error MC: {str(e)}\n"
|
| 155 |
-
|
| 156 |
browser.close()
|
| 157 |
|
| 158 |
if not resultados:
|
| 159 |
-
return f"{log_visible}\n❌ NO
|
| 160 |
|
| 161 |
df = pd.DataFrame(resultados)
|
| 162 |
|
| 163 |
-
# Generar PDF
|
| 164 |
-
pdf_path =
|
| 165 |
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
# --- INTERFAZ ---
|
| 169 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 170 |
-
gr.Markdown("## 🤖 TramitIA Pro:
|
| 171 |
|
| 172 |
with gr.Row():
|
| 173 |
-
with gr.Column():
|
| 174 |
c = gr.Textbox(label="Ciudad", value="Bogota")
|
| 175 |
z = gr.Textbox(label="Zona (Ej: Fontibon)", value="Fontibon")
|
| 176 |
-
a = gr.Number(label="Área M2", value=60)
|
| 177 |
t = gr.Dropdown(["Apartamento", "Casa"], label="Tipo", value="Apartamento")
|
| 178 |
|
| 179 |
with gr.Row():
|
|
@@ -185,14 +198,20 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 185 |
["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"],
|
| 186 |
label="Antigüedad", value="1 a 8 años"
|
| 187 |
)
|
| 188 |
-
btn = gr.Button("
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
| 195 |
|
| 196 |
-
btn.click(
|
| 197 |
|
| 198 |
demo.launch()
|
|
|
|
| 13 |
subprocess.run(["playwright", "install", "chromium"], check=True)
|
| 14 |
except: pass
|
| 15 |
|
| 16 |
+
# --- 1. GENERADOR DE URLS ---
|
| 17 |
def construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad):
|
| 18 |
# Mapeo Antigüedad
|
| 19 |
mapa_ant = {
|
|
|
|
| 24 |
"Más de 30 años": "mas-de-30-anos"
|
| 25 |
}
|
| 26 |
slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anios")
|
|
|
|
|
|
|
| 27 |
slug_park = f"{int(park)}-parqueadero" if int(park) == 1 else f"{int(park)}-parqueaderos"
|
|
|
|
|
|
|
| 28 |
query_geo = f"{zona.lower().replace(' ', '-')}-{ciudad.lower().replace(' ', '-')}"
|
| 29 |
|
| 30 |
# URL FINCA RAÍZ
|
|
|
|
| 36 |
|
| 37 |
return url_fr, url_mc
|
| 38 |
|
| 39 |
+
# --- 2. GENERADOR PDF CON ESTIMACIÓN FINANCIERA ---
|
| 40 |
+
def generar_pdf_con_estimacion(zona, area, df, url_fr, url_mc):
|
| 41 |
+
# Cálculos Matemáticos
|
| 42 |
+
promedio_m2 = df['Precio_M2'].mean()
|
| 43 |
+
precio_sugerido = promedio_m2 * area
|
| 44 |
+
precio_min = df['Precio'].min()
|
| 45 |
+
precio_max = df['Precio'].max()
|
| 46 |
+
|
| 47 |
+
pdf_path = f"Estimacion_{int(time.time())}.pdf"
|
| 48 |
pdf = FPDF()
|
| 49 |
pdf.add_page()
|
| 50 |
|
| 51 |
+
# Encabezado
|
| 52 |
+
pdf.set_font("Arial", 'B', 16)
|
| 53 |
+
pdf.cell(0, 10, f"ESTIMACION DE CANON: {zona.upper()}", ln=True, align='C')
|
| 54 |
+
pdf.ln(5)
|
| 55 |
+
|
| 56 |
+
# --- BLOQUE DE VALORACIÓN (LO NUEVO) ---
|
| 57 |
+
pdf.set_fill_color(220, 230, 240)
|
| 58 |
+
pdf.rect(10, 30, 190, 40, 'F')
|
| 59 |
+
pdf.set_y(35)
|
| 60 |
|
| 61 |
+
pdf.set_font("Arial", 'B', 12)
|
| 62 |
+
pdf.cell(0, 8, f"CANON SUGERIDO (Area: {area}m2):", ln=True, align='C')
|
| 63 |
+
|
| 64 |
+
pdf.set_font("Arial", 'B', 20)
|
| 65 |
+
pdf.set_text_color(0, 100, 0)
|
| 66 |
+
pdf.cell(0, 10, f"${precio_sugerido:,.0f}", ln=True, align='C')
|
| 67 |
+
|
| 68 |
+
pdf.set_font("Arial", '', 10)
|
| 69 |
+
pdf.set_text_color(0, 0, 0)
|
| 70 |
+
pdf.cell(0, 8, f"Rango de Mercado: ${precio_min:,.0f} - ${precio_max:,.0f}", ln=True, align='C')
|
| 71 |
+
pdf.ln(15)
|
| 72 |
+
|
| 73 |
+
# Fuentes
|
| 74 |
pdf.set_font("Arial", 'B', 8)
|
| 75 |
pdf.set_text_color(100, 100, 100)
|
| 76 |
+
pdf.cell(0, 5, "URLS DE BÚSQUEDA (AUDITORÍA):", ln=True)
|
| 77 |
pdf.set_font("Courier", '', 7)
|
| 78 |
+
pdf.multi_cell(0, 4, f"FR: {url_fr}\nMC: {url_mc}")
|
|
|
|
| 79 |
pdf.ln(5)
|
| 80 |
|
| 81 |
+
# Detalle de Comparables
|
| 82 |
pdf.set_text_color(0, 0, 0)
|
| 83 |
+
pdf.set_font("Arial", 'B', 12)
|
| 84 |
+
pdf.cell(0, 10, "COMPARABLES UTILIZADOS:", ln=True)
|
| 85 |
|
| 86 |
+
pdf.set_font("Arial", '', 10)
|
| 87 |
for _, r in df.iterrows():
|
| 88 |
+
pdf.set_fill_color(245, 245, 245)
|
| 89 |
+
pdf.cell(0, 8, f"{r['Portal']} - ${r['Precio']:,.0f} (M2: ${r['Precio_M2']:,.0f})", ln=True, fill=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
pdf.set_font("Arial", 'U', 8)
|
| 91 |
pdf.set_text_color(0, 0, 255)
|
| 92 |
+
pdf.cell(0, 5, f"{r['URL']}", ln=True)
|
| 93 |
pdf.set_text_color(0, 0, 0)
|
| 94 |
pdf.ln(2)
|
| 95 |
|
| 96 |
pdf.output(pdf_path)
|
| 97 |
+
return pdf_path, precio_sugerido, precio_min, precio_max
|
| 98 |
|
| 99 |
# --- 3. MOTOR DE EXTRACCIÓN ---
|
| 100 |
+
def motor_tramitia_estimador(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
|
| 101 |
resultados = []
|
|
|
|
|
|
|
| 102 |
url_fr, url_mc = construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad)
|
| 103 |
+
log_visible = f"✅ URLs AUDITABLES:\nFR: {url_fr}\nMC: {url_mc}\n\n--- INICIANDO ESCANEO ---\n"
|
|
|
|
|
|
|
| 104 |
|
| 105 |
with sync_playwright() as p:
|
| 106 |
browser = p.chromium.launch(headless=True)
|
|
|
|
| 125 |
"Portal": "Finca Raiz",
|
| 126 |
"Precio": max(precios),
|
| 127 |
"Precio_M2": max(precios) / area,
|
|
|
|
| 128 |
"URL": href if href else url_fr
|
| 129 |
})
|
| 130 |
except: continue
|
| 131 |
page.close()
|
| 132 |
+
except Exception as e: log_visible += f"⚠️ Error FR: {e}\n"
|
|
|
|
| 133 |
|
| 134 |
# --- METROCUADRADO ---
|
| 135 |
try:
|
|
|
|
| 137 |
page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
|
| 138 |
for _ in range(3): page.mouse.wheel(0, 800); time.sleep(1)
|
| 139 |
|
|
|
|
| 140 |
details = page.query_selector_all("div.property-card__detail")
|
| 141 |
for d in details[:5]:
|
| 142 |
try:
|
|
|
|
| 144 |
precios = [int(s) for s in txt.replace('.', '').replace('$', '').split() if s.isdigit() and len(s) >= 6]
|
| 145 |
if not precios: continue
|
| 146 |
|
|
|
|
| 147 |
href = d.evaluate("""el => {
|
| 148 |
let card = el.closest('li') || el.closest('div[class*="card"]');
|
| 149 |
return card && card.querySelector('a') ? card.querySelector('a').href : '';
|
|
|
|
| 153 |
"Portal": "Metrocuadrado",
|
| 154 |
"Precio": max(precios),
|
| 155 |
"Precio_M2": max(precios) / area,
|
|
|
|
| 156 |
"URL": href if href else url_mc
|
| 157 |
})
|
| 158 |
except: continue
|
| 159 |
page.close()
|
| 160 |
+
except Exception as e: log_visible += f"⚠️ Error MC: {e}\n"
|
|
|
|
|
|
|
| 161 |
browser.close()
|
| 162 |
|
| 163 |
if not resultados:
|
| 164 |
+
return f"{log_visible}\n❌ NO HAY DATOS PARA CALCULAR.", None, None, "---"
|
| 165 |
|
| 166 |
df = pd.DataFrame(resultados)
|
| 167 |
|
| 168 |
+
# Generar PDF y Cálculos
|
| 169 |
+
pdf_path, sug, min_p, max_p = generar_pdf_con_estimacion(zona, area, df, url_fr, url_mc)
|
| 170 |
|
| 171 |
+
resumen_financiero = (
|
| 172 |
+
f"💰 **ESTIMACIÓN DE RENTA**\n"
|
| 173 |
+
f"🔹 **Canon Sugerido:** ${sug:,.0f}\n"
|
| 174 |
+
f"📉 Mínimo Zona: ${min_p:,.0f}\n"
|
| 175 |
+
f"📈 Máximo Zona: ${max_p:,.0f}\n"
|
| 176 |
+
f"📊 Basado en {len(df)} comparables."
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
return f"{log_visible}\n✅ Cálculo Completado.", df, pdf_path, resumen_financiero
|
| 180 |
|
| 181 |
# --- INTERFAZ ---
|
| 182 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 183 |
+
gr.Markdown("## 🤖 TramitIA Pro: Calculadora de Renta Justificada")
|
| 184 |
|
| 185 |
with gr.Row():
|
| 186 |
+
with gr.Column(scale=1):
|
| 187 |
c = gr.Textbox(label="Ciudad", value="Bogota")
|
| 188 |
z = gr.Textbox(label="Zona (Ej: Fontibon)", value="Fontibon")
|
| 189 |
+
a = gr.Number(label="Área M2 (Tu Cliente)", value=60)
|
| 190 |
t = gr.Dropdown(["Apartamento", "Casa"], label="Tipo", value="Apartamento")
|
| 191 |
|
| 192 |
with gr.Row():
|
|
|
|
| 198 |
["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"],
|
| 199 |
label="Antigüedad", value="1 a 8 años"
|
| 200 |
)
|
| 201 |
+
btn = gr.Button("CALCULAR CANON DE ARRENDAMIENTO", variant="primary")
|
| 202 |
+
|
| 203 |
+
with gr.Column(scale=2):
|
| 204 |
+
# RESULTADO DESTACADO
|
| 205 |
+
res_fin = gr.Markdown("### 💰 El resultado aparecerá aquí...")
|
| 206 |
|
| 207 |
+
with gr.Tabs():
|
| 208 |
+
with gr.TabItem("🔍 Log URLs"):
|
| 209 |
+
msg = gr.Textbox(lines=6, label="Auditoría de Enlaces")
|
| 210 |
+
with gr.TabItem("📋 Tabla de Comparables"):
|
| 211 |
+
out_df = gr.Dataframe()
|
| 212 |
+
with gr.TabItem("📄 Reporte PDF"):
|
| 213 |
+
out_pdf = gr.File()
|
| 214 |
|
| 215 |
+
btn.click(motor_tramitia_estimador, [z, c, a, t, h, b, p, e], [msg, out_df, out_pdf, res_fin])
|
| 216 |
|
| 217 |
demo.launch()
|