jcalbornoz commited on
Commit
12d3acf
·
verified ·
1 Parent(s): 0e0e4a1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -105
app.py CHANGED
@@ -1,11 +1,4 @@
1
  import sys
2
-
3
- # Silenciador de errores fantasma de consola
4
- def silenciador_errores_basura(unraisable):
5
- if unraisable.exc_type == ValueError and "Invalid file descriptor: -1" in str(unraisable.exc_value): pass
6
- else: sys.__unraisablehook__(unraisable)
7
- sys.unraisablehook = silenciador_errores_basura
8
-
9
  import os
10
  import subprocess
11
  import pandas as pd
@@ -21,7 +14,11 @@ from PIL import Image
21
  import io
22
  import traceback
23
 
24
- # --- INSTALACIÓN DE DEPENDENCIAS ---
 
 
 
 
25
  try: subprocess.run(["playwright", "install", "chromium"], check=True)
26
  except: pass
27
  try: from fake_useragent import UserAgent
@@ -29,9 +26,28 @@ except ImportError:
29
  subprocess.run(["pip", "install", "fake-useragent"], check=True)
30
  from fake_useragent import UserAgent
31
 
32
- # --- URLS DE LOGOS OFICIALES ---
33
- URL_LOGO_SAE = "https://cloudfront-us-east-1.images.arcpublishing.com/elespectador/JEZKDDS66NCH3M4OLNPGAMIKNU.jpg"
34
- URL_LOGO_ACTIVOS = "https://www.activosporcolombia.com/_next/image?url=%2FLogo_Horizontal.png&w=640&q=75"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  # --- FUNCIONES DE SOPORTE ---
37
  def sanear_texto(texto):
@@ -41,7 +57,7 @@ def sanear_texto(texto):
41
  def descargar_imagen(url, idx):
42
  if not url or len(url) < 5 or url.startswith("data:"): return None
43
  try:
44
- headers = {"User-Agent": "Mozilla/5.0", "Accept": "image/*"}
45
  r = requests.get(url, timeout=8, headers=headers)
46
  if r.status_code == 200:
47
  img = Image.open(io.BytesIO(r.content))
@@ -52,17 +68,6 @@ def descargar_imagen(url, idx):
52
  except: return None
53
  return None
54
 
55
- def descargar_logo(url, nombre_archivo):
56
- """Descarga un logo institucional para el PDF"""
57
- try:
58
- r = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
59
- if r.status_code == 200:
60
- with open(nombre_archivo, 'wb') as f:
61
- f.write(r.content)
62
- return nombre_archivo
63
- except: pass
64
- return None
65
-
66
  def construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina):
67
  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"}
68
  slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anos")
@@ -107,11 +112,35 @@ def es_inmueble_valido(href, portal):
107
  if "/inmueble/" in href or "-id-" in href: return True
108
  return False
109
 
110
- # --- MOTOR DE EXTRACCIÓN ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina):
112
- resultados = []
113
- log_visible = ""
114
- urls_vistas = set()
115
 
116
  try:
117
  url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
@@ -122,16 +151,12 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
122
  browser = p.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled', '--no-sandbox'])
123
  context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
124
 
125
- # FINCA RAÍZ
126
  try:
127
- page = context.new_page()
128
- page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
129
  try: page.wait_for_load_state("networkidle", timeout=10000)
130
  except: pass
131
- for _ in range(4):
132
- page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
133
- elementos = page.query_selector_all("a")
134
- cont_fr = 0
135
  for el in elementos:
136
  if cont_fr >= 12: break
137
  try:
@@ -154,16 +179,12 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
154
  page.close(); log_visible += f"✅ FR: {cont_fr} inmuebles.\n"
155
  except Exception as e: log_visible += f"⚠️ Error FR.\n"
156
 
157
- # METROCUADRADO
158
  try:
159
- page = context.new_page()
160
- page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
161
  try: page.wait_for_load_state("networkidle", timeout=10000)
162
  except: pass
163
- for _ in range(4):
164
- page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
165
- elementos = page.query_selector_all("a")
166
- cont_mc = 0
167
  for el in elementos:
168
  if cont_mc >= 12: break
169
  try:
@@ -185,7 +206,6 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
185
  except: continue
186
  page.close(); log_visible += f"✅ MC: {cont_mc} inmuebles.\n"
187
  except Exception as e: log_visible += f"⚠️ Error MC.\n"
188
-
189
  browser.close()
190
 
191
  if not resultados: return f"{log_visible}\n❌ NO HAY DATOS.", pd.DataFrame(), None, "---"
@@ -195,6 +215,8 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
195
  df_mc = df_final_completo[df_final_completo['Portal'] == 'Metrocuadrado'].head(6)
196
  df_final = pd.concat([df_fr, df_mc]).reset_index(drop=True)
197
 
 
 
198
  # CÁLCULOS TÉCNICOS
199
  margen_negociacion = 0.08
200
  mediana_m2 = df_final['Precio_M2'].median()
@@ -202,78 +224,90 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
202
  valor_tecnico_m2 = mediana_m2 * (1 - margen_negociacion)
203
  valor_total_sugerido = valor_tecnico_m2 * area
204
 
205
- # --- GENERACIÓN DEL PDF INSTITUCIONAL ---
206
- # Definimos una clase personalizada para manejar la cabecera automática
207
- class PDF_Institucional(FPDF):
208
- def header(self):
209
- # Descargamos logos temporalmente si no existen
210
- logo_sae_path = "logo_sae_temp.jpg"
211
- logo_activos_path = "logo_activos_temp.jpg"
212
- if not os.path.exists(logo_sae_path): descargar_logo(URL_LOGO_SAE, logo_sae_path)
213
- if not os.path.exists(logo_activos_path): descargar_logo(URL_LOGO_ACTIVOS, logo_activos_path)
214
-
215
- # Insertamos logos en el header
216
- if os.path.exists(logo_sae_path):
217
- try: self.image(logo_sae_path, x=10, y=8, w=35) # Izquierda
218
- except: pass
219
-
220
- if os.path.exists(logo_activos_path):
221
- try: self.image(logo_activos_path, x=160, y=8, w=40) # Derecha
222
- except: pass
223
-
224
- self.ln(20) # Espacio para que el header no pise el contenido
225
-
226
- pdf_path = f"Estudio_Mercado_Institucional_{int(time.time())}.pdf"
227
- pdf = PDF_Institucional() # Usamos la nueva clase
228
 
229
  # PÁGINA 1: PORTADA
230
  pdf.add_page()
231
- pdf.set_font("Arial", 'B', 18)
232
- pdf.set_text_color(20, 40, 90)
233
- pdf.cell(0, 20, sanear_texto("ESTUDIO DE MERCADO INMOBILIARIO"), ln=True, align='C')
234
- pdf.set_font("Arial", 'B', 14)
235
- pdf.cell(0, 10, sanear_texto(f"ESTIMACION DE {operacion.upper()}"), ln=True, align='C')
236
- pdf.line(20, 65, 190, 65)
 
237
  pdf.ln(15)
238
 
239
- pdf.set_font("Arial", 'B', 12); pdf.set_text_color(0, 0, 0)
 
240
  pdf.cell(0, 10, sanear_texto("1. DATOS DEL INMUEBLE SUJETO"), ln=True)
241
- pdf.set_font("Arial", '', 11)
 
 
242
  pdf.cell(0, 6, sanear_texto(f"- Tipo de Inmueble: {tipo.capitalize()}"), ln=True)
243
  pdf.cell(0, 6, sanear_texto(f"- Ubicacion: Barrio {barrio.title()}, {ciudad.title()}"), ln=True)
244
- pdf.cell(0, 6, sanear_texto(f"- Area Privada/Construida: {area} m2"), ln=True)
245
  pdf.cell(0, 6, sanear_texto(f"- Caracteristicas: {hab} Hab, {ban} Banos, {park} Parqueaderos"), ln=True)
246
  pdf.ln(10)
247
 
248
- pdf.set_font("Arial", 'B', 12); pdf.cell(0, 10, sanear_texto("2. METODOLOGIA (Metodo Comparativo)"), ln=True)
249
- pdf.set_font("Arial", '', 11); pdf.multi_cell(0, 5, sanear_texto("Utiliza el Metodo Comparativo de Mercado, deduciendo el valor mediante la comparacion sistematica de ofertas recientes de inmuebles similares en la zona."))
250
- pdf.ln(5); pdf.multi_cell(0, 5, sanear_texto("Se aplico un factor de comercializacion (Margen de Negociacion) del 8% sobre precios de oferta para estimar el valor real de cierre contractual."))
 
 
 
251
 
252
  # PÁGINA 2: CONCLUSIONES
253
  pdf.add_page()
254
- pdf.set_font("Arial", 'B', 14); pdf.set_fill_color(230, 230, 230)
255
- pdf.cell(0, 12, sanear_texto("3. RESULTADOS ECONÓMICOS"), ln=True, fill=True)
256
- pdf.ln(5); pdf.set_font("Arial", '', 11)
257
- pdf.cell(80, 8, sanear_texto("Total Inmuebles Analizados:"), border=1)
 
 
 
 
 
 
258
  pdf.cell(50, 8, sanear_texto(f"{len(df_final)}"), border=1, ln=True, align='C')
259
- pdf.cell(80, 8, sanear_texto("Valor Mediana Zona (M2):"), border=1)
260
  pdf.cell(50, 8, sanear_texto(f"${mediana_m2:,.0f}"), border=1, ln=True, align='C')
261
- pdf.cell(80, 8, sanear_texto("Margen Negociacion Aplicado:"), border=1)
262
  pdf.cell(50, 8, sanear_texto("8.00%"), border=1, ln=True, align='C')
263
  pdf.ln(10)
264
 
265
- pdf.set_font("Arial", 'B', 14); pdf.set_text_color(0, 100, 0)
266
- pdf.cell(0, 10, sanear_texto(f"VALOR ESTIMADO DE {operacion.upper()} (M2): ${valor_tecnico_m2:,.0f}"), ln=True)
267
- pdf.set_font("Arial", 'B', 16); pdf.cell(0, 12, sanear_texto(f"VALOR TOTAL SUGERIDO: ${valor_total_sugerido:,.0f}"), ln=True)
 
 
268
 
269
- pdf.set_text_color(0, 0, 0); pdf.ln(15); pdf.set_font("Arial", '', 9)
270
- pdf.multi_cell(0, 4, sanear_texto("Nota: Este es un Estudio de Mercado Institucional generado analiticamente como anexo tecnico referencial."))
271
 
272
  # PÁGINA 3+: ANEXOS
273
  pdf.add_page()
274
- pdf.set_font("Arial", 'B', 14); pdf.set_fill_color(40, 53, 147); pdf.set_text_color(255, 255, 255)
275
- pdf.cell(0, 12, sanear_texto("4. ANEXO: TESTIGOS DE MERCADO"), ln=True, fill=True)
276
- pdf.set_text_color(0, 0, 0); pdf.ln(5)
 
 
277
 
278
  for idx, r in df_final.iterrows():
279
  if pdf.get_y() > 240: pdf.add_page()
@@ -281,15 +315,15 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
281
  if img_path and os.path.exists(img_path):
282
  try: pdf.image(img_path, x=10, y=y_start, w=45, h=30); text_x = 60
283
  except: pass
284
- pdf.set_xy(text_x, y_start); pdf.set_font("Arial", 'B', 11)
285
  pdf.cell(0, 6, f"${r['Precio']:,.0f} COP", ln=True)
286
- pdf.set_x(text_x); pdf.set_font("Arial", 'B', 8); pdf.set_text_color(100, 100, 100)
287
  pdf.cell(0, 4, f"Ubicacion: {sanear_texto(r['Ubicacion'])} | Fuente: {sanear_texto(r['Portal'])}", ln=True)
288
- pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(0, 0, 0)
289
  pdf.multi_cell(0, 4, sanear_texto(r['Descripcion']))
290
- pdf.set_x(text_x); pdf.set_font("Arial", 'U', 8); pdf.set_text_color(0, 102, 204)
291
- pdf.cell(0, 4, ">> Ver publicacion fuente", link=r['URL'], ln=True)
292
- pdf.set_text_color(0, 0, 0); y_end = pdf.get_y(); pdf.set_y(max(y_start + 35, y_end + 5))
293
  pdf.set_draw_color(200, 200, 200); pdf.line(10, pdf.get_y(), 200, pdf.get_y()); pdf.ln(4)
294
  if img_path and os.path.exists(img_path):
295
  try: os.remove(img_path)
@@ -297,10 +331,6 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
297
 
298
  pdf.output(pdf_path)
299
 
300
- # --- LIMPIEZA DE LOGOS TEMPORALES ---
301
- if os.path.exists("logo_sae_temp.jpg"): os.remove("logo_sae_temp.jpg")
302
- if os.path.exists("logo_activos_temp.jpg"): os.remove("logo_activos_temp.jpg")
303
-
304
  # --- CÁLCULOS INTERFAZ ---
305
  resumen = (
306
  f"🏢 **ESTUDIO DE MERCADO INSTITUCIONAL**\n"
@@ -321,7 +351,7 @@ def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo,
321
 
322
  # --- INTERFAZ GRÁFICA ---
323
  with gr.Blocks() as demo:
324
- gr.Markdown("## 🏢 TramitIA Pro: Estudio de Mercado Institucional (SAE & Activos)")
325
 
326
  with gr.Row():
327
  with gr.Column(scale=1):
@@ -340,14 +370,14 @@ with gr.Blocks() as demo:
340
  with gr.Row():
341
  h = gr.Number(label="Habitaciones", value=3); ban = gr.Number(label="Baños", value=2); p = gr.Number(label="Parqueaderos", value=1)
342
  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")
343
- btn = gr.Button("GENERAR ESTUDIO DE MERCADO OFICIAL", variant="primary")
344
 
345
  with gr.Column(scale=2):
346
  res_fin = gr.Markdown("### 💰 Resumen Financiero...")
347
  with gr.Tabs():
348
- with gr.TabItem("Descargar Estudio Técnico (PDF)"): out_pdf = gr.File()
349
  with gr.TabItem("Matriz de Datos"): out_df = gr.Dataframe()
350
- with gr.TabItem("Trazabilidad (Logs)"): msg = gr.Textbox(lines=10)
351
 
352
  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])
353
 
 
1
  import sys
 
 
 
 
 
 
 
2
  import os
3
  import subprocess
4
  import pandas as pd
 
14
  import io
15
  import traceback
16
 
17
+ def silenciador_errores_basura(unraisable):
18
+ if unraisable.exc_type == ValueError and "Invalid file descriptor: -1" in str(unraisable.exc_value): pass
19
+ else: sys.__unraisablehook__(unraisable)
20
+ sys.unraisablehook = silenciador_errores_basura
21
+
22
  try: subprocess.run(["playwright", "install", "chromium"], check=True)
23
  except: pass
24
  try: from fake_useragent import UserAgent
 
26
  subprocess.run(["pip", "install", "fake-useragent"], check=True)
27
  from fake_useragent import UserAgent
28
 
29
+ # --- URLS LOGOS OFICIALES Y FUENTES (MANUAL SAE) ---
30
+ URL_LOGO_GOBIERNO = "https://wsp.presidencia.gov.co/dapre/Documentos/escudo-colombia-y-logo-presidencia.png"
31
+ URL_LOGO_SAE = "https://pbs.twimg.com/profile_images/1699971973678071808/Lz9bC7b5_400x400.jpg" # Logo cuadrado para redes (Rosado SAE)
32
+
33
+ # Fuentes Montserrat (Requeridas por el manual)
34
+ URL_FONT_REGULAR = "https://github.com/google/fonts/raw/main/ofl/montserrat/Montserrat-Regular.ttf"
35
+ URL_FONT_BOLD = "https://github.com/google/fonts/raw/main/ofl/montserrat/Montserrat-Bold.ttf"
36
+
37
+ def descargar_recurso(url, nombre_archivo):
38
+ try:
39
+ r = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
40
+ if r.status_code == 200:
41
+ with open(nombre_archivo, 'wb') as f: f.write(r.content)
42
+ return nombre_archivo
43
+ except: pass
44
+ return None
45
+
46
+ def preparar_entorno_pdf():
47
+ descargar_recurso(URL_LOGO_GOBIERNO, "logo_gob.png")
48
+ descargar_recurso(URL_LOGO_SAE, "logo_sae.jpg")
49
+ descargar_recurso(URL_FONT_REGULAR, "Montserrat-Regular.ttf")
50
+ descargar_recurso(URL_FONT_BOLD, "Montserrat-Bold.ttf")
51
 
52
  # --- FUNCIONES DE SOPORTE ---
53
  def sanear_texto(texto):
 
57
  def descargar_imagen(url, idx):
58
  if not url or len(url) < 5 or url.startswith("data:"): return None
59
  try:
60
+ headers = {"User-Agent": "Mozilla/5.0", "Accept": "image/*", "Referer": "https://www.fincaraiz.com.co/"}
61
  r = requests.get(url, timeout=8, headers=headers)
62
  if r.status_code == 200:
63
  img = Image.open(io.BytesIO(r.content))
 
68
  except: return None
69
  return None
70
 
 
 
 
 
 
 
 
 
 
 
 
71
  def construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina):
72
  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"}
73
  slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anos")
 
112
  if "/inmueble/" in href or "-id-" in href: return True
113
  return False
114
 
115
+ # --- CLASE PDF MANUAL SAE ---
116
+ class PDF_SAE(FPDF):
117
+ def header(self):
118
+ # 1. LOGOS (Gobierno Izquierda, SAE Derecha según Manual)
119
+ if os.path.exists("logo_gob.png"):
120
+ try: self.image("logo_gob.png", x=10, y=8, w=35)
121
+ except: pass
122
+ if os.path.exists("logo_sae.jpg"):
123
+ try: self.image("logo_sae.jpg", x=175, y=8, w=20)
124
+ except: pass
125
+ self.ln(22) # Espacio base (1.5cm a 2.5cm)
126
+
127
+ def footer(self):
128
+ self.set_y(-25)
129
+ # Franja pie de página institucional (Gris y texto formal)
130
+ try:
131
+ self.set_font('Montserrat', '', 7)
132
+ self.set_text_color(137, 137, 137) # Gris Institucional
133
+ texto_pie = "Dirección General: Carrera 7 # 32-42 Centro Comercial San Martín Local 107 / PBX: 7431444\nLinea Gratuita Nacional: 01 8000 111612 - atencionalciudadano@saesas.gov.co - www.saesas.gov.co"
134
+ self.multi_cell(0, 3, sanear_texto(texto_pie), align='C')
135
+ except:
136
+ pass
137
+ self.set_y(-15)
138
+ self.set_font('Arial', 'I', 8)
139
+ self.cell(0, 10, f'Pagina {self.page_no()}', 0, 0, 'C')
140
+
141
+ # --- MOTOR PRINCIPAL ---
142
  def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina):
143
+ resultados = []; log_visible = ""; urls_vistas = set()
 
 
144
 
145
  try:
146
  url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
 
151
  browser = p.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled', '--no-sandbox'])
152
  context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
153
 
 
154
  try:
155
+ page = context.new_page(); page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
 
156
  try: page.wait_for_load_state("networkidle", timeout=10000)
157
  except: pass
158
+ for _ in range(4): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
159
+ elementos = page.query_selector_all("a"); cont_fr = 0
 
 
160
  for el in elementos:
161
  if cont_fr >= 12: break
162
  try:
 
179
  page.close(); log_visible += f"✅ FR: {cont_fr} inmuebles.\n"
180
  except Exception as e: log_visible += f"⚠️ Error FR.\n"
181
 
 
182
  try:
183
+ page = context.new_page(); page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
 
184
  try: page.wait_for_load_state("networkidle", timeout=10000)
185
  except: pass
186
+ for _ in range(4): page.mouse.wheel(0, 1000); page.wait_for_timeout(2000)
187
+ elementos = page.query_selector_all("a"); cont_mc = 0
 
 
188
  for el in elementos:
189
  if cont_mc >= 12: break
190
  try:
 
206
  except: continue
207
  page.close(); log_visible += f"✅ MC: {cont_mc} inmuebles.\n"
208
  except Exception as e: log_visible += f"⚠️ Error MC.\n"
 
209
  browser.close()
210
 
211
  if not resultados: return f"{log_visible}\n❌ NO HAY DATOS.", pd.DataFrame(), None, "---"
 
215
  df_mc = df_final_completo[df_final_completo['Portal'] == 'Metrocuadrado'].head(6)
216
  df_final = pd.concat([df_fr, df_mc]).reset_index(drop=True)
217
 
218
+ if df_final.empty: return f"{log_visible}\n❌ DATOS VACÍOS.", pd.DataFrame(), None, "---"
219
+
220
  # CÁLCULOS TÉCNICOS
221
  margen_negociacion = 0.08
222
  mediana_m2 = df_final['Precio_M2'].median()
 
224
  valor_tecnico_m2 = mediana_m2 * (1 - margen_negociacion)
225
  valor_total_sugerido = valor_tecnico_m2 * area
226
 
227
+ # PREPARAR PDF Y FUENTES
228
+ preparar_entorno_pdf()
229
+ pdf_path = f"Estudio_Mercado_SAE_{int(time.time())}.pdf"
230
+ pdf = PDF_SAE()
231
+
232
+ # Instalar fuente Montserrat (Si falla, usa Arial por defecto)
233
+ fuente_ppal = 'Arial'
234
+ try:
235
+ if os.path.exists("Montserrat-Regular.ttf") and os.path.exists("Montserrat-Bold.ttf"):
236
+ pdf.add_font('Montserrat', '', 'Montserrat-Regular.ttf', uni=True)
237
+ pdf.add_font('Montserrat', 'B', 'Montserrat-Bold.ttf', uni=True)
238
+ fuente_ppal = 'Montserrat'
239
+ except: pass
240
+
241
+ # COLORES SAE (HEX: #FE1978 -> RGB: 254, 25, 120)
242
+ COLOR_ROSADO = (254, 25, 120)
243
+ COLOR_GRIS = (137, 137, 137)
244
+ COLOR_NEGRO = (0, 0, 0)
 
 
 
 
 
245
 
246
  # PÁGINA 1: PORTADA
247
  pdf.add_page()
248
+ pdf.set_font(fuente_ppal, 'B', 18)
249
+ pdf.set_text_color(*COLOR_ROSADO)
250
+ pdf.cell(0, 15, sanear_texto("ESTUDIO DE MERCADO INMOBILIARIO"), ln=True, align='C')
251
+ pdf.set_font(fuente_ppal, 'B', 14)
252
+ pdf.set_text_color(*COLOR_GRIS)
253
+ pdf.cell(0, 8, sanear_texto(f"METODOLOGIA COMPARATIVA DE MERCADO"), ln=True, align='C')
254
+ pdf.line(20, 60, 190, 60)
255
  pdf.ln(15)
256
 
257
+ pdf.set_font(fuente_ppal, 'B', 12)
258
+ pdf.set_text_color(*COLOR_ROSADO)
259
  pdf.cell(0, 10, sanear_texto("1. DATOS DEL INMUEBLE SUJETO"), ln=True)
260
+ pdf.set_font(fuente_ppal, '', 11)
261
+ pdf.set_text_color(*COLOR_NEGRO)
262
+ pdf.cell(0, 6, sanear_texto(f"- Operacion: {operacion.capitalize()}"), ln=True)
263
  pdf.cell(0, 6, sanear_texto(f"- Tipo de Inmueble: {tipo.capitalize()}"), ln=True)
264
  pdf.cell(0, 6, sanear_texto(f"- Ubicacion: Barrio {barrio.title()}, {ciudad.title()}"), ln=True)
265
+ pdf.cell(0, 6, sanear_texto(f"- Area: {area} m2"), ln=True)
266
  pdf.cell(0, 6, sanear_texto(f"- Caracteristicas: {hab} Hab, {ban} Banos, {park} Parqueaderos"), ln=True)
267
  pdf.ln(10)
268
 
269
+ pdf.set_font(fuente_ppal, 'B', 12)
270
+ pdf.set_text_color(*COLOR_ROSADO)
271
+ pdf.cell(0, 10, sanear_texto("2. METODOLOGIA"), ln=True)
272
+ pdf.set_font(fuente_ppal, '', 11)
273
+ pdf.set_text_color(*COLOR_NEGRO)
274
+ pdf.multi_cell(0, 5, sanear_texto("Estudio deducido mediante la comparacion sistematica de ofertas recientes de inmuebles similares en la zona. Se ha aplicado un factor de comercializacion (Margen de Negociacion) del 8% sobre los precios de oferta para estimar el valor real de cierre contractual."))
275
 
276
  # PÁGINA 2: CONCLUSIONES
277
  pdf.add_page()
278
+ pdf.set_font(fuente_ppal, 'B', 12)
279
+ pdf.set_fill_color(*COLOR_ROSADO)
280
+ pdf.set_text_color(255, 255, 255)
281
+ pdf.cell(0, 10, sanear_texto(" 3. RESULTADOS ESTADISTICOS"), ln=True, fill=True)
282
+ pdf.set_text_color(*COLOR_NEGRO)
283
+ pdf.ln(5); pdf.set_font(fuente_ppal, '', 11)
284
+
285
+ # Tabla de resultados
286
+ ancho_col = 80
287
+ pdf.cell(ancho_col, 8, sanear_texto("Total Testigos Analizados:"), border=1)
288
  pdf.cell(50, 8, sanear_texto(f"{len(df_final)}"), border=1, ln=True, align='C')
289
+ pdf.cell(ancho_col, 8, sanear_texto("Mediana Mercado (Oferta M2):"), border=1)
290
  pdf.cell(50, 8, sanear_texto(f"${mediana_m2:,.0f}"), border=1, ln=True, align='C')
291
+ pdf.cell(ancho_col, 8, sanear_texto("Margen Negociacion:"), border=1)
292
  pdf.cell(50, 8, sanear_texto("8.00%"), border=1, ln=True, align='C')
293
  pdf.ln(10)
294
 
295
+ pdf.set_font(fuente_ppal, 'B', 14)
296
+ pdf.set_text_color(0, 100, 0)
297
+ pdf.cell(0, 10, sanear_texto(f"VALOR {operacion.upper()} SUGERIDO (M2): ${valor_tecnico_m2:,.0f}"), ln=True)
298
+ pdf.set_font(fuente_ppal, 'B', 16)
299
+ pdf.cell(0, 12, sanear_texto(f"VALOR TOTAL DEL INMUEBLE: ${valor_total_sugerido:,.0f} COP"), ln=True)
300
 
301
+ pdf.set_text_color(*COLOR_GRIS); pdf.ln(15); pdf.set_font(fuente_ppal, '', 8)
302
+ pdf.multi_cell(0, 4, sanear_texto("Documento generado analiticamente como anexo tecnico referencial comercial."))
303
 
304
  # PÁGINA 3+: ANEXOS
305
  pdf.add_page()
306
+ pdf.set_font(fuente_ppal, 'B', 12)
307
+ pdf.set_fill_color(*COLOR_GRIS)
308
+ pdf.set_text_color(255, 255, 255)
309
+ pdf.cell(0, 10, sanear_texto(" 4. ANEXO TECNICO: TESTIGOS COMPARABLES"), ln=True, fill=True)
310
+ pdf.set_text_color(*COLOR_NEGRO); pdf.ln(5)
311
 
312
  for idx, r in df_final.iterrows():
313
  if pdf.get_y() > 240: pdf.add_page()
 
315
  if img_path and os.path.exists(img_path):
316
  try: pdf.image(img_path, x=10, y=y_start, w=45, h=30); text_x = 60
317
  except: pass
318
+ pdf.set_xy(text_x, y_start); pdf.set_font(fuente_ppal, 'B', 11)
319
  pdf.cell(0, 6, f"${r['Precio']:,.0f} COP", ln=True)
320
+ pdf.set_x(text_x); pdf.set_font(fuente_ppal, 'B', 8); pdf.set_text_color(*COLOR_ROSADO)
321
  pdf.cell(0, 4, f"Ubicacion: {sanear_texto(r['Ubicacion'])} | Fuente: {sanear_texto(r['Portal'])}", ln=True)
322
+ pdf.set_x(text_x); pdf.set_font(fuente_ppal, '', 8); pdf.set_text_color(*COLOR_NEGRO)
323
  pdf.multi_cell(0, 4, sanear_texto(r['Descripcion']))
324
+ pdf.set_x(text_x); pdf.set_font(fuente_ppal, '', 8); pdf.set_text_color(0, 102, 204)
325
+ pdf.cell(0, 4, ">> Ver publicacion original", link=r['URL'], ln=True)
326
+ pdf.set_text_color(*COLOR_NEGRO); y_end = pdf.get_y(); pdf.set_y(max(y_start + 35, y_end + 5))
327
  pdf.set_draw_color(200, 200, 200); pdf.line(10, pdf.get_y(), 200, pdf.get_y()); pdf.ln(4)
328
  if img_path and os.path.exists(img_path):
329
  try: os.remove(img_path)
 
331
 
332
  pdf.output(pdf_path)
333
 
 
 
 
 
334
  # --- CÁLCULOS INTERFAZ ---
335
  resumen = (
336
  f"🏢 **ESTUDIO DE MERCADO INSTITUCIONAL**\n"
 
351
 
352
  # --- INTERFAZ GRÁFICA ---
353
  with gr.Blocks() as demo:
354
+ gr.Markdown("## 🏢 TramitIA Pro: Estudio de Mercado Institucional (Cumplimiento SAE)")
355
 
356
  with gr.Row():
357
  with gr.Column(scale=1):
 
370
  with gr.Row():
371
  h = gr.Number(label="Habitaciones", value=3); ban = gr.Number(label="Baños", value=2); p = gr.Number(label="Parqueaderos", value=1)
372
  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")
373
+ btn = gr.Button("GENERAR REPORTE NORMATIVO", variant="primary")
374
 
375
  with gr.Column(scale=2):
376
  res_fin = gr.Markdown("### 💰 Resumen Financiero...")
377
  with gr.Tabs():
378
+ with gr.TabItem("Descargar Documento Oficial"): out_pdf = gr.File()
379
  with gr.TabItem("Matriz de Datos"): out_df = gr.Dataframe()
380
+ with gr.TabItem("Log de Sistema"): msg = gr.Textbox(lines=10)
381
 
382
  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])
383