jueves, 28 de marzo de 2019

VBA: Rellenar un formulario PDF desde Excel

Hace un par de semanas un lector preguntaba por la manera de completar varios formularios guardados en pdf desde una base de datos en una hoja de cálculo...
Hoy veremos un procedimiento bastante sencillo si tenemos una versión de Adobe Acrobat (no Reader!!).


Obviamente tenemos nuestro formulario creado, a modo de ejemplo con cinco campos:
'Nombre'
'Email'
'Fecha registro'
'Ciudad origen'
y 'Núm expediente'

VBA: Rellenar un formulario PDF desde Excel



Tener identificado los nombres de los diferentes campos del formulario es fundamental.. ya que la macro los identificará para su cumplimentación...

Por otra parte, en la hoja de cálculo tenemos una tabla 'TblDatos' con la información a trasladar

VBA: Rellenar un formulario PDF desde Excel



Insertamos un módulo estándar, y en esta ventana de código añadiremos nuestra macro:

Sub RellenarFormularioPDF()
'OJO!! solo funciona con Adobe ACROBAT, NO con Reader !!!

'Declaramos las variables
Dim stRutaPDF As String, nombreForm As String, stRutaCompletaPDF As String
Dim stRutaGuardadoPDF As String

Dim objAcrobatApp As Object, objAcrobatAVDoc As Object, objAcrobatPDDoc As Object
Dim objJSO As Object
    
Application.ScreenUpdating = False
'la ruta del formulario a completar
stRutaPDF = "E:\excelforo\"
nombreForm = "FormWord_PDF.pdf"
stRutaCompletaPDF = stRutaPDF & nombreForm

'Recorremos los registros en la tabla
For Each dato In Range("TblDatos[Nombre]")
    On Error Resume Next        'controlamos posibles errores al crear el objeto Acrobat o el documento pdf
    'Iniciamos al aplicación Acrobat
    Set objAcrobatApp = CreateObject("AcroExch.App")
    'Creamos el objeto AVDoc (el documento pdf)
    Set objAcrobatAVDoc = CreateObject("AcroExch.AVDoc")
    
        'Comprobamos si el objeto Acrobat ya existiera o generase algún tipo de error...
    If Err.Number <> 0 Then
        MsgBox "No podemos crear el objeto Acrobat", vbCritical
        'liberamos memoria antes de salir del procedimiento.
        Set objAcrobatApp = Nothing: Set objAcrobatAVDoc = Nothing
        Exit Sub
    End If
    On Error GoTo 0             'damos por acabado posibles errores
    
    'Abrimos el pdf con el formulario a rellenar.
    If objAcrobatAVDoc.Open(stRutaCompletaPDF, "") = True Then
        'Declaramos el obejto PDDoc
        Set objAcrobatPDDoc = objAcrobatAVDoc.GetPDDoc
        'declaramos el Objeto Java Script
        Set objJSO = objAcrobatPDDoc.GetJSObject
        
        On Error Resume Next
        'Comenzamos a rellenar los campos del formulario para cada registro
        'recuperamos el dato de la tabla de la hoja de cálculo...
        'los pasamos como texto/string para evitar fallos.
        objJSO.GetField("Nombre").Value = CStr(dato.Value)
        objJSO.GetField("Email").Value = CStr(dato.Offset(0, 1).Value)
        objJSO.GetField("Fecha registro").Value = CStr(dato.Offset(0, 2).Value)
        objJSO.GetField("Ciudad origen").Value = CStr(dato.Offset(0, 3).Value)
        objJSO.GetField("Núm expediente").Value = CStr(dato.Offset(0, 4).Value)
            
        If Err.Number <> 0 Then
            'si aparece algún error, cerramos el doc sin guardar y salimos de Acrobat!!!
            objAcrobatAVDoc.Close True
            objAcrobatApp.Exit
            
            'liberamos memoria y salimos
            Set objJSO = Nothing
            Set objAcrobatPDDoc = Nothing: Set objAcrobatAVDoc = Nothing
            Set objAcrobatApp = Nothing
            Exit Sub
        End If
        On Error GoTo 0
        
        'Generamos la ruta de lo spdf guardados
        stRutaGuardadoPDF = stRutaPDF & dato & ".pdf"
                
        'Guardamos el formulario como pdf y un nombre personalizado
        objAcrobatPDDoc.Save 1, stRutaGuardadoPDF

        'cerramos el formualrio pdf sin guardar cambios
        objAcrobatAVDoc.Close True
        
        'y cerramos la aplicación (Acrobat) para finalizar
        objAcrobatApp.Exit
           
        'liberamos memoria
        Set objJSO = Nothing
        Set objAcrobatPDDoc = Nothing: Set objAcrobatAVDoc = Nothing: Set objAcrobatApp = Nothing
        
    Else
        
        MsgBox "No podemos abrir el fichero...", vbCritical, "Algo ocurre con el fichero o su ruta."
        
        'Cerramos Acrobat
        objAcrobatApp.Exit
        
        'liberamos memoria
        Set objAcrobatAVDoc = Nothing: Set objAcrobatApp = Nothing
        Exit Sub
        
    End If
Next dato

Application.ScreenUpdating = True

'mensaje final de completado.
MsgBox "Todos los formularios se han rellenado correctamente!", vbInformation, "ok... proceso acabado"
    
End Sub



Tras ejecutar la macro sinningún error podemos comprobar cómo se han generado los distintos pdf con los datos rellenos...

VBA: Rellenar un formulario PDF desde Excel



lunes, 25 de marzo de 2019

Power Query: Agrupar y Extraer valores

Volvemos con la herramienta de moda por su alto rendimiento: Power Query.
Tiempo atrás una lectora consultaba por la maneara de listar de una manera concreta datos a partir de un listado.
La idea es la que se ve en la siguiente imagen:

Power Query: Agrupar y Extraer valores



El objetivo es claro, pasar de un listado vertical de países y ciudades a un listado de filas únicas por país, mostrando en su misma fila, las diferentes ciudades de éste.

Como siempre comenzaremos cargando a Power Query (Obtener y Transformar) la tabla principal 'TblPaises')

Power Query: Agrupar y Extraer valores



El siguiente paso a incorporar a nuestra consulta es fundamental. Procederemos a Agrupar por... en su modo avanzado:

Power Query: Agrupar y Extraer valores


la agrupación se ha basado en los elementos del campo 'País', acumulando 'Todos las filas' del campo de la 'Ciudad'.
Y acabamos acumulando, por Suma, el número de habitantes de cada Ciudad...


Esto deja nuestra consulta como sigue:

Power Query: Agrupar y Extraer valores


Comprueba como el campo nuevo (que hemos llamado 'ciudades' en la anterior agrupación) muestra a modo de tabla el resultado...
Esto lo vamos a personalizar, y en la barra de fórmula (por ejemplo... también en el editor avanzado) cambiaremos:
each _, type table
por
each [Ciudad]
como sigue...
= Table.Group(#"Changed Type", {"País"}, {{"ciudades", each _, type table}, 
   {"Hab total", each List.Sum([Habitantes]), type number}})
por
= Table.Group(#"Changed Type", {"País"}, {{"ciudades", each [Ciudad]}, 
   {"Hab total", each List.Sum([Habitantes]), type number}})

Este cambio provoca que el campo nuevo 'ciudades' se muestre como Lista y no como Tabla!!

Power Query: Agrupar y Extraer valores



El último paso es fácil... basta hacer clic sobre el botón de expandir sobre ese campo 'ciudades' y elegir la segunda opción: Extraer valores...

Power Query: Agrupar y Extraer valores


Configuramos en qué forma queremos extraer los valores eligiendo el separador en ventana siguiente y aceptamos

Power Query: Agrupar y Extraer valores



Y una vez finalizado el proceso ya podemos Cargar y cerrar en... nuestra hoja de cálculo con el resultado del inicio del post, tal como queríamos conseguir.

jueves, 21 de marzo de 2019

Columnas de gráfico ordenadas en descendente

Hace algún tiempo un lector preguntaba por la forma de conseguir en un gráfico de columnas tener los valores de distintas series ordenadas en descendente para cada punto del gráfico (en el ejemplo para cada mes)... Como se ve en la imagen siguiente:

Columnas de gráfico ordenadas en descendente



Como tantas veces para conseguir nuestra meta construiremos un rango auxiliar donde ordenaremos en descendente, de mayor a menor, los valores de cada país por mes.
Así en la celda I3 añadimos la siguiente fórmula:
=K.ESIMO.MAYOR($C3:$F3;COLUMNAS($I2:I2))
que arrastraremos hasta L8.
Esto nos devuelve en el orden deseado cada mes...

Columnas de gráfico ordenadas en descendente



En el siguiente paso construiremos un nuevo rango auxiliar que nos retornará los títulos de los países de acuerdo a la ordenación descendente.
Así en la celda N3 añadimos la fórmula:
=INDICE($C$2:$F$2;1;COINCIDIR(I3;$C3:$F3;0))
y arrastramos hasta Q8.

Columnas de gráfico ordenadas en descendente



Ya casi estamos...
Construimos nuestro gráfico de columnas.
Seleccionamos el rango H3:L8 e insertamos el gráfico de columnas agrupadas en 2D.

Columnas de gráfico ordenadas en descendente


En este punto he optado por dar formato de color a las diferentes columnas/países de cada punto, esto es, a cada mes... serie por serie he dado formato de color de relleno, y en mi caso me he decidido por el mismo color.

Columnas de gráfico ordenadas en descendente



Para otros casos habría que ir pasando individualmente punto por punto de cada serie...

Último paso, añadir los países como etiquetas de datos..
Para esto recorreremos cada una de las cuatro series de valores y asignaremos Valor de las celdas personalizadas como etiquetas de datos.
Seleccionaremos la primera y haremos clic en Agregar Etiquetas de datos y encima.
A continuación seleccionaremos las recién añadidas etiquetas y clic derecho del ratón sobre ellas para Dar formato a etiquetas de datos, lo que nos abrirá el Panel de formato de etiqueta de datos (normalmente a la derecha de nuestra pantalla).

Columnas de gráfico ordenadas en descendente



Marcaremos solo la opción de Valor de las celdas y para la primera serie seleccionaremos el rango N3:N8

Columnas de gráfico ordenadas en descendente


Repetiremos la misma acción para las tres series restantes, seleccionando respectivamente los rangos de celdas
O3:O8
P3:P8
y
Q3:Q8


Llegando a si al final de nuestro trabajo, y con el objetivo cumplido: Disponer nuedtros países ordenados en descendente para cada mes

Columnas de gráfico ordenadas en descendente

martes, 19 de marzo de 2019

Power Query: Conexión a base de datos MySQL

Hoy aprenderemos a acceder a una base de datos en SQL, en particular una base de datos MySQL.

Obviamente debemos conocer la ubicación de nuestra base de datos (nombre del servidor o IP) así como el nombre de la base de datos.

En mi caso para el ejemplo la base de datos llamada 'bbdd_excelforo' se encuentra en mi local, servidor: 'localhost'.

No necesitamos nada más...


Para acceder a esta base de datos desde Power Query (Obtener y transformar) iremos a la ficha Datos > grupo Obtener y transformar > desplegable Nueva consulta > Desde una base de datos > opción Desde una base de datos de MYSQL

Power Query: Conexión a base de datos MySQL



Se puede observar que existen diferentes posibilidades de acceso a distintos tipos de base de datos (basadas en SQL Server, Oracle, Access, PostgreSQL, IBM ...)

En todos los casos es el asistente de conexión de Power Query quien gestiona la relación... visualmente, para el usuario, todos los casos son muy similares.


Para nuestro ejemplo de acceso a MySQL, tras la selección anterior se nos preguntará por el nombre del servidor y nombre de la base de datos:

Power Query: Conexión a base de datos MySQL



Tras aceptar veremos una nueva ventana con las tablas, consulta y vistas de la base de datos...

Power Query: Conexión a base de datos MySQL


En mi ejemplo me interesa trabajar sobre las dos únicas tablas que componen mi base de datos 'bbdd_excelforo', por lo que marco la opción: Seleccionar varios elementos, para marcar después ambas tablas...

El paso siguiente consiste en Editar la nueva consulta de Power Query.
Esto nos lleva al Editor de Power Query donde ejecutar todo tipo de acciones como con cualquier consulta....

Power Query: Conexión a base de datos MySQL



Y listo...

Sin duda una forma muy ágil y simple de conectar a base de datos tipo 'SQL'.

jueves, 14 de marzo de 2019

Pasar fecha de yyyymmdd a dd/mm/yyyy

Un clásico que tenía hace tiempo en los pendiente...
Convertir una 'fecha' dada como 'yyyymmdd' a fecha estándar 'dd/mm/yyyy'.

Es un problema bastante frecuente ya que muchos sistemas devuelven las fechas como valores numéricos no tratables directamente...

Pasar fecha de yyyymmdd a dd/mm/yyyy



Veamos tres posibles fórmulas para la conversión.
=FECHANUMERO(TEXTO(B3;"0000\/00\/00"))

La función TEXTO con ese formato de barras opuestas \/ separa los dígitos como corresponde 4+2+2... (año + mes + día), para que luego la función FECHANUMERO pase la fecha como texto a fecha real...

La segunda función es casi idéntica
=--TEXTO(B3;"0000\/00\/00")

Misma función TEXTO.. pero esta vez el encargado de devolver la fecha como tal es el doble signo menos.

La última es una clásica muy laboriosa.... que consiste en extraer los caracteres por posiciones de año, mes y día:
=FECHA(IZQUIERDA(B3;4);EXTRAE(B3;5;2);DERECHA(B3;2))


Existe una alternativa a las funciones que es el empleo del Asistente de Texto en columnas.
Si seleccionamos la/s fecha/s con forma 'yyyymmdd' y desde la ficha de Datos > grupo Herramienta de datos > botón Texto en columnas, en el tercer paso del asistente seleccionaremos formato Fecha: AMD y aceptaremos...

Pasar fecha de yyyymmdd a dd/mm/yyyy

martes, 12 de marzo de 2019

VBA: Extraer elementos de las diagonales de una matriz

Hoy construiremos una macro para extraer los distintos elementos que componen la diagonal de un elemento seleccionado en una matriz...
Se trata de dar una posible solución a un lector que preguntaba:
[...]Tengo una matriz en Excel de 5x5
por favor me ayudan con una forma de extraer todas las diagonales y saber cuales son los números que le corresponden cada ubicación.
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25

Hay unas particularidades con los números 3, 6, 13, 19
las casillas que le corresponden a las diagonales del 3 son ( 3, 7,11,9 y 15)
las casillas que le corresponden a las diagonales del 6 son ( 6,2,12,18, y 24)
las casillas que le corresponden a las diagonales del 13 son ( 1,7,13,19, y 25)
las casillas que le corresponden a las diagonales del 19 son ( 1,7,13,19, 25,23 y 15)[...]

VBA: Extraer elementos de las diagonales de una matriz



El funcionamiento será simple, sobre una matriz de cualquier dimensión (en el ejemplo 5x5), seleccionamos un único elemento... y tras presionar el botón que ejecuta la macro, obtendremos todos los elementos de las diagonales
de dicho elemento.


Insertamos un módulo estándar, y en esta ventana de código añadiremos nuestra macro:

Sub Diagonales()
'www.excelforo.com
Dim myRange As Range
Set myRange = Selection.CurrentRegion

Dim iniFil As Long, iniCol As Long, finFila As Long, finCol As Long, i As Long
iniFil = myRange.Row
iniCol = myRange.Column
finFila = myRange.Rows.Count + iniFil - 1
finCol = myRange.Columns.Count + iniCol - 1

Dim numFil As Long, numCol As Long
'contamos número de filas y columnas de la matriz
numFil = myRange.Rows.Count
numCol = myRange.Columns.Count

Dim celdaIni As Range
Dim fil As Long, col As Long
'una sola celda seleccionada dentro de la matriz de datos!!
Set celdaIni = Selection
fil = celdaIni.Row - iniFil + 1
col = celdaIni.Column - iniCol + 1

Dim delta As Long

Dim elementos() As Long
Dim columna As Long, fila As Long   'para identificar los elementos que cumplan..
Dim n As Long
n = 0
'recorremos cada elemento de la matriz
'por fila y luego por columna
For f = 1 To numFil
        For c = 1 To numCol
            delta = col - fil   'valor de ajuste según posición inicial en la matriz
            'me compara con la posición de la celda seleccionada
            If c = (fil - Abs(fil - f) + delta) Or c = (fil + Abs(fil - f) + delta) Then
                n = n + 1
                'añadimos elemento que pertenezca a su diagonal
                ReDim Preserve elementos(n)
                elementos(n) = myRange.Cells(f, c).Value
            End If
        Next c
Next f

'llevamos los elementos a la hoja de cálculo...
Range("J1:J99").Clear   'Limpia el destino...
For i = 1 To n
    Range("J1").Offset(i - 1, 0).Value = elementos(i)
Next i

End Sub



El algoritmo es simple... de especial relevancia el empleo de ReDim Preserve para cargar los elementos de las diagonales... un número de elementos a priori desconocido.

jueves, 7 de marzo de 2019

VBA: Crear tareas en Outlook desde Excel

El trabajo de hoy consiste en generar tareas de Outlook desde nuestro Excel, pero además la crearemos en la subcarpeta de tareas que queramos (en lugar de la carpeta por defecto).

En mi caso he creado en mi Outlook, una carpeta dentro de las tareas que he llamado 'Prueba':

VBA: Crear tareas en Outlook desde Excel


Notemos como de momento no existe ninguna tarea...

Comenzaremos insertando un módulo estándar en nuestro proyecto de VB:

Sub CrearTareaOutlook()
'OJO Añadir la referencia 'Microsoft Outlook 16.0 Object Library'
On Error GoTo Trata_Error     'control de errores genérico...
Dim olApp As Object, objNS as object, defaultTasksFolder as object, subFolder as object, OlTask As Object

'Iniciamos MS Outlook... si no estuviera ya abierto
On Error Resume Next
Set olApp = GetObject(, "Outlook.Application")
If Err.Number <> 0 Then 'si no está abierto
  Set olApp = CreateObject("Outlook.Application")
End If
On Error GoTo 0

'las siguientes líneas permiten añadir la nueva tarea
'en la carpeta que queramos....
Set objNS = olApp.GetNamespace("MAPI")
Set defaultTasksFolder = objNS.GetDefaultFolder(olFolderTasks)
Set subFolder = defaultTasksFolder.Folders("Prueba") 'Reemplaza 'Prueba' con el nombre de la subcarpeta que quieras
'definimos el objeto Tarea dentro de la Subcarpeta
Set OlTask = subFolder.Items.Add(olTaskItem)

'y asignamos valores a las propiedades de la Tarea
With OlTask
    .Display    'la mostramos / hacemos visible (si así lo queremos)
    
    .Subject = "Cargado desde Excel"         'asunto
    .StartDate = "01/02/2019"    'Fecha de inico: dd/mm/yyyy
    .DueDate = "11/02/2019"              'Fecha de vencimiento
    .Status = 3         '0=no comenzada, 1=en curso, 2=completada, 3=a la espera de otra persona, 4=aplazada
    .Importance = 2     '0=baja, 1=normal, 2=alta (Prioridad)
    .ReminderSet = True 'check de Aviso True/False
    .ReminderTime = "05/02/2019 12:30"  'fecha de aviso
    .Categories = "Categoría roja"    'Categoria
    .PercentComplete = 13   '% completado
    .Body = "Prueba de crear tarea desde una macro de Excel"    'Cuerpo de la tarea...
    
    .Save       'se guarda la tarea
    .Close (olPromptForSave)     'cerramos la tarea
End With


Trata_Salida:
    On Error Resume Next
    Set OlTask = Nothing
    Set subFolder = Nothing
    Set defaultTasksFolder = Nothing
    Set objNS = Nothing
    Set olApp = Nothing
    Exit Sub

Trata_Error:
    MsgBox "Ha ocurrido el siguiente error" & vbCrLf & _
    "Número error: " & Err.Number & vbCrLf & _
    "Descripción del error: " & Err.Description, _
    vbCritical, "FALLOOOOOOO!"
    Resume Trata_Salida
End Sub



Tras ejecutar la macro comprobamos la carpeta de tareas de Outlook

VBA: Crear tareas en Outlook desde Excel



Si editamos la tarea comprobaremos los datos cargados desde la macro:

VBA: Crear tareas en Outlook desde Excel

martes, 5 de marzo de 2019

Power Query: List.Transform y List.Accumulate alternativas a List.Sum

Tiempo atrás escribí sobre cómo obtener un 'Running total' (acumulado creciente) sobre una tabla (ver aquí), donde empleamos una función M de Power Query: List.Sum.

Hoy nos adentraremos un poco más en este lenguaje y descubriremos algunas 'joyas' de esta programación:
List.Buffer(lista as list)
Carga la lista en la memoria. El resultado de esta llamada es una lista estable, lo que significa que tendrá un recuento determinado y un orden de los elementos.

List.Transform(lista as list, transformación as function)
Aplica la función/transformación en cada elemento de la lista y devuelve una nueva lista transformada.

List.Accumulate(lista as list, valor_incial as any, acumulador as function)
Acumula el resultado a partir de los valores de la lista dada; i.e., a partir del valor inicial, esta función aplica la función del acumulador y devuelve el resultado final acumulado.


En su momento empleamos la función List.Sum efectiva y válida cuando hay 'pocos' elementos sobre los que trabajar.. pero en ocasiones nuestras tablas contienen un número elevado de registros y List.Sum se hace 'pesado' y 'lento'.
Será en estos casos cuando podremos emplear las alternativas comentadas...


Partiremos de una tabla sencilla con cuatro campos: Fecha / Concepto / Debe / Haber

Power Query: List.Transform y List.Accumulate alternativas a List.Sum



Trabajaremos de dos maneras.
Primero usando List.Transform y List.Buffer.

Cargaremos la Tabla al Editor de Power Query y en el editor avanzado añadiremos el siguiente código comentado:
let
    //carga los datos de la Tabla 'TblMov'
    Origen = Excel.CurrentWorkbook(){[Name="TblMov"]}[Content],
    //obtenemos el cálculo del saldo como diferencia de Debe-haber
    colSaldo = Table.AddColumn(Origen, "Saldo", each (if [Debe] = "" then 0 else [Debe]) - (if [Haber] = "" then 0 else [Haber])),
    //añadimos un columna de índice
    colIndice = Table.AddIndexColumn(colSaldo,"TempIndex"), 
    
    //recuperamos(duplicamos el dato de la columna de Saldo
    Input = colIndice[Saldo],
    //generamos una lista nueva para cada registro de la tabla, 
    //calculada como la suma del saldo del registro más el dato del registro anterior del campo que estamos creando
    //y la cargamos en memoria con List.Buffer
    Output = List.Buffer(List.Transform({0..List.Count(Input)-1}, each Input{_} + (if _ = 0 then 0 else @Output{_-1}))),

    //recuperamos de la memoria el dato acumulado
    colAcumulado = Table.AddColumn(colIndice, "Acumulado", each Output{[TempIndex]})
in
    colAcumulado


Power Query: List.Transform y List.Accumulate alternativas a List.Sum


Con lo que obtendremos es siguiente resultado esperado...

Power Query: List.Transform y List.Accumulate alternativas a List.Sum



Segunda forma usando List.Accumulate.
Añadiremos una consulta en blanco y desde el editor avanzado añadiremos el siguiente código comentado:
let
    //cargamos la tabla desde la hoja de cálculo
    Origen = Excel.CurrentWorkbook(){[Name="TblMov"]}[Content],
    //calculamos el Saldo para cada registro como diferencia de Debe-Haber
    colSaldo = Table.AddColumn(Origen, "Saldo", each (if [Debe] = "" then 0 else [Debe]) - (if [Haber] = "" then 0 else [Haber])),
    //añadimos una columna de índice
    colIndice = Table.AddIndexColumn(colSaldo,"TempIndex"), 
    
    //añadimos una columna personalizada con el cálculo Acumulado requerido
    //List.Accumulate acumula el rango variable obtenido por List.Range
    Fin = Table.AddColumn(colIndice, "Acum", 
                    each List.Accumulate(
                        List.Range(colIndice[Saldo],0,[TempIndex]+1),
                        0,
                        (state, current) => state + current
                    ))
in
    Fin


Power Query: List.Transform y List.Accumulate alternativas a List.Sum


A partir de lo que obtenemos...

Power Query: List.Transform y List.Accumulate alternativas a List.Sum



Consiguiendo nuestra meta por dos nuevos caminos...