from fpdf import FPDF import datetime import os import httpx import tempfile class CrowDataPDF(FPDF): def header(self): self.set_fill_color(30, 41, 59) self.rect(0, 0, 210, 32, 'F') logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logo.png") if os.path.exists(logo_path): self.image(logo_path, x=10, y=6, w=40) self.set_font('helvetica', 'B', 22) self.set_text_color(255, 255, 255) self.set_y(6) self.set_x(55) self.cell(0, 10, 'CrowData Intelligence', new_x="LMARGIN", new_y="NEXT", align='L') self.set_font('helvetica', '', 10) self.set_text_color(203, 213, 225) self.set_x(55) self.cell(0, 5, 'Dossier de Inteligencia Consolidado - Confidencial', new_x="LMARGIN", new_y="NEXT", align='L') self.set_fill_color(16, 185, 129) self.rect(0, 31, 210, 1, 'F') self.ln(12) def footer(self): self.set_y(-15) self.set_font('helvetica', 'I', 8) self.set_text_color(100, 116, 139) fecha_gen = datetime.datetime.now().strftime("%d/%m/%Y %H:%M") self.cell(0, 10, f'Pagina {self.page_no()} | Generado por CrowData el {fecha_gen}', align='C') def clean_txt(val): if val is None: return "" s = str(val) s = s.replace("\u2013", "-").replace("\u2014", "-").replace("\u201c", '"').replace("\u201d", '"') s = s.replace("\u2018", "'").replace("\u2019", "'").replace("\u2022", "*") try: return s.encode('latin-1', 'replace').decode('latin-1') except Exception: return s def safe_float(val, default=0.0): """ Convierte valor a float de forma segura, manejando None, strings y errores. Usado para prevenir ValueError en conversiones de montos. """ import logging logger = logging.getLogger(__name__) if val is None: return default if isinstance(val, (int, float)): return float(val) if isinstance(val, str): # Limpiar formato de moneda/puntos/comas clean_val = (val.replace('$', '') .replace(',', '') .replace('.', '') .replace('k', '000') .replace('K', '000') .strip()) try: return float(clean_val) if clean_val else default except ValueError: logger.warning(f"No se pudo convertir '{val}' a float, usando default {default}") return default logger.warning(f"Tipo inesperado para safe_float: {type(val)}, valor: {val}, usando default {default}") return default def safe_int(val, default=0): """Convierte valor a int de forma segura""" try: return int(safe_float(val, default)) except (ValueError, TypeError): return default def _section_header(pdf, title): pdf.ln(4) pdf.set_font('helvetica', 'B', 12) pdf.set_text_color(30, 41, 59) pdf.cell(0, 8, clean_txt(title), new_x="LMARGIN", new_y="NEXT") pdf.set_fill_color(226, 232, 240) pdf.rect(10, pdf.get_y(), 190, 0.3, 'F') pdf.ln(3) def _kv_line(pdf, label, value, label_w=50, font_size=9.5): pdf.set_font('helvetica', 'B', font_size) pdf.set_text_color(71, 85, 105) pdf.cell(label_w, 6, clean_txt(label), new_x="RIGHT", new_y="TOP") pdf.set_font('helvetica', '', font_size) pdf.set_text_color(15, 23, 42) pdf.cell(0, 6, clean_txt(str(value) if value else "-"), new_x="LMARGIN", new_y="NEXT") def _item_bullet(pdf, text, font_size=9.5): pdf.set_x(10) pdf.set_font('helvetica', '', font_size) pdf.set_text_color(15, 23, 42) pdf.multi_cell(190, 5.5, clean_txt(text)) def _empty_notice(pdf, text): pdf.set_font('helvetica', '', 9.5) pdf.set_text_color(100, 116, 139) pdf.cell(0, 6, clean_txt(text), new_x="LMARGIN", new_y="NEXT") def _add_photo_to_pdf(pdf, foto_url: str, x: float = 150, y: float = 35, max_w: float = 45, max_h: float = 45): """Descarga y agrega foto de perfil al PDF. Silencioso en caso de error. Timeout reducido a 3s.""" if not foto_url: return try: with httpx.Client(timeout=3, follow_redirects=True) as client: # Reducido de 10s a 3s resp = client.get(foto_url, headers={"User-Agent": "Mozilla/5.0"}) if resp.status_code != 200: return content_type = resp.headers.get("content-type", "") if "image" not in content_type and not foto_url.endswith((".jpg", ".jpeg", ".png", ".webp")): return with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp: tmp.write(resp.content) tmp_path = tmp.name try: pdf.image(tmp_path, x=x, y=y, w=max_w, h=max_h) finally: os.unlink(tmp_path) except Exception: pass pdf.ln(2) def _get_nested(obj, *keys, default=None): for k in keys: if obj is None: return default if isinstance(obj, dict): obj = obj.get(k) else: return default return obj if obj is not None else default def generate_report_pdf(data: dict, report_type: str = "persona") -> bytes: pdf = CrowDataPDF() pdf.set_auto_page_break(auto=True, margin=20) pdf.add_page() ident = data.get("identificacion", {}) rc = data.get("registro_civil", {}) fiscal = data.get("fiscal", {}) fin = data.get("financiero", {}) judicial = data.get("judicial", {}) patrimonial = data.get("patrimonial", {}) societario = data.get("societario", {}) prev = data.get("previsional", {}) salud = data.get("salud", {}) contacto = data.get("contacto", {}) meta = data.get("meta", {}) # ═══════════════════════════════════════════════════════════ # SECTION 1: TITLE & IDENTIFICACION # ═══════════════════════════════════════════════════════════ if report_type == "persona": nombre_informe = f"{ident.get('apellido', '')} {ident.get('nombres', '')}".strip() elif report_type == "vehiculo": veh = data.get("vehiculo", {}) nombre_informe = f"Vehículo {veh.get('dominio', '')} - {veh.get('marca', '')} {veh.get('modelo', '')} ({veh.get('anio', '')})".strip() elif report_type == "propiedad": nombre_informe = data.get("direccion", "Propiedad") elif report_type == "grupo": target = data.get("target", {}) target_ident = target.get("identificacion", {}) if target else {} nombre_informe = f"Grupo Económico: {target_ident.get('apellido', '')} {target_ident.get('nombres', '')}".strip() else: nombre_informe = ident.get('razon_social', '') or ident.get('cuit', 'Empresa') pdf.set_font('helvetica', 'B', 16) pdf.set_text_color(30, 41, 59) pdf.cell(0, 10, clean_txt(f"INFORME: {nombre_informe}"), new_x="LMARGIN", new_y="NEXT") pdf.set_fill_color(226, 232, 240) pdf.rect(10, pdf.get_y(), 190, 0.5, 'F') pdf.ln(5) if report_type == "persona": _kv_line(pdf, "CUIT/CUIL:", ident.get('cuil')) _kv_line(pdf, "DNI:", ident.get('dni')) _kv_line(pdf, "Sexo:", ident.get('sexo')) _kv_line(pdf, "Nacionalidad:", ident.get('nacionalidad')) fecha_nac = ident.get('fecha_nacimiento') if fecha_nac: try: parts = str(fecha_nac).split('-') if len(parts) == 3: fn = datetime.date(int(parts[0]), int(parts[1]), int(parts[2])) today = datetime.date.today() edad = today.year - fn.year - ((today.month, today.day) < (fn.month, fn.day)) fecha_fmt = fn.strftime('%d/%m/%Y') _kv_line(pdf, "Fecha Nacimiento:", f"{fecha_fmt} ({edad} anios)") else: _kv_line(pdf, "Fecha Nacimiento:", str(fecha_nac)) except Exception: _kv_line(pdf, "Fecha Nacimiento:", str(fecha_nac)) fallecido = rc.get("fallecido", False) or bool(ident.get('fecha_defuncion')) if fallecido: fecha_def = rc.get("fecha_defuncion") or ident.get('fecha_defuncion') or "Fallecido" pdf.set_font('helvetica', 'B', 10) pdf.set_text_color(71, 85, 105) pdf.cell(50, 7, "Estado Vital:", new_x="RIGHT", new_y="TOP") pdf.set_font('helvetica', '', 10) pdf.set_text_color(185, 28, 28) pdf.cell(0, 7, clean_txt(f"FALLECIDO ({fecha_def})"), new_x="LMARGIN", new_y="NEXT") lugar_def = rc.get("lugar_defuncion") if lugar_def: _kv_line(pdf, "Lugar Defuncion:", str(lugar_def)) else: _kv_line(pdf, "Estado Vital:", "Activo / Vivo") _kv_line(pdf, "Validacion RENAPER:", "Verificado" if ident.get('validado_renaper') else "No verificado") foto_url = ident.get('foto_perfil') if foto_url: _add_photo_to_pdf(pdf, foto_url, x=155, y=40, max_w=40, max_h=40) pdf.set_font('helvetica', 'I', 7) pdf.set_text_color(100, 116, 139) pdf.set_xy(155, 82) pdf.cell(40, 4, clean_txt(f"Foto: {ident.get('foto_perfil_fuente', 'N/A')}"), align='C') pdf.set_xy(10, pdf.get_y() + 5) elif report_type == "vehiculo": veh = data.get("vehiculo", {}) _kv_line(pdf, "Dominio:", veh.get('dominio')) _kv_line(pdf, "Marca / Modelo:", f"{veh.get('marca', '')} {veh.get('modelo', '')}") _kv_line(pdf, "Año:", str(veh.get('anio', ''))) _kv_line(pdf, "Tipo:", veh.get('tipo')) radicacion_parts = [veh.get('registro'), veh.get('localidad'), veh.get('provincia')] _kv_line(pdf, "Radicacion:", " — ".join(p for p in radicacion_parts if p)) _kv_line(pdf, "VTV:", veh.get('validez_vtv') or 'Sin datos') _kv_line(pdf, "Seguro:", 'SI' if veh.get('tiene_seguro') else 'NO') _kv_line(pdf, "Estado:", veh.get('estado') or 'Sin datos') # Titular titular = data.get("titular") or {} if titular: pdf.ln(2) pdf.set_font('helvetica', 'B', 10) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "TITULAR REGISTRADO:", new_x="LMARGIN", new_y="NEXT") nombre_titular = f"{titular.get('apellido', '')} {titular.get('nombres', '')}".strip() or 'Protegido' _kv_line(pdf, "Nombre:", nombre_titular) _kv_line(pdf, "CUIL:", titular.get('cuil')) # Prendas prendas = data.get("prendas", []) if prendas: _section_header(pdf, "GRAVAMENES Y PRENDAS") for pr in prendas: _item_bullet(pdf, f"Entidad: {pr.get('entidad', '-')} | Fecha: {pr.get('fecha', '-')} | {pr.get('descripcion', 'Prenda registrada')}") else: _section_header(pdf, "GRAVAMENES Y PRENDAS") _empty_notice(pdf, "Sin prendas o gravamenes registrados sobre el vehiculo.") # Denuncias de robo denuncias = data.get("denuncias_robo", []) if denuncias: _section_header(pdf, "DENUNCIAS DE ROBO") for den in denuncias: _item_bullet(pdf, f"Fecha: {den.get('fecha', '-')} | {den.get('descripcion', 'Denuncia registrada')}") else: _section_header(pdf, "DENUNCIAS DE ROBO") _empty_notice(pdf, "Sin denuncias de robo registradas.") # Infracciones del vehiculo inf_veh = data.get("infracciones", []) if inf_veh: _section_header(pdf, "INFRACCIONES DE TRANSITO") for inf in inf_veh: monto_inf = inf.get('monto') or 0 _item_bullet(pdf, f"Acta: {inf.get('acta', '-')} | Fecha: {inf.get('fecha', '-')} | Motivo: {inf.get('motivo', '-')} | Monto: ${safe_float(monto_inf):,.0f} | {inf.get('jurisdiccion', '')}") else: _section_header(pdf, "INFRACCIONES DE TRANSITO") _empty_notice(pdf, "Sin infracciones de transito registradas.") # Indicadores de riesgo riesgos = data.get("riesgo", []) _section_header(pdf, "INDICADORES DE RIESGO") if riesgos: for r in riesgos: _item_bullet(pdf, f"[{r.get('nivel', 'INFO').upper()}] {r.get('descripcion', '')} - {r.get('hallazgo', '')}") else: _empty_notice(pdf, "Sin indicadores de riesgo detectados.") elif report_type == "propiedad": _kv_line(pdf, "Direccion:", data.get('direccion')) cat = data.get("datos_catastrales") or {} if cat: _kv_line(pdf, "Partida:", cat.get('partida')) _kv_line(pdf, "Nomenclatura:", cat.get('nomenclatura')) _kv_line(pdf, "Partido:", cat.get('partido_nombre')) _kv_line(pdf, "Tipo Inmueble:", cat.get('tipo_inmueble')) sup = cat.get('superficie_m2') if sup: _kv_line(pdf, "Superficie:", f"{sup:,.0f} m2") vf = cat.get('valuacion_fiscal') if vf: _kv_line(pdf, "Valuacion Fiscal (ARBA):", f"${safe_float(vf):,.0f}") bi = cat.get('base_imponible') if bi: _kv_line(pdf, "Base Imponible:", f"${safe_float(bi):,.0f}") ve = data.get("valor_estimado") if ve: _kv_line(pdf, "Valor Mercado Estimado:", f"${safe_float(ve):,.0f}") geo = data.get("geolocalizacion") or {} if geo.get('lat') and geo.get('lng'): _kv_line(pdf, "Geolocalizacion:", f"Lat: {geo.get('lat')} | Lng: {geo.get('lng')}") # Titulares titulares = data.get("titulares_detectados", []) if titulares: _section_header(pdf, "PROPIETARIOS / TITULARES DETECTADOS") for t in titulares: nombre_t = f"{t.get('apellido', '')} {t.get('nombres', '')}".strip() or 'Titular detectado' cuil_t = t.get('cuil', '') cuil_str = f" | CUIL: {cuil_t}" if cuil_t else '' cruzado = ' [Posible Propietario]' if t.get('cruzado_con_busqueda') else '' _item_bullet(pdf, f"{nombre_t}{cuil_str}{cruzado}") # Boletín bo_prop = data.get("historial_boletin", []) if bo_prop: _section_header(pdf, "PUBLICACIONES EN BOLETIN OFICIAL") for pub in bo_prop[:10]: texto = pub.get('snippet') or pub.get('texto') or pub.get('titulo') or '' if len(texto) > 200: texto = texto[:200] + "..." pdf.set_font('helvetica', 'B', 9) pdf.set_text_color(15, 23, 42) pdf.cell(0, 5, clean_txt(f"{pub.get('fecha', 'S/F')} - {pub.get('tipo', pub.get('seccion', 'BO'))}"), new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 8.5) pdf.multi_cell(190, 4, clean_txt(f" {texto}")) pdf.ln(2) # Deudas ARBA deudas = data.get("deudas_impositivas") or {} if deudas.get('con_deuda'): _section_header(pdf, "DEUDAS IMPOSITIVAS (ARBA)") monto_d = deudas.get('monto_total', 0) _kv_line(pdf, "Monto Total Adeudado:", f"${safe_float(monto_d):,.0f}") for per in (deudas.get('periodos_adeudados') or [])[:10]: if isinstance(per, dict): _item_bullet(pdf, f"Periodo: {per.get('periodo', '')} | Monto: ${safe_float(per.get('monto', 0)):,.0f}") # Riesgos riesgos_prop = data.get("riesgo", []) _section_header(pdf, "INDICADORES DE RIESGO") if riesgos_prop: for r in riesgos_prop: _item_bullet(pdf, f"[{r.get('nivel', 'INFO').upper()}] {r.get('descripcion', '')} - {r.get('hallazgo', '')}") else: _empty_notice(pdf, "Sin indicadores de riesgo detectados para este inmueble.") else: _kv_line(pdf, "CUIT Empresa:", ident.get('cuit')) _kv_line(pdf, "Tipo Societario:", ident.get('tipo_societario')) _kv_line(pdf, "Fecha Constitucion:", ident.get('fecha_constitucion')) _kv_line(pdf, "Nombre Fantasia:", ident.get('nombre_fantasia')) igj = data.get("igj", {}) if igj and igj.get("numero_inscripcion"): _kv_line(pdf, "Nro Inscripcion IGJ:", igj.get("numero_inscripcion")) # ═══════════════════════════════════════════════════════════ # GRUPO: RESUMEN DEL GRUPO ECONOMICO # ═══════════════════════════════════════════════════════════ if report_type == "grupo": empresas = data.get("empresas_vinculadas", []) vehiculos = data.get("vehiculos_vinculados", []) riesgos_con = data.get("riesgo_consolidado", []) patrimonio = data.get("total_patrimonio_estimado") _section_header(pdf, "RESUMEN DEL GRUPO ECONOMICO") _kv_line(pdf, "Empresas integradas:", str(len(empresas))) _kv_line(pdf, "Vehiculos registrados:", str(len(vehiculos))) if patrimonio: _kv_line(pdf, "Patrimonio estimado:", f"${safe_float(patrimonio):,.0f}") if empresas: _section_header(pdf, "SOCIEDADES / EMPRESAS VINCULADAS") for emp in empresas: emp_id = emp.get("identificacion", {}) if isinstance(emp, dict) else {} razon = emp_id.get("razon_social", "") if isinstance(emp_id, dict) else "" cuit_e = emp_id.get("cuit", "") if isinstance(emp_id, dict) else "" tipo = emp_id.get("tipo_societario", "") if isinstance(emp_id, dict) else "" score_e = emp.get("score", {}) if isinstance(emp, dict) else {} nivel_e = score_e.get("nivel", "") if isinstance(score_e, dict) else "" _item_bullet(pdf, f"{razon} ({cuit_e}) - {tipo} - Score: {nivel_e}") if vehiculos: _section_header(pdf, "VEHICULOS DEL GRUPO") for vh in vehiculos: vh_data = vh.get("vehiculo", {}) if isinstance(vh, dict) else {} dominio = vh_data.get("dominio", "") if isinstance(vh_data, dict) else "" marca = vh_data.get("marca", "") if isinstance(vh_data, dict) else "" modelo = vh_data.get("modelo", "") if isinstance(vh_data, dict) else "" anio = vh_data.get("anio", "") if isinstance(vh_data, dict) else "" _item_bullet(pdf, f"{dominio} - {marca} {modelo} ({anio})") if riesgos_con: _section_header(pdf, "RIESGO CONSOLIDADO DEL GRUPO") for r in riesgos_con: nivel_r = r.get("nivel", "INFO") if isinstance(r, dict) else "INFO" desc = r.get("descripcion", "") if isinstance(r, dict) else str(r) _item_bullet(pdf, f"[{nivel_r.upper()}] {desc}") pdf.ln(5) # ═══════════════════════════════════════════════════════════ # SECTION 2: SCORE CREDITICIO # ═══════════════════════════════════════════════════════════ score_obj = data.get("score") if score_obj: _section_header(pdf, "SCORE CREDITICIO CROWDATA") score_val = score_obj.get("valor", 60) nivel = score_obj.get("nivel", "Bueno") if nivel == "Excelente": r, g, b = 34, 197, 94 elif nivel == "Bueno": r, g, b = 59, 130, 246 elif nivel == "Regular": r, g, b = 245, 158, 11 elif nivel == "Malo": r, g, b = 239, 68, 68 else: r, g, b = 185, 28, 28 pdf.set_font('helvetica', 'B', 10) pdf.set_text_color(71, 85, 105) pdf.cell(80, 7, "Valor:", new_x="RIGHT", new_y="TOP") pdf.set_font('helvetica', 'B', 14) pdf.set_text_color(r, g, b) pdf.cell(40, 7, clean_txt(f"{score_val} / 100"), new_x="RIGHT", new_y="TOP") pdf.set_font('helvetica', 'B', 10) pdf.cell(0, 7, clean_txt(f"Nivel: {nivel.upper()}"), new_x="LMARGIN", new_y="NEXT") pdf.set_fill_color(226, 232, 240) pdf.rect(10, pdf.get_y() + 1, 190, 4, 'F') pdf.set_fill_color(r, g, b) bar_w = max(1, int(190 * (score_val / 100))) pdf.rect(10, pdf.get_y() + 1, bar_w, 4, 'F') pdf.ln(7) historial = score_obj.get("historial", []) if historial: pdf.set_font('helvetica', 'B', 8) pdf.set_text_color(100, 116, 139) hist_items = [] for h in historial[-6:]: hist_items.append(f"{h.get('fecha', '')}: {h.get('valor', 0)}") pdf.cell(0, 5, clean_txt("Historial (12M): " + " | ".join(hist_items)), new_x="LMARGIN", new_y="NEXT") factores = score_obj.get("factores", []) if factores: pdf.ln(2) pdf.set_font('helvetica', 'B', 9) pdf.set_text_color(71, 85, 105) pdf.cell(0, 5, "Factores del Score:", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 8.5) for f in factores[:8]: if isinstance(f, dict): impacto = f.get("impacto", 0) desc = f.get('descripcion', f.get('factor', '')) signo = "+" if impacto >= 0 else "" color = (22, 101, 52) if impacto >= 0 else (185, 28, 28) pdf.set_text_color(*color) pdf.cell(0, 4.5, clean_txt(f" {signo}{impacto}: {desc}"), new_x="LMARGIN", new_y="NEXT") else: pdf.set_text_color(100, 116, 139) pdf.cell(0, 4.5, clean_txt(f" {f}"), new_x="LMARGIN", new_y="NEXT") pdf.set_text_color(15, 23, 42) pdf.ln(3) else: _empty_notice(pdf, "Sin factores de score disponibles.") # ═══════════════════════════════════════════════════════════ # SECTION 3: REGISTRO CIVIL (persona) # ═══════════════════════════════════════════════════════════ if report_type == "persona": estado_civil = rc.get("estado_civil") conyuge = rc.get("conyuge_nombre") fecha_mat = rc.get("fecha_matrimonio") if estado_civil or conyuge or fecha_mat: _section_header(pdf, "REGISTRO CIVIL") if estado_civil: _kv_line(pdf, "Estado Civil:", estado_civil) if conyuge: _kv_line(pdf, "Conyuge:", conyuge) if fecha_mat: _kv_line(pdf, "Fecha Matrimonio:", fecha_mat) # ═══════════════════════════════════════════════════════════ # SECTION 4: INDICADORES DE RIESGO # ═══════════════════════════════════════════════════════════ riesgos = data.get("riesgo", []) _section_header(pdf, "INDICADORES DE RIESGO DETECTADOS") if riesgos: for r in riesgos: nivel_r = r.get('nivel', 'INFO').upper() desc_r = r.get('descripcion', '') hallazgo_r = r.get('hallazgo', '') pdf.set_font('helvetica', 'B', 9) pdf.set_text_color(153, 27, 27) pdf.multi_cell(190, 5.5, clean_txt(f"[{nivel_r}] {desc_r} ({hallazgo_r})")) pdf.ln(1) else: _empty_notice(pdf, "Sin indicadores de riesgo detectados.") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 5: PEP / UIF # ═══════════════════════════════════════════════════════════ peps = data.get("peps", []) _section_header(pdf, "PERSONA EXPUESTA POLITICAMENTE (PEP / UIF)") if peps: for p in peps: cargo = p.get('cargo', 'Funcionario') jurisdiccion = p.get('jurisdiccion', 'Nacional') fecha_p = p.get('fecha_presentacion', 'Reciente') pdf.set_font('helvetica', '', 9.5) pdf.set_text_color(15, 23, 42) pdf.multi_cell(190, 5.5, clean_txt(f"Cargo: {cargo} | Jurisdiccion: {jurisdiccion} | DDJJ: {fecha_p}")) pdf.ln(1) else: _empty_notice(pdf, "No registra como Persona Expuesta Politicamente (PEP) en los registros oficiales.") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 6: DOMICILIOS # ═══════════════════════════════════════════════════════════ domicilios = contacto.get("domicilios", []) or data.get("historial_domicilios", []) if report_type == "empresa" and data.get("igj", {}).get("domicilio_legal"): igj_dom = data["igj"]["domicilio_legal"] if igj_dom: domicilios = [igj_dom] + domicilios _section_header(pdf, "DOMICILIOS REGISTRADOS") if domicilios: for d in domicilios: piso_dpto = "" if d.get("piso") or d.get("dpto"): piso_dpto = f" Piso {d.get('piso', '')} Dpto {d.get('dpto', '')}" full_dom = f"{d.get('calle', '')} {d.get('numero', '')}{piso_dpto}, {d.get('localidad', '')}, {d.get('provincia', '')} (CP: {d.get('cp', '')})" tipo = f"[{d.get('tipo', 'Otros').upper()}]" _item_bullet(pdf, f"{tipo} {full_dom}") else: _empty_notice(pdf, "Sin domicilios registrados.") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 7: MAPA DE UBICACION # ═══════════════════════════════════════════════════════════ _section_header(pdf, "MAPA DE UBICACION") if domicilios: for d in domicilios: prov = d.get('provincia', '') loc = d.get('localidad', '') cp = d.get('cp', '') calle = d.get('calle', '') num = d.get('numero', '') texto = f"{calle} {num}" if loc: texto += f" - {loc}" if prov: texto += f", {prov}" if cp: texto += f" (CP: {cp})" _item_bullet(pdf, texto) else: _empty_notice(pdf, "Sin datos de ubicacion disponibles.") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 8: CONTACTO Y REDES (persona) # ═══════════════════════════════════════════════════════════ if report_type == "persona": telefonos = data.get("telefonos", []) redes = data.get("redes_sociales", []) pagina_web = ident.get("pagina_web") email_contacto = ident.get("email_contacto") linkedin = ident.get("linkedin") if telefonos or redes or pagina_web or email_contacto or linkedin: _section_header(pdf, "CONTACTO Y REDES OSINT") if pagina_web: _item_bullet(pdf, f"Web: {pagina_web}") if email_contacto: _item_bullet(pdf, f"Email: {email_contacto}") if linkedin: _item_bullet(pdf, f"LinkedIn: {linkedin}") if telefonos: _item_bullet(pdf, f"Telefonos detectados: {', '.join(str(t) for t in telefonos)}") for r in redes: _item_bullet(pdf, f"{r.get('plataforma', 'Social')}: {r.get('usuario', '')} ({r.get('url', '')})") foto = ident.get("foto_perfil") if foto: _item_bullet(pdf, f"Foto de perfil detectada: {foto}") else: _section_header(pdf, "CONTACTO Y REDES OSINT") _empty_notice(pdf, "Sin datos de contacto o redes sociales detectados.") # ═══════════════════════════════════════════════════════════ # SECTION 9: SITUACION FISCAL (AFIP) # ═══════════════════════════════════════════════════════════ _section_header(pdf, "SITUACION FISCAL (AFIP)") if fiscal: _kv_line(pdf, "Estado AFIP:", fiscal.get("estado_afip")) _kv_line(pdf, "Condicion IVA:", fiscal.get("condicion_iva")) _kv_line(pdf, "Inicio Actividad:", fiscal.get("fecha_inicio_actividad")) mono = fiscal.get("monotributo") if isinstance(mono, dict) and mono.get("categoria"): mono_str = f"Categoria {mono.get('categoria')}" if mono.get("actividad_principal"): mono_str += f" - {mono.get('actividad_principal')}" if mono.get("fecha_alta"): mono_str += f" (Alta: {mono.get('fecha_alta')})" _kv_line(pdf, "Monotributo:", mono_str) actividades = fiscal.get("actividades", []) if actividades: pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "Actividades Economicas Declaradas:", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) for act in actividades: principal = "[PRINCIPAL]" if act.get("es_principal") else "" _item_bullet(pdf, f"CLAE {act.get('codigo_clae', '')} - {act.get('descripcion', '')} {principal}", 9) else: _empty_notice(pdf, "Sin datos fiscales disponibles.") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 10: SITUACION FINANCIERA (BCRA) # ═══════════════════════════════════════════════════════════ _section_header(pdf, "SITUACION FINANCIERA (BCRA)") if fin: sit_val = fin.get("bcra_situacion_actual") sit_actual = sit_val if sit_val is not None else 1 labels_sit = {1: "Normal", 2: "Seguimiento especial", 3: "Con problemas", 4: "Alto riesgo", 5: "Irrecuperable", 6: "Irrecuperable por disp. tecnica"} pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(50, 6, "Situacion Actual BCRA:", new_x="RIGHT", new_y="TOP") pdf.set_font('helvetica', '', 9.5) if sit_actual >= 3: pdf.set_text_color(185, 28, 28) else: pdf.set_text_color(15, 23, 42) pdf.cell(0, 6, clean_txt(f"{sit_actual} - {labels_sit.get(sit_actual, 'Normal')}"), new_x="LMARGIN", new_y="NEXT") total_deuda = fin.get("total_deuda_miles") dias_max = fin.get("dias_atraso_max") if total_deuda is not None or dias_max is not None: deuda_str = f"${safe_float(total_deuda):,.0f}k" if total_deuda else "-" dias_str = f" | Max. dias atraso: {dias_max}" if dias_max else "" _kv_line(pdf, "Deuda Total:", f"{deuda_str}{dias_str}") cheques = fin.get("cheques_rechazados", []) pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(50, 6, "Cheques Rechazados:", new_x="RIGHT", new_y="TOP") pdf.set_font('helvetica', '', 9.5) if cheques: pdf.set_text_color(185, 28, 28) pdf.cell(0, 6, clean_txt(f"SI - {len(cheques)} cheque(s) rechazado(s)"), new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 8.5) pdf.set_text_color(15, 23, 42) for chq in cheques: monto_chq = chq.get('monto') or 0 nro = chq.get('nro_cheque') or '' nro_str = f" | Nro: {nro}" if nro else "" estado_multa = chq.get('estado_multa') or '' multa_str = f" | Multa: {estado_multa}" if estado_multa else "" en_rev = " (En revisión)" if chq.get('en_revision') else "" proc_jud = " (Judicializado)" if chq.get('proceso_judicial') else "" estado_str = f"{chq.get('estado') or 'Rechazado'}{en_rev}{proc_jud}" # NUEVOS CAMPOS AGREGADOS denom_jur = chq.get('denom_juridica') or '' denom_str = f" | {denom_jur}" if denom_jur else "" cta_tipo = " (Cta.Personal)" if chq.get('cta_personal') else " (Cta.Empresarial)" if chq.get('cta_personal') is not None else "" _item_bullet(pdf, f"Fecha: {chq.get('fecha')} | Banco: {chq.get('banco')} | Monto: ${safe_float(monto_chq):,.2f} | Estado: {estado_str}{nro_str}{multa_str}{denom_str}{cta_tipo}", 8.5) else: pdf.set_text_color(15, 23, 42) pdf.cell(0, 6, "NO registra cheques rechazados en el ultimo anio", new_x="LMARGIN", new_y="NEXT") historial = fin.get("bcra_historial", []) if historial: pdf.ln(2) pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "Historial de Entidades y Deudas:", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) for h in historial[:12]: monto_deuda = h.get('monto_deuda') or 0 denominacion = h.get('denominacion') or '' dias_atraso = h.get('dias_atraso') denom_str = f" ({denominacion})" if denominacion else "" dias_str = f" | Dias atraso: {dias_atraso}" if dias_atraso else "" sit_jur = h.get('situacion_juridica') or '' jur_str = f" | Juridica: {sit_jur}" if sit_jur else "" en_rev = " | En Revisión" if h.get('en_revision') else "" proc_jud = " | Judicializado" if h.get('proceso_judicial') else "" # NUEVOS CAMPOS AGREGADOS refin_str = " | Refinanciada" if h.get('refinanciaciones') else "" recateg_str = " | Recategorizada" if h.get('recategorizacion') else "" irrec_disp_str = " | Irrec.Disp.Téc." if h.get('irrec_disp_tecnica') else "" _item_bullet(pdf, f"Periodo: {h.get('periodo')} | Entidad: {h.get('entidad')}{denom_str} | Sit. {h.get('situacion')} | Deuda: ${safe_float(monto_deuda):,.0f}k{dias_str}{jur_str}{refin_str}{recateg_str}{irrec_disp_str}{en_rev}{proc_jud}", 9) else: _empty_notice(pdf, "Sin datos financieros BCRA disponibles.") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 11: SOCIEDADES, CNV & MATRICULAS # ═══════════════════════════════════════════════════════════ cnv_registros = data.get("cnv_registros", []) if societario and societario.get("cnv_registros"): cnv_registros = cnv_registros + societario.get("cnv_registros") matriculas = societario.get("matriculas_profesionales", []) if societario else [] _section_header(pdf, "REGISTROS SOCIETARIOS, CNV Y PROFESIONALES") has_societario = bool(cnv_registros or matriculas) if has_societario: if cnv_registros: pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "Registros Comision Nacional de Valores (CNV):", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) for cnv in cnv_registros: _item_bullet(pdf, f"{cnv.get('razon_social') or 'CNV'} | Cat: {cnv.get('categoria')} | Matricula: {cnv.get('matricula')} | Estado: {cnv.get('estado')}", 9) pdf.ln(2) if matriculas: pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "Matriculas Profesionales:", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) for mat in matriculas: _item_bullet(pdf, f"Consejo: {mat.get('consejo')} | Matricula: {mat.get('matricula')} | Estado: {mat.get('estado') or 'Activo'}", 9) else: _empty_notice(pdf, "Sin registros societarios, CNV ni matriculas profesionales.") igj_socios = data.get("igj", {}).get("socios_directivos", []) if report_type == "empresa" and igj_socios: pdf.ln(2) pdf.set_font('helvetica', 'B', 12) pdf.set_text_color(30, 41, 59) pdf.cell(0, 8, "SOCIOS Y DIRECTIVOS (IGJ)", new_x="LMARGIN", new_y="NEXT") pdf.set_fill_color(226, 232, 240) pdf.rect(10, pdf.get_y(), 190, 0.3, 'F') pdf.ln(3) for s in igj_socios: _item_bullet(pdf, f"{s.get('nombre')} | CUIL: {s.get('cuil')} | Rol/Cargo: {s.get('rol') or 'Socio'}") igj_data = data.get("igj", {}) objeto_social = igj_data.get("objeto_social") if objeto_social: _item_bullet(pdf, f"Objeto Social: {str(objeto_social)[:120]}") balances = igj_data.get("balances", []) if balances: for b in balances[:3]: periodo = b.get("periodo", "") if isinstance(b, dict) else "" resultado = b.get("resultado", "") if isinstance(b, dict) else "" _item_bullet(pdf, f"Balance {periodo}: {resultado}") pagina_web = ident.get("pagina_web") email_contacto = ident.get("email_contacto") linkedin = ident.get("linkedin") if pagina_web or email_contacto or linkedin: contact_parts = [] if pagina_web: contact_parts.append(f"Web: {pagina_web}") if email_contacto: contact_parts.append(f"Email: {email_contacto}") if linkedin: contact_parts.append(f"LinkedIn: {linkedin}") _item_bullet(pdf, f"Contacto: {' | '.join(contact_parts)}") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 12: BILLETERAS VIRTUALES / FINTECHS # ═══════════════════════════════════════════════════════════ if report_type == "persona": bv = data.get("billeteras_virtuales", {}) if bv and (bv.get("detalle", []) or bv.get("total_deuda_fintech", 0) > 0): _section_header(pdf, "BILLETERAS VIRTUALES / FINTECHS") total = bv.get("total_deuda_fintech", 0) wallets = bv.get("cantidad_wallets_con_deuda", 0) _kv_line(pdf, "Deuda total fintechs:", f"${total:,.0f}") _kv_line(pdf, "Billeteras con deuda:", str(wallets)) fintech_count = bv.get("cantidad_fintech_detectadas", 0) if fintech_count: _kv_line(pdf, "Fintechs detectadas:", str(fintech_count)) pdf.ln(2) for item in bv.get("detalle", []): marca = item.get("marca", item.get("entidad_bcra", "?")) situacion = item.get("situacion", 1) monto = item.get("monto", 0) or 0 desc = item.get("situacion_desc", "") rubro = item.get("rubro", "") pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(180, 83, 9) pdf.multi_cell(190, 5.5, clean_txt(f"{marca} ({rubro}) - Sit. {situacion}: {desc}")) pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) pdf.cell(0, 5, clean_txt(f" Monto: ${monto:,.0f} | Periodo: {item.get('periodo', '?')}"), new_x="LMARGIN", new_y="NEXT") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 13: ANTECEDENTES JUDICIALES (PJN + JUBA) # ═══════════════════════════════════════════════════════════ _section_header(pdf, "ANTECEDENTES JUDICIALES (PJN, MEV Y JUBA)") causas = judicial.get("causas", []) if judicial else [] if causas: for c in causas: exp = c.get('expediente') or 'S/N' caratula = c.get('caratula_completa') or c.get('caratula') or 'Sin caratula' fuero = c.get('fuero') or 'Ordinario' estado = c.get('estado') or 'Tramitacion' fecha = c.get('fecha') or 'Reciente' juzgado = c.get('juzgado') or 'Juzgado de turno' voces = c.get('voces') or '' magistrados = c.get('magistrados') or '' tipo_fallo = c.get('tipo_fallo') or '' pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(15, 23, 42) pdf.cell(0, 5, clean_txt(f"Exp: {exp} - {fecha} (Estado: {estado})"), new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) lineas = [f" Caratula: {caratula}", f" Fuero: {fuero} | Juzgado: {juzgado}"] if voces: lineas.append(f" Voces: {voces}") if magistrados: lineas.append(f" Magistrados: {magistrados}") if tipo_fallo: lineas.append(f" Tipo Fallo: {tipo_fallo}") pdf.multi_cell(190, 4.5, clean_txt("\n".join(lineas))) pdf.ln(2.5) else: _empty_notice(pdf, "Sin causas judiciales federales, comerciales ni provinciales (JUBA) detectadas.") # ═══════════════════════════════════════════════════════════ # SECTION 14: PATRIMONIO (VEHICULOS E INMUEBLES) # ═══════════════════════════════════════════════════════════ vehiculos = patrimonial.get("vehiculos", []) if patrimonial else [] inmuebles = patrimonial.get("inmuebles", []) if patrimonial else [] if vehiculos or inmuebles: _section_header(pdf, "REGISTROS PATRIMONIALES (VEHICULOS E INMUEBLES)") if vehiculos: pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "Vehiculos Registrados (DNRPA / ARBA Automotores):", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) for v in vehiculos: detalle_v = f"Dominio: {v.get('dominio')} | {v.get('marca', '')} {v.get('modelo', '')} ({v.get('anio', '')}) | Radicacion: {v.get('radicacion', '') or v.get('provincia', '')}" vtv = v.get('validez_vtv') seguro = "Seguro OK" if v.get('tiene_seguro') else "Sin Seguro" estado = v.get('estado') extras = " | ".join(filter(None, [f"VTV: {vtv}" if vtv else None, seguro, estado])) if extras: detalle_v += f"\n {extras}" pdf.multi_cell(190, 5.5, clean_txt(f"{detalle_v}")) pdf.ln(2) if inmuebles: pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "Bienes Inmuebles Registrados (ARBA Catastro / Boletin):", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) for inm in inmuebles: partida = f"Partida: {inm.get('nro_partida') or 'N/A'}" matricula = f"Matricula: {inm.get('matricula') or '-'}" superficie = f"Sup: {inm.get('superficie') or '-'}" desc = inm.get('descripcion') or 'Inmueble' prov = inm.get('provincia') or 'Bs.As.' fuente = inm.get('fuente') or 'ARBA' val_val = inm.get('valuacion_fiscal') try: val_str = f"${int(val_val):,}".replace(",", ".") if val_val else "-" except Exception: val_str = "-" val_fiscal = f"Valuacion: {val_str}" _item_bullet(pdf, f"[{fuente}] {partida} | {matricula} | {desc} ({prov}) | {superficie} | {val_fiscal}", 9) deuda_patentes = patrimonial.get("deuda_patentes", []) if patrimonial else [] if deuda_patentes: pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "Deuda de Patentes Vehiculares (AGIP):", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) for dp in deuda_patentes[:10]: anio = dp.get("anio", "") if isinstance(dp, dict) else "" cuota = dp.get("cuota", "") if isinstance(dp, dict) else "" concepto = dp.get("concepto", "") if isinstance(dp, dict) else "" importe = dp.get("importe", "") if isinstance(dp, dict) else "" _item_bullet(pdf, f"{anio} {cuota} | {concepto} | {importe}", 9) else: _section_header(pdf, "REGISTROS PATRIMONIALES (VEHICULOS E INMUEBLES)") _empty_notice(pdf, "Sin registros patrimoniales de vehiculos ni inmuebles.") pdf.ln(2) # ═══════════════════════════════════════════════════════════ # SECTION 15: CONTRATOS ESTATALES (COMPR.AR) # ═══════════════════════════════════════════════════════════ _section_header(pdf, "REGISTROS EN CONTRATACIONES DEL ESTADO (COMPR.AR)") compras = data.get("compras_estatales", []) if compras: for comp in compras: registro = comp.get('registro') or '' reg_str = f" | Registro: {registro}" if registro else "" _item_bullet(pdf, f"Proveedor: {comp.get('razon_social') or '-'} | CUIT: {comp.get('cuit_proveedor', '')} | Inscripcion: {comp.get('estado_inscripcion', '')} | Rubro: {comp.get('rubro_principal', '')}{reg_str}") adjudicaciones = comp.get('adjudicaciones', []) contratos = comp.get('contratos', []) if adjudicaciones: pdf.set_font('helvetica', '', 8) for adj in adjudicaciones[:5]: desc = adj.get('descripcion_proceso') or adj.get('objeto') or '-' monto = adj.get('monto') or adj.get('monto_total') or '' monto_str = f" | Monto: ${safe_float(monto):,.0f}" if monto else "" org = adj.get('organismo') or adj.get('entidad') or '' org_str = f" | Org: {org}" if org else "" _item_bullet(pdf, f" Adjudicacion: {desc[:80]}{monto_str}{org_str}", 8) if contratos: pdf.set_font('helvetica', '', 8) for ct in contratos[:5]: desc = ct.get('descripcion_proceso') or ct.get('objeto') or '-' monto = ct.get('monto') or ct.get('monto_total') or '' monto_str = f" | Monto: ${safe_float(monto):,.0f}" if monto else "" fecha_c = ct.get('fecha') or ct.get('fecha_fin') or '' fecha_str = f" | Fecha: {fecha_c}" if fecha_c else "" _item_bullet(pdf, f" Contrato: {desc[:80]}{monto_str}{fecha_str}", 8) pdf.ln(1) else: _empty_notice(pdf, "No registra inscripciones ni contratos vigentes como proveedor del Estado en COMPR.AR.") # ═══════════════════════════════════════════════════════════ # SECTION 16: PREVISIONAL & SALUD (ANSES / RUIDO) # ═══════════════════════════════════════════════════════════ if report_type == "persona": _section_header(pdf, "PREVISIONAL Y OBRA SOCIAL (ANSES / RUIDO / SSSALUD)") if prev and (prev.get("tiene_aportes") is not None or prev.get("ultimo_empleador") or prev.get("obra_social")): _kv_line(pdf, "Aportes al dia:", "SI" if prev.get('tiene_aportes') else "NO") _kv_line(pdf, "Tipo Beneficiario:", prev.get('tipo_beneficiario')) _kv_line(pdf, "Ultimo Empleador:", prev.get('ultimo_empleador')) _kv_line(pdf, "Obra Social (ANSES):", prev.get('obra_social')) fecha_alta_os = prev.get('fecha_alta_obra_social') if fecha_alta_os: _kv_line(pdf, "Alta OS:", fecha_alta_os) estado_padron = prev.get('estado_padron') if estado_padron: _kv_line(pdf, "Estado Padron ANSES:", estado_padron) else: estado_padron = prev.get('estado_padron') if prev else None if estado_padron: _kv_line(pdf, "Estado Padron ANSES:", estado_padron) else: _empty_notice(pdf, "Sin datos previsionales activos en ANSES.") beneficios = prev.get("beneficios_sociales", []) if prev else [] if beneficios: _kv_line(pdf, "Beneficios Sociales:", ', '.join(str(b) for b in beneficios[:5])) jubilaciones = prev.get("jubilaciones_pensiones", []) if prev else [] if jubilaciones: _kv_line(pdf, "Jubilaciones/Pensiones:", ', '.join(str(j) for j in jubilaciones[:5])) proximo_cobro = prev.get("fecha_proximo_cobro") if prev else None lugar_cobro = prev.get("lugar_cobro") if prev else None if proximo_cobro or lugar_cobro: cobro_parts = [] if proximo_cobro: cobro_parts.append(f"Proximo cobro: {proximo_cobro}") if lugar_cobro: cobro_parts.append(f"Lugar: {lugar_cobro}") _kv_line(pdf, "Cobro:", ' | '.join(cobro_parts)) if salud and (salud.get("cobertura_activa") or (isinstance(salud.get("detalles"), dict) and salud["detalles"].get("nombre_obra_social"))): obrasocial = salud.get("detalles", {}).get("nombre_obra_social") or "Obra Social Activa" _kv_line(pdf, "Cobertura Medica SSSalud:", f"SI | {obrasocial}") else: _kv_line(pdf, "Cobertura Medica SSSalud:", "Sin cobertura de salud activa.") historial_coberturas = _get_nested(salud, "detalles", "historial_coberturas", default=[]) if historial_coberturas: pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, "Historial de Coberturas:", new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) for hc in historial_coberturas: if isinstance(hc, dict): fecha_h = hc.get('fecha', '') nombre_h = hc.get('nombre_obra_social', '') or hc.get('nombre', '') estado_h = hc.get('estado', '') parts = [p for p in [fecha_h, nombre_h, estado_h] if p] _item_bullet(pdf, " | ".join(parts), 9) else: _item_bullet(pdf, str(hc), 9) seccion_electoral = ident.get("seccion_electoral") lugar_votacion = ident.get("lugar_votacion") mesa_votacion = ident.get("mesa_votacion") if seccion_electoral or lugar_votacion or mesa_votacion: pdf.ln(2) pdf.set_font('helvetica', 'B', 9) pdf.set_text_color(71, 85, 105) pdf.cell(0, 5, "PADRON ELECTORAL:", new_x="LMARGIN", new_y="NEXT") _kv_line(pdf, "Seccion:", seccion_electoral or "-") _kv_line(pdf, "Lugar de votacion:", lugar_votacion or "-") _kv_line(pdf, "Mesa:", mesa_votacion or "-") # ═══════════════════════════════════════════════════════════ # SECTION 17: DEUDORES ALIMENTARIOS (siempre mostrar) # ═══════════════════════════════════════════════════════════ _section_header(pdf, "DEUDORES ALIMENTARIOS MOROSOS (DAM)") deudores = data.get("deudores_alimentarios") if deudores and isinstance(deudores, dict): resultado = deudores.get("resultado") or "" if resultado and resultado != "NO_REGISTRADO": nombre_dam = deudores.get("nombre_completo") or "-" dni_dam = deudores.get("dni") or "-" _kv_line(pdf, "Nombre:", nombre_dam) _kv_line(pdf, "DNI:", dni_dam) _kv_line(pdf, "Resultado:", resultado) else: _empty_notice(pdf, "Resultado: NO_REGISTRADO - No se registra como deudor alimentario moroso.") else: _empty_notice(pdf, "Sin datos de deudores alimentarios morosos.") # ═══════════════════════════════════════════════════════════ # SECTION 18: BOLETIN OFICIAL # ═══════════════════════════════════════════════════════════ _section_header(pdf, "PUBLICACIONES EN BOLETINES OFICIALES") boletines = data.get("boletin_oficial", []) if boletines: for pub in boletines[:15]: fecha = pub.get('fecha') or 'S/F' secc = pub.get('seccion') or 'BORA' tipo = pub.get('tipo') or '' texto = pub.get('texto') or '' if len(texto) > 250: texto = texto[:250] + "..." pdf.set_font('helvetica', 'B', 9) pdf.set_text_color(15, 23, 42) header = f"{fecha} - Secc: {secc}" if tipo: header += f" - {tipo}" pdf.cell(0, 5, clean_txt(header), new_x="LMARGIN", new_y="NEXT") pdf.set_font('helvetica', '', 8.5) pdf.multi_cell(190, 4, clean_txt(f" {texto}")) pdf.ln(2) else: _empty_notice(pdf, "Sin publicaciones en Boletin Oficial Nacional ni Provinciales detectadas.") # ═══════════════════════════════════════════════════════════ # SECTION 19: INFRACCIONES DE TRANSITO # ═══════════════════════════════════════════════════════════ infracciones = data.get("infracciones_transito", []) if report_type == "persona": _section_header(pdf, "INFRACCIONES DE TRANSITO (SINAI / ANSV)") if infracciones: for inf in infracciones: monto_inf = inf.get('monto') or 0 dominio = inf.get('dominio') or '' dom_str = f" | Dominio: {dominio}" if dominio else "" nro_causa = inf.get('nro_causa') or '' causa_str = f" | Causa: {nro_causa}" if nro_causa else "" venc = inf.get('vencimiento') or '' venc_str = f" | Vence: {venc}" if venc else "" _item_bullet(pdf, f"Acta: {inf.get('acta') or 'S/N'} | Fecha: {inf.get('fecha', '-')} | " f"Motivo: {inf.get('motivo', '-')} | Monto: ${safe_float(monto_inf):,.0f} | " f"Estado: {inf.get('estado', '-')} | Jurisdiccion: {inf.get('jurisdiccion', '-')}{dom_str}{causa_str}{venc_str}") else: _empty_notice(pdf, "Sin infracciones de transito registradas en ANSV / SINAI.") # ═══════════════════════════════════════════════════════════ # SECTION 20: MARCAS E INPI # ═══════════════════════════════════════════════════════════ marcas = data.get("marcas_inpi", []) _section_header(pdf, "MARCAS Y PATENTES REGISTRADAS (INPI)") if marcas: for m in marcas: acta = m.get('acta') or '' acta_str = f" | Acta: {acta}" if acta else "" titulares = m.get('titulares') or '' tit_str = f" | Titulares: {titulares[:50]}" if titulares else "" tipo_marca = m.get('tipo_marca') or '' tipo_str = f" | Tipo: {tipo_marca}" if tipo_marca else "" nro_res = m.get('numero_resolucion') or '' res_str = f" | Resol.: {nro_res}" if nro_res else "" _item_bullet(pdf, f"{m.get('denominacion', '-')} | Clase: {m.get('clase', '-')} | " f"Estado: {m.get('estado', '-')} | Vencimiento: {m.get('fecha_vencimiento', '-')}{acta_str}{tipo_str}{res_str}{tit_str}") else: _empty_notice(pdf, "Sin marcas o patentes registradas en INPI.") # ═══════════════════════════════════════════════════════════ # SECTION 21: INHIBICIONES Y EMBARGOS # ═══════════════════════════════════════════════════════════ inhibiciones = ( patrimonial.get("inhibiciones", []) or (judicial.get("inhibiciones_embargos", []) if judicial else []) or [] ) _section_header(pdf, "INHIBICIONES Y EMBARGOS") if inhibiciones: for inh in inhibiciones: if isinstance(inh, dict): tipo = inh.get('tipo', 'INHIBICION') fecha = inh.get('fecha', '') organismo = inh.get('organismo', 'Juzgado') descripcion = inh.get('descripcion', '') else: tipo = getattr(inh, 'tipo', 'INHIBICION') fecha = getattr(inh, 'fecha', '') organismo = getattr(inh, 'organismo', 'Juzgado') descripcion = getattr(inh, 'descripcion', '') pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(153, 27, 27) pdf.multi_cell(190, 5, clean_txt(f"[{str(tipo).upper()}] - {fecha or 'Fecha no disponible'} - {organismo}")) if descripcion: pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) pdf.multi_cell(190, 5, clean_txt(f" {descripcion}")) pdf.ln(2) else: _empty_notice(pdf, "Sin inhibiciones ni embargos activos detectados.") # ═══════════════════════════════════════════════════════════ # SECTION 21b: ANTECEDENTES PENALES # ═══════════════════════════════════════════════════════════ if report_type == "persona": ant_penales = data.get("antecedentes_penales", []) _section_header(pdf, "ANTECEDENTES PENALES") if ant_penales: for ap in ant_penales: tipo = ap.get('tipo', 'Antecedente') if isinstance(ap, dict) else 'Antecedente' fecha = ap.get('fecha', '') if isinstance(ap, dict) else '' delito = ap.get('delito', '') if isinstance(ap, dict) else '' estado = ap.get('estado', '') if isinstance(ap, dict) else '' desc = ap.get('descripcion', ap.get('detalle', '')) if isinstance(ap, dict) else '' organismo = ap.get('organismo', '') if isinstance(ap, dict) else '' pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(153, 27, 27) pdf.multi_cell(190, 5, clean_txt(f"[{str(tipo).upper()}] - {fecha or 'Sin fecha'} - {organismo or ''}")) if delito: pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) pdf.cell(0, 5, clean_txt(f" Delito: {delito}"), new_x="LMARGIN", new_y="NEXT") if desc: pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) pdf.multi_cell(190, 5, clean_txt(f" {desc}")) if estado: pdf.set_font('helvetica', 'I', 8.5) pdf.set_text_color(100, 116, 139) pdf.cell(0, 5, clean_txt(f" Estado: {estado}"), new_x="LMARGIN", new_y="NEXT") pdf.ln(2) else: _empty_notice(pdf, "Sin antecedentes penales detectados en los registros consultados.") # ═══════════════════════════════════════════════════════════ # SECTION 21c: CONCURSOS Y QUIEBRAS # ═══════════════════════════════════════════════════════════ concursos = (societario.get("concursos", []) if societario else []) quiebras = (societario.get("quiebras", []) if societario else []) all_cq = concursos + quiebras _section_header(pdf, "CONCURSOS Y QUIEBRAS") if all_cq: for cq in all_cq: if isinstance(cq, dict): tipo = cq.get('tipo', 'Concurso') fecha = cq.get('fecha', '') empresa = cq.get('empresa', '') cuit_emp = cq.get('cuit_empresa', '') desc = cq.get('descripcion', cq.get('detalle', '')) else: tipo = getattr(cq, 'tipo', 'Concurso') fecha = getattr(cq, 'fecha', '') empresa = getattr(cq, 'empresa', '') cuit_emp = getattr(cq, 'cuit_empresa', '') desc = getattr(cq, 'descripcion', getattr(cq, 'detalle', '')) pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(146, 64, 14) pdf.multi_cell(190, 5, clean_txt(f"[{str(tipo).upper()}] - {fecha or 'Sin fecha'}")) if empresa: pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) emp_str = f" Empresa: {empresa}" if cuit_emp: emp_str += f" (CUIT: {cuit_emp})" pdf.cell(0, 5, clean_txt(emp_str), new_x="LMARGIN", new_y="NEXT") if desc: pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) pdf.multi_cell(190, 5, clean_txt(f" {desc}")) pdf.ln(2) else: _empty_notice(pdf, "Sin concursos ni quiebras registrados.") # ═══════════════════════════════════════════════════════════ # SECTION 21d: PARTICIPACIONES SOCIETARIAS (IGJ) - persona # ═══════════════════════════════════════════════════════════ if report_type == "persona": part = societario.get("participaciones", []) if societario else [] if not part: part = data.get("participaciones", []) _section_header(pdf, "PARTICIPACIONES SOCIETARIAS (IGJ)") if part: for p in part: if isinstance(p, dict): empresa_p = p.get('empresa', '') rol = p.get('rol', 'Socio') cuit_p = p.get('cuit', '') porcentaje = p.get('porcentaje', '') fecha_ins = p.get('fecha_inscripcion', '') detalles = p.get('detalles', p.get('descripcion', '')) else: empresa_p = getattr(p, 'empresa', '') rol = getattr(p, 'rol', 'Socio') cuit_p = getattr(p, 'cuit', '') porcentaje = getattr(p, 'porcentaje', '') fecha_ins = getattr(p, 'fecha_inscripcion', '') detalles = getattr(p, 'detalles', getattr(p, 'descripcion', '')) pdf.set_font('helvetica', 'B', 9.5) pdf.set_text_color(30, 64, 175) parts = [f"[{str(rol).upper()}]"] if empresa_p: parts.append(empresa_p) if cuit_p: parts.append(f"CUIT: {cuit_p}") pdf.multi_cell(190, 5, clean_txt(" | ".join(parts))) if porcentaje: pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) pdf.cell(0, 5, clean_txt(f" Participacion: {porcentaje}"), new_x="LMARGIN", new_y="NEXT") if fecha_ins: pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) pdf.cell(0, 5, clean_txt(f" Inscripto: {fecha_ins}"), new_x="LMARGIN", new_y="NEXT") if detalles: pdf.set_font('helvetica', '', 9) pdf.set_text_color(15, 23, 42) pdf.multi_cell(190, 5, clean_txt(f" {detalles}")) pdf.ln(2) else: _empty_notice(pdf, "Sin participaciones societarias detectadas en IGJ.") # ═══════════════════════════════════════════════════════════ # SECTION 22: MAPA DE RELACIONES # ═══════════════════════════════════════════════════════════ vinculos = data.get("vinculos", []) if report_type == "persona" else [] _section_header(pdf, "MAPA DE RELACIONES") if vinculos: pdf.set_font('helvetica', 'B', 8.5) pdf.set_text_color(71, 85, 105) pdf.cell(65, 5, "Nombre", new_x="RIGHT", new_y="TOP") pdf.cell(45, 5, "CUIT/CUIL", new_x="RIGHT", new_y="TOP") pdf.cell(35, 5, "Tipo de Vinculo", new_x="RIGHT", new_y="TOP") pdf.cell(0, 5, "Detalle", new_x="LMARGIN", new_y="NEXT") pdf.set_fill_color(226, 232, 240) pdf.rect(10, pdf.get_y(), 190, 0.3, 'F') pdf.ln(2) pdf.set_font('helvetica', '', 8.5) pdf.set_text_color(15, 23, 42) for v in vinculos[:10]: if isinstance(v, dict): nombre_v = v.get('nombre', '') cuit_v = v.get('cuit', '') tipo_v = v.get('tipo', '') detalle_v = v.get('detalle', '') else: nombre_v = getattr(v, 'nombre', '') cuit_v = getattr(v, 'cuit', '') tipo_v = getattr(v, 'tipo', '') detalle_v = getattr(v, 'detalle', '') pdf.cell(65, 5, clean_txt(str(nombre_v)[:30]), new_x="RIGHT", new_y="TOP") pdf.cell(45, 5, clean_txt(str(cuit_v)), new_x="RIGHT", new_y="TOP") pdf.cell(35, 5, clean_txt(str(tipo_v)[:18]), new_x="RIGHT", new_y="TOP") pdf.cell(0, 5, clean_txt(str(detalle_v)[:30]), new_x="LMARGIN", new_y="NEXT") else: _empty_notice(pdf, "Sin vinculos detectados.") # ═══════════════════════════════════════════════════════════ # SECTION 23: FORMACION ACADEMICA (persona) # ═══════════════════════════════════════════════════════════ if report_type == "persona": academico = data.get("academico", {}) titulos = academico.get("titulos", []) if academico else [] certificaciones = academico.get("certificaciones", []) if academico else [] if titulos or certificaciones: _section_header(pdf, "FORMACION ACADEMICA") for t in titulos: titulo = t.get('titulo', '') if isinstance(t, dict) else getattr(t, 'titulo', '') inst = t.get('institucion', '') if isinstance(t, dict) else getattr(t, 'institucion', '') anio = t.get('anio_graduacion', '') if isinstance(t, dict) else getattr(t, 'anio_graduacion', '') nivel = t.get('nivel', 'Grado') if isinstance(t, dict) else getattr(t, 'nivel', 'Grado') _item_bullet(pdf, f"[{nivel}] {titulo} | {inst or '-'} | Graduacion: {anio or '-'}") for c in certificaciones: nombre = c.get('nombre', '') if isinstance(c, dict) else str(c) _item_bullet(pdf, f"Certificacion: {nombre}") # ═══════════════════════════════════════════════════════════ # SECTION 24: LINEA DE TIEMPO # ═══════════════════════════════════════════════════════════ timeline = data.get("timeline", []) if timeline: _section_header(pdf, "LINEA DE TIEMPO") cat_colors = { "fiscal": (34, 197, 94), "financiero": (59, 130, 246), "judicial": (239, 68, 68), "previsional": (245, 158, 11), "patrimonial": (168, 85, 247), "general": (100, 116, 139), } for ev in timeline[:25]: fecha = ev.get("fecha", "") titulo = ev.get("titulo", "") desc = ev.get("descripcion", "") cat = ev.get("categoria", "general") r, g, b = cat_colors.get(cat, (100, 116, 139)) pdf.set_font('helvetica', 'B', 9) pdf.set_text_color(r, g, b) pdf.cell(0, 5, clean_txt(f"[{fecha}] {titulo}"), new_x="LMARGIN", new_y="NEXT") if desc: pdf.set_font('helvetica', '', 8) pdf.set_text_color(100, 116, 139) pdf.cell(0, 4, clean_txt(f" {desc[:160]}"), new_x="LMARGIN", new_y="NEXT") pdf.ln(1.5) # ═══════════════════════════════════════════════════════════ # SECTION 25: HISTORIAL WEB (ARCHIVE.ORG) # ═══════════════════════════════════════════════════════════ web_hist_obj = data.get("web_historial") if web_hist_obj: _section_header(pdf, "HISTORIAL WEB (ARCHIVE.ORG)") wh_url = web_hist_obj.get("url_consultada", "") wh_total = web_hist_obj.get("total_snapshots", 0) wh_snap = web_hist_obj.get("snapshot_mas_cercano") or {} wh_historial = web_hist_obj.get("historial", []) if wh_url: pdf.set_font('helvetica', 'B', 9) pdf.set_text_color(59, 130, 246) url_short = wh_url[:70] + "..." if len(wh_url) > 70 else wh_url pdf.cell(0, 5, clean_txt(f"URL: {url_short}"), new_x="LMARGIN", new_y="NEXT") if wh_total: snap_date = wh_snap.get("timestamp", "") if isinstance(wh_snap, dict) else "" _kv_line(pdf, "Total capturas:", str(wh_total)) if snap_date: _kv_line(pdf, "Ultima captura:", snap_date) pdf.ln(2) for wh in wh_historial[:10]: ts = wh.get("timestamp", "") or wh.get("fecha", "") status = wh.get("status", "") tipo = wh.get("tipo", "") url_w = wh.get("url_wayback", "") detail = f" {ts} | {tipo} | {status}" if url_w: detail += f" | {url_w[:60]}" pdf.set_font('helvetica', '', 8) pdf.set_text_color(100, 116, 139) pdf.cell(0, 4, clean_txt(detail), new_x="LMARGIN", new_y="NEXT") pdf.ln(3) # ═══════════════════════════════════════════════════════════ # SECTION 26: BITACORA DE FUENTES CONSULTADAS # ═══════════════════════════════════════════════════════════ sources = meta.get("sources", []) if meta else [] empty_sources = meta.get("empty_sources", []) if meta else [] generated_at = meta.get("generated_at", "") if meta else "" pdf.add_page() _section_header(pdf, "BITACORA DE FUENTES CONSULTADAS") n_ok = len(sources) n_fail = len(meta.get("failures", []) if meta else []) n_empty = len(empty_sources) pdf.set_font('helvetica', '', 9) pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, clean_txt(f"Resumen: {n_ok} fuentes con datos | {n_fail} fallidas | {n_empty} sin datos para este CUIT"), new_x="LMARGIN", new_y="NEXT") pdf.ln(3) all_sources = [ ("ARCA / AFIP", "Situacion fiscal, IVA, Monotributo, Actividades"), ("BCRA", "Situacion crediticia, historial, cheques rechazados"), ("Boletin Oficial", "Publicaciones oficiales nacionales y edictos"), ("Timeline BORA", "Historial de sociedades del Boletin Oficial"), ("Boletines Provinciales", "Boletines de 24 jurisdicciones provinciales"), ("IGJ", "Sociedades, directivos y sede social registrada"), ("Poder Judicial (PJN)", "Causas judiciales federales y comerciales"), ("JUBA (SCBA)", "Causas en Justicia de la Provincia de Buenos Aires"), ("Poder Judicial Provincial", "Causas judiciales provinciales"), ("DNRPA", "Vehiculos registrados a nombre de la persona"), ("SINAI / ANSV", "Infracciones de transito nacionales y PBA"), ("INPI", "Marcas y patentes comerciales registradas"), ("ANSES", "Aportes previsionales, obra social y beneficios"), ("Redes Sociales OSINT", "Perfiles publicos en LinkedIn, Google, Instagram, etc."), ("Telefonia OSINT", "Telefonos vinculados a nombre o CUIT"), ("RENAPER", "Validacion de identidad y datos del DNI"), ("RENAPER Facial", "Estado biometrico del documento nacional"), ("Colegios Profesionales", "Matriculas habilitadas en consejos profesionales"), ("SSSalud / RUIDO", "Cobertura de salud activa y obra social"), ("COMPR.AR", "Inscripcion como proveedor del Estado Nacional"), ("Padron Electoral", "Datos del padron electoral y domicilio electoral"), ("SGARHU / SIU", "Titulos universitarios oficiales registrados"), ("SISCOP / Reg. Civil", "Deteccion de fallecimiento y estado del DNI"), ("ARBA Automotores", "Deuda de patente de vehiculos en ARBA"), ("ARBA Catastro / AGIP", "Inmuebles e impositivas en PBA y CABA"), ("CNV", "Registro de agentes y fondos en Comision de Valores"), ("UIF / PEPs", "Personas Expuestas Politicamente - lista UIF"), ("Name Search OSINT", "Busqueda de nombre en fuentes de datos abiertas"), ("Infracciones", "Infracciones de transito"), ("Inhibiciones / Embargos", "Inhibiciones y embargos judiciales"), ("Billeteras Virtuales / Fintechs", "Deudas en billeteras virtuales y fintechs"), ("Google Images", "Busqueda de fotos de perfil por nombre"), ("Antecedentes Penales", "Antecedentes penales de fuente publica"), ("Concursos y Quiebras", "Concursos preventivos y quiebras"), ("Participaciones IGJ", "Participaciones societarias en sociedades"), ] failures = meta.get("failures", []) if meta else [] for i, (src_name, src_desc) in enumerate(all_sources): is_used = src_name in sources is_failed = src_name in failures is_empty = src_name in empty_sources if is_failed: fill = (254, 242, 242) prefix = "[FALLO]" color = (185, 28, 28) desc_text = f"{src_desc} - FALLO LA CONSULTA (Requiere revision)" elif is_used: fill = (245, 250, 245) prefix = "[x]" color = (22, 101, 52) desc_text = src_desc elif is_empty: fill = (255, 251, 235) prefix = "[ ]" color = (146, 64, 14) desc_text = f"{src_desc} - Sin datos para este CUIT" else: fill = (248, 248, 248) prefix = "[ ]" color = (100, 116, 139) desc_text = src_desc pdf.set_fill_color(*fill) pdf.rect(10, pdf.get_y(), 190, 7, 'F') pdf.set_text_color(*color) pdf.set_font('helvetica', 'B' if (is_used or is_failed) else '', 8.5) pdf.cell(12, 6, prefix, new_x="RIGHT", new_y="TOP") pdf.set_font('helvetica', 'B', 8.5) pdf.set_text_color(15, 23, 42) pdf.cell(50, 6, clean_txt(src_name), new_x="RIGHT", new_y="TOP") pdf.set_font('helvetica', '', 8) if is_failed: pdf.set_text_color(185, 28, 28) elif is_empty: pdf.set_text_color(146, 64, 14) else: pdf.set_text_color(71, 85, 105) pdf.cell(0, 6, clean_txt(desc_text), new_x="LMARGIN", new_y="NEXT") pdf.ln(5) pdf.set_font('helvetica', 'I', 8) pdf.set_text_color(100, 116, 139) pdf.multi_cell(190, 4.5, clean_txt( "Este informe fue generado automaticamente por CrowData Intelligence consultando fuentes " "publicas y abiertas del Estado Argentino. Los datos presentados tienen caracter informativo " "y deben ser verificados ante cada organismo competente. CrowData no se responsabiliza " "por inexactitudes en las fuentes oficiales.\n" f"Generado el: {generated_at or datetime.datetime.now().strftime('%d/%m/%Y %H:%M')} - " "CrowData (c) 2025 - Todos los derechos reservados." )) return pdf.output()