viernes, 30 de diciembre de 2011

Navegando Datos Usando Arboles

En esta entrada vamos a ver como introducir un control de tipo árbol en las paginas de Peoplesoft. Anteriormente para cumplir este requerimiento el Application Designer proveía un control llamado tree. En la últimas versiones de las PeopleTools se recomienda no usar más este control, en cambio Peoplecode posee una función llamada GenerateTree que permite cargar los árboles en un control de tipo HTML Area. 



En los peoplebooks se encuentra un ejemplo de como cargar los árboles creados por el gestor de árboles en una página creada por nosotros. La ruta del ejemplo en los PeopleBooks es Home > PeopleBooks > Enterprise PeopleTools 8.51 PeopleBook: PeopleCode Developer's Guide > Using HTML Trees and the GenerateTree Function.

El ejemplo que vamos a describir en esta entrada nos permitirá crear un árbol a partir de cualquier estructura jerárquica que se cree dentro de peoplesoft.  

El primer paso es crear nuestra estructura jerarquica,  para eso creamos un componente que permita insertar los empleados de una organización de tal forma que estén relacionados con su jefe. 

A continuación podemos ver la estructura del registro PE_TREE_EMPLE que vamos a utilizar para ingresar la estructura jerárquica de los empleados.



Luego creamos la página PE_TREE_EMPLEADOS para administrar los datos del registro PE_TREE_EMPLE.  La página debe verse como se muestra a continuación



Luego creamos un componente con registro de búsqueda INSTALLATION y un menú que nos permita registrar el componente dentro del portal.

En nuestro nuevo componente ingresamos unos datos de prueba como se ven en la ilustración:





El siguiente paso es crear un Application Package que nos permita administrar los datos ingresado por el componente PE_TREE_EMPLEADOS en forma de nodos de un árbol.  Para lo anterior crearemos la clase es pe_nodoEmpleado dentro del paquete PE_TREE_EMPLEADOS. Está clase nos permitirá manejar como un nodo cada uno de los registros ingresados por el componente PE_TREE_EMPLEADOS.  La clase también poseerá un arreglo de objetos de está misma clase que representará los hijos del nodo.


A continuación podemos ver el código completo de la clase descrito línea por línea.

/*Lo primero es declarar nuestra clase*/


class pe_nodoEmpleado

/*El método constructor recibirá compo parámetros el nombre del nodo (En nuestro caso el nombre del nodo será el id del empleado que se creo), la descripción (El nombre del empleado que se creo) y un indicador que indica si el nodo raíz del árbol o no*/

   method pe_nodoEmpleado(&str_nombreNodo_par As string, &str_descNodo_par As string, &bol_nodoRaiz_par As boolean);

/*El siguiente método permite cargar los hijos del nodo actual en un arreglo */
method pe_cargarHijos();

/*El método getNodo permite retornar un nodo que se encuentre dentro de la estructura de nodos que se está procesando. */

   method pe_getNodo(&str_nombreNodo_par As string) Returns PE_TREE_EMPLEADOS:pe_nodoEmpleado;

/*El método getNodo permite retornar un nodo que se encuentre dentro de la estructura de nodos que se está procesando. */

   method pe_getNodoNumber(&nbr_idNodo As number) Returns PE_TREE_EMPLEADOS:pe_nodoEmpleado;

/*Método que retorna el total de hijos del nodo*/
   method pe_totalHijos() Returns number;

/*Propiedad para consultar el nombre del nodo*/
   property string NOMBRE_NODO get;

/*Propiedad para consultar la descripción del nodo*/


   property string DESCR_NODO get;

/*Propiedad que indica si ya fueron cargados los nodos hijos*/




   property boolean CARGO_HIJOS get;


/*Propiedad que indica si el nodo tiene hijos o no*/


   property boolean TIENE_HIJOS get;

/*Propiedad que indica si el nodo es el nodo raíz de la estructura */
   property boolean NODO_RAIZ get;

private

/*variable para almacenar el nombre del nodo*/
   instance string &str_nombreNodo;

/*variable para almacenar la descripción del nodo*/
   instance string &str_descNodo;

/*variable para almacenar si ya se cargaron los hijos*/
   instance boolean &bol_cargoHijos;

/*variable para almacenar si el nodo es el nodo raíz*/
   instance boolean &bol_nodoRaiz;


/*variable para almacenar el listado de nodos hijos del nodo actual*/
   instance array of PE_TREE_EMPLEADOS:pe_nodoEmpleado &obj_hijos;
end-class;


method pe_nodoEmpleado
   /+ &str_nombreNodo_par as String, +/
   /+ &str_descNodo_par as String, +/
   /+ &bol_nodoRaiz_par as Boolean +/

/*Se inicializan las variables privadas de la clase*/
   &str_nombreNodo = &str_nombreNodo_par;
   &str_descNodo = &str_descNodo_par;
   &bol_cargoHijos = False;
   &bol_nodoRaiz = &bol_nodoRaiz_par;
   
end-method;

method pe_cargarHijos


/*Se inicializa el arreglo &obj_hijos en el que se almacenarán los nodos hijos del nodo actual*/
   Local PE_TREE_EMPLEADOS:pe_nodoEmpleado &obj_hijo = create PE_TREE_EMPLEADOS:pe_nodoEmpleado(" ", " ", False);
   Local string &str_cod, &str_nombre;
   Local SQL &sql_hijos;
   &obj_hijos = CreateArrayRept(&obj_hijo, 0);


/*Si el nodo actual está marcado como el nodo raíz entonces se cargan todos los empleados que no tengan ningún jefe*/
   If &bol_nodoRaiz Then
      &sql_hijos = CreateSQL("SELECT PE_CODIGO_EMPL, PE_NOMBRE_EMPLEADO FROM PS_PE_TREE_EMPLE WHERE PE_CODIGO_JEFE = ' '");
   Else


/*Si el nodo actual no es el nodo raíz se cargan todos los empleados cuyo jefe sea el nodo actual*/
      &sql_hijos = CreateSQL("SELECT PE_CODIGO_EMPL, PE_NOMBRE_EMPLEADO FROM PS_PE_TREE_EMPLE WHERE PE_CODIGO_JEFE = :1", &str_nombreNodo);
   End-If;
 
/*Para cada uno de los empleados hijos del nodo actual,  se creará un nodo y se agrega al arreglo de nodos hijos del nodo actual*/

   While &sql_hijos.Fetch(&str_cod, &str_nombre)
      &obj_hijo = create PE_TREE_EMPLEADOS:pe_nodoEmpleado(&str_cod, &str_nombre, False);
      &obj_hijos.Push(&obj_hijo);
   End-While;
 
/*Se marca el nodo para que indique que ya fueron cargados los nodos*/
   &bol_cargoHijos = True;
end-method;

get NOMBRE_NODO
   /+ Returns String +/
   Return &str_nombreNodo;
end-get;

get DESCR_NODO
   /+ Returns String +/
   Return &str_descNodo;
end-get;

get CARGO_HIJOS
   /+ Returns Boolean +/;
   Return &bol_cargoHijos;
end-get;



get TIENE_HIJOS
   /+ Returns Boolean +/;
   Local number &nbr_hijos = 0;
 
/*si ya fueron cargados los hijos se valida que el arreglo de nodos hijos tenga hijos*/
   If &bol_cargoHijos Then
      If &obj_hijos.Len = 0 Then
         Return False;
      Else
         Return True;
      End-If;
   Else


/*Si no han sido cargados los hijos se consulta la tabla de empleados para verificar si tienes nodos hijos*/
      If &bol_nodoRaiz Then
         SQLExec("select count(1) from ps_PE_TREE_EMPLE where PE_CODIGO_JEFE = ' '", &nbr_hijos);
      Else
         SQLExec("select count(1) from ps_PE_TREE_EMPLE where PE_CODIGO_JEFE = :1", &str_nombreNodo, &nbr_hijos);
      End-If;
      If &nbr_hijos = 0 Then
         Return False;
      Else
         Return True;
      End-If;
   End-If;
   
end-get;

get NODO_RAIZ
   /+ Returns Boolean +/
   Return &bol_nodoRaiz
end-get;

method pe_getNodo
   /+ &str_nombreNodo_par as String +/
   /+ Returns PE_TREE_EMPLEADOS:pe_nodoEmpleado +/
   Local number &nbr_nodos;
   Local PE_TREE_EMPLEADOS:pe_nodoEmpleado &obj_hijo;


/*Retorna el nodo buscado,  si es el actual, si es algún nodo hijo o si es algún nodo hijo de los nodos hijos del nodo actual*/
   If &str_nombreNodo_par = &str_nombreNodo Then
      Return %This;
   Else
      If &bol_cargoHijos Then
         For &nbr_nodos = 1 To &obj_hijos.Len
            If &str_nombreNodo_par = &obj_hijos [&nbr_nodos].NOMBRE_NODO Then
               Return &obj_hijos [&nbr_nodos];
            Else
               &obj_hijo = &obj_hijos [&nbr_nodos].pe_getNodo(&str_nombreNodo_par);
               If &obj_hijo <> Null Then
                  Return &obj_hijo;
               End-If;
            End-If;
         End-For;
         Return Null
      Else
         Return Null;
      End-If;
   End-If;
   
end-method;

method pe_getNodoNumber
   /+ &nbr_idNodo as Number +/
   /+ Returns PE_TREE_EMPLEADOS:pe_nodoEmpleado +/
   Return &obj_hijos [&nbr_idNodo];
end-method;

method pe_totalHijos
   /+ Returns Number +/
   Return &obj_hijos.Len;
end-method;


Con algunas modificaciones a la clase anterior se puede tener una clase que administre cualquier tabla que almacene una estructura jerárquica. 

La función GenerateTree funciona utilizando 2 records,  uno que almacena los parámetros de generación del árbol y otro que contiene cada uno de los nodos que pertenecen al árbol. El primer registro lo llamaremos PE_TREE_HDR y se debe crear utilizando el subrecord TREECTL_HDR_SBR y el segundo registro los llamaremos PE_TREE_NDE y se debe crear utilizando el subrecord TREECTL_NDE_SBR.   

El siguiente paso en nuestro ejemplo es crear un registro derived que almacene el campo tipo LONG que tendrá el código HTML que generará la función GenerateTree y el campo Char(50) que controlará los eventos que se generen en el HTML creado por la función GenerateTree. 

En la siguiente imagen se puede ver la estructura de los 4 registro que estamos utilizando en nuestro proyecto. 



El siguiente paso es crear la página PE_TREE_EXAMPLE en la cual mostraremos el árbol de empleados.  A la página se le debe agregar un objeto HTMLAREA asociado al campo HTML_AREA_01 del registro PE_TREE_EXP.  También se debe agregar un Editbox asociado al campo PE_EVENT_FIELD del registro PE_TREE_EXP que debe ser invisible, debe tener marcada la propiedad Modifiable by JavaScript y en la propiedad Page Field Name debe tener PE_EVENT_FIELD.

La página debemos agregarla a un componente cuyo registro de búsqueda sea INSTALLATION.  El componente lo llamamos PE_TREE_EXAMPLE y se debe registrar en el portal para poder acceder a la página.

En el evento PostBuild del componente debemos agregar el código que inicializa nuestro árbol. A continuación se lista el código que debe ir en el postbuild:

/*Se importa la clase que creamos para administrar los nodos del árbol de empleados*/

import PE_TREE_EMPLEADOS:pe_nodoEmpleado;


/*Declaramos la variable que tendrá el nodo raíz de nuestro árbol*/
Component PE_TREE_EMPLEADOS:pe_nodoEmpleado &obj_nodos;


/*Declaramos el rowset que contendrá la estructura que se utilizará para ejecutar la función GenerateTree*/
Component Rowset &TREECTL;


&NODE_ROWSET = CreateRowset(Record.PE_TREE_NDE);
&TREECTL = CreateRowset(Record.PE_TREE_HDR, &NODE_ROWSET);


&TREECTL.InsertRow(1);
&REC = &TREECTL.GetRow(2).GetRecord(1);


/* Set the HDR options: 

/*Asignamos el nombre de la página en la que va a ser mostrado el árbol*/
&REC.GetField(Field.PAGE_NAME).Value = "PE_TREE_EXAMPLE";


/*Asignamos el nombre del campo que usaremos para controlar los eventos generados en el árbol*/
&REC.GetField(Field.PAGE_FIELD_NAME).Value = "PE_EVENT_FIELD";


/*Los siguientes campos se asignan por defecto*/
&REC.GetField(Field.PAGE_SIZE).Value = 15;
&REC.GetField(Field.DISPLAY_LEVELS).Value = 8;
&REC.GetField(Field.COLLAPSED_IMAGE).Value = "PT_TREE_COLLAPSED";
&REC.GetField(Field.EXPANDED_IMAGE).Value = "PT_TREE_EXPANDED";
&REC.GetField(Field.END_NODE_IMAGE).Value = "PT_TREE_END_NODE";
&REC.GetField(Field.LEAF_IMAGE).Value = "PT_TREE_LEAF";
&REC.GetField(Field.IMAGE_WIDTH).Value = 15;
&REC.GetField(Field.IMAGE_HEIGHT).Value = 12;
&REC.GetField(Field.INDENT_PIXELS).Value = 20;


/*Se crea el nodo raíz de nuestro árbol de empleados*/
&obj_nodos = create PE_TREE_EMPLEADOS:pe_nodoEmpleado("Empleados", "Todos los Empleados", True);


/*Si el nodo tiene hijos en la variable &PARENT_FLAG X que indica que el nodo tiene hijos y no estos no se han desplegado*/
If &obj_nodos.TIENE_HIJOS Then
   &PARENT_FLAG = "X";
Else
   &PARENT_FLAG = "Y";
End-If;


&NODE_ROWSET = &TREECTL.GetRow(2).GetRowset(1);
&NODE_ROWSET.InsertRow(1);
&REC = &NODE_ROWSET.GetRow(2).GetRecord(1);


/*Se le indica al nodo que no es una hoja*/
&REC.GetField(Field.LEAF_FLAG).Value = "N";


/*Se asigna el nombre del nodo*/
&REC.GetField(Field.TREE_NODE).Value = "Empleados";


/*Se asigna la descripción del nodo*/
&REC.GetField(Field.DESCR).Value = "Todos los Empleados";


/*Se asignan los siguiente valores por defecto*/
&REC.GetField(Field.RANGE_FROM).Value = "";
&REC.GetField(Field.RANGE_TO).Value = "";
&REC.GetField(Field.DYNAMIC_FLAG).Value = "N";
&REC.GetField(Field.ACTIVE_FLAG).Value = "Y";
&REC.GetField(Field.DISPLAY_OPTION).Value = "B";
&REC.GetField(Field.STYLECLASSNAME).Value = "PSHYPERLINK";


/*Se asigna el valor de la variable PARENT_FLAG*/
&REC.GetField(Field.PARENT_FLAG).Value = &PARENT_FLAG;


/*Se indica que el nodo está a nivel 1 dentro del árbol*/
&REC.GetField(Field.TREE_LEVEL_NUM).Value = 1;
&REC.GetField(Field.LEVEL_OFFSET).Value = 0;


/*Se ejecuta la función GenerateTree y el resultado se asigna al control HTMLAREA que agregamos a la página */
PE_TREE_EXP.HTML_AREA_01 = GenerateTree(&TREECTL);


El último paso de nuestro ejemplo es agregar el código necesario al evento FieldChange del campo PE_TREE_EXP.PE_FIELD_EVENT. El código que agregaremos a este evento controla cuando se despliegan o se colapsan los nodos y la carga de los nodos hijos cada vez que sea necesario. 

/*Se importa la clase para administrar los nodos de empelados*/

import PE_TREE_EMPLEADOS:pe_nodoEmpleado;


/*Se declara el rowset que contiene la información de los nodos que serán generados por la función GenerateTree*/
Component Rowset &TREECTL;


/*Se declara el objeto que contiene el nodo raíz del árbol de empleados*/
Component PE_TREE_EMPLEADOS:pe_nodoEmpleado &obj_nodos, &obj_nodoActual;


Local PE_TREE_EMPLEADOS:pe_nodoEmpleado &obj_nodoActual, &obj_Hijo;


/*Si el evento es de despliegue de un nodo*/
If Left(PE_TREE_EXP.PE_EVENT_FIELD, 1) = "X" Then
/*Se obtiene la fila que representa el nodo sobre el cual fue ejecutado el evento*/
   &ROW = Value(Right(PE_TREE_EXP.PE_EVENT_FIELD, Len(PE_TREE_EXP.PE_EVENT_FIELD) - 1)) + 1;
   &NODE_ROWSET = &TREECTL.GetRow(2).GetRowset(1);


/*Se obtiene el registro que corresponde al nodo sobre el que se ejecutó el evento*/
   &PARENT_REC = &NODE_ROWSET.GetRow(&ROW).GetRecord(1);


/*Se valida el nivel del nodo sobre el que se ejecuto el evento */
   &PARENT_LEVEL = &PARENT_REC.GetField(Field.TREE_LEVEL_NUM).Value;
   &ROW = &ROW + 1;
   
/*Se obtiene el objeto nodo que corresponde al nodo del árbol sobre el que se ejecuto el evento*/   
   &obj_nodoActual = &obj_nodos.pe_getNodo(&PARENT_REC.GetField(Field.TREE_NODE).Value);


/*Si el nodo tiene hijos se cargan los hijos al árbol*/
   If &obj_nodoActual.TIENE_HIJOS Then
      &obj_nodoActual.pe_cargarHijos();
      Local number &nbr_nodos;
/*Se recorren los hijos del nodo y se agregan al rowset que se está usando de base para el árbol*/
      For &nbr_nodos = 1 To &obj_nodoActual.pe_totalHijos()
         &obj_Hijo = &obj_nodoActual.pe_getNodoNumber(&nbr_nodos);
         &LEVEL_OFFSET = 0;
         &NODE_ROWSET.InsertRow(&ROW - 1);
         &REC = &NODE_ROWSET.GetRow(&ROW).GetRecord(1);
         
         If &obj_Hijo.TIENE_HIJOS Then
            &PARENT_FLAG = "X";
            &REC.GetField(Field.LEAF_FLAG).Value = "N";
            &REC.GetField(Field.TREE_NODE).Value = &obj_Hijo.NOMBRE_NODO;
            &REC.GetField(Field.DESCR).Value = &obj_Hijo.DESCR_NODO;
            &REC.GetField(Field.RANGE_FROM).Value = "";
            &REC.GetField(Field.RANGE_TO).Value = "";
         Else
/*Si el nodo no tiene hijos se marca como una hoja para diferenciarlo en el árbol */
            &PARENT_FLAG = "N";
            &REC.GetField(Field.LEAF_FLAG).Value = "Y";
            &REC.GetField(Field.TREE_NODE).Value = " ";
            &REC.GetField(Field.DESCR).Value = " ";
            &REC.GetField(Field.RANGE_FROM).Value = &obj_Hijo.NOMBRE_NODO;
            &REC.GetField(Field.RANGE_TO).Value = &obj_Hijo.DESCR_NODO;
         End-If;
         
         &REC.GetField(Field.DYNAMIC_FLAG).Value = "N";
         &REC.GetField(Field.ACTIVE_FLAG).Value = "Y";
         &REC.GetField(Field.DISPLAY_OPTION).Value = "B";
         &REC.GetField(Field.STYLECLASSNAME).Value = "PSHYPERLINK";
         &REC.GetField(Field.PARENT_FLAG).Value = &PARENT_FLAG;
         &REC.GetField(Field.TREE_LEVEL_NUM).Value = &PARENT_LEVEL + 1;
         &REC.GetField(Field.LEVEL_OFFSET).Value = &LEVEL_OFFSET;
         
         &ROW = &ROW + 1;
         
      End-For;
      
      /* Se indica al nodo actual que ya se cargaron los nodos*/
      &PARENT_REC.GetField(Field.PARENT_FLAG).Value = "Y";
      
   End-If;
Else
   
   /* Se procesa el evento de selección de un nodo*/
   
   
   If Left(PE_TREE_EXP.PE_EVENT_FIELD, 1) = "S" Then
      Local number &nbr_filas;


/*Se recupera el id del nodo sobre el cual se genero el evento*/
      &ROW = Value(Right(PE_TREE_EXP.PE_EVENT_FIELD, Len(PE_TREE_EXP.PE_EVENT_FIELD) - 1)) + 1;
      &NODE_ROWSET = &TREECTL.GetRow(2).GetRowset(1);


/* se marca como no seleccionado el nodo que había sido seleccionado previamente*/
      For &nbr_filas = 1 To &NODE_ROWSET.ActiveRowCount
         &REC = &NODE_ROWSET.GetRow(&nbr_filas).GetRecord(1);
         If &REC.STYLECLASSNAME.value = "PSTREENODESELECTED" Then
            &REC.STYLECLASSNAME.value = "PSHYPERLINK";
         End-If;
      End-For;
      &REC = &NODE_ROWSET.GetRow(&ROW).GetRecord(1);


/*Se marca el nodo seleccionado como seleccionado*/
      &REC.STYLECLASSNAME.value = "PSTREENODESELECTED";
      
   Else
      rem Procesa los eventos adicionales;
   End-If;
End-If;


/*Se ejecuta nuevamente la función GenerateTree para actualizar el estado del árbol*/
PE_TREE_EXP.HTML_AREA_01 = GenerateTree(&TREECTL, PE_TREE_EXP.PE_EVENT_FIELD);


PE_TREE_EXP.PE_EVENT_FIELD = "";


Con el código anterior finalizamos el desarrollo de nuestro proyecto,   en la imagen siguiente podemos ver el resultado.


Con la anterior imagen me despido por esta vez, espero que le encuentren utilidad a la información anterior.  Hasta la próxima entrada.

sábado, 19 de noviembre de 2011

Como usar Control de Ruta en AWE (Application Workflow Engine))

En la entrada anterior aprendimos como crear un workflow para una transacción utilizando AWE. En esta entrada queremos aumentar las posibilidades del AWE incluyendo el manejo del control ruta dentro de nuestro Workflow. 

Para los que no está familiarizados con el Control Ruta les cuento que es una herramienta que se utiliza en la anterior tecnología de workflow para restringir el enrutamiento de las transacciones.  Por ejemplo, tu tienes una trasacción que solo necesita una aprobación, entonces creamos un rol de aprobador y se lo asociamos a las personas que pueden aprobar la transacción, pero si la transacción se debe aprobar por una sola persona teniendo en cuenta la ciudad donde se haya generado la transacción, o sea el que aprueba las transacciones de Cali no es el mismo que aprueba el de Bucaramanga, ahí es donde entra a jugar el control ruta,  este nos permite asignarle a un usuario el rol de aprobador y además las ciudades para las cuales el debe realizar las aprobaciones.  

Para nuestro ejemplo vamos a usar el control de ruta para separar los aprobadores de transacciones por país, dependiendo del país en el que se registre el equipo debemos asignar un aprobador diferente.

El primer paso en nuestro ejemplo es crear un tipo control ruta, esto lo hacemos por la siguiente ruta: Inicio > Peopletools > Workflow > Rutas y Roles > Tipos de Control Ruta. En este componente agregamos un nuevo tipo llamado PAISES y como tabla de validación le asignamos COUNTRY_TBL. 

El siguiente paso es crear agrupaciones de países para que luego sean asignados a los aprobadores. En nuestro caso crearemos 2 grupos,  uno que representará a los países involucrados en la NBA y otro en el que asociaremos los países de latinoamerica.  Para esto vamos a Inicio > Peopletools > Workflow > Rutas y Roles > Perfiles de   Control de Rutas.  En esta ruta agregamos 2 perfiles:

- EQUIPOS LATAM:  A este perfil le asociamos el tipo de control ruta que creamos (PAISES) y le agregamos los países de Suramerica. 

- EQUIPOS NBA: A este perfil le asociamos el tipo de control ruta que creamos (PAISES) y le agregamos los países de la NBA. 

En la entrada anterior habíamos creado 3 usuarios para realizar las aprobaciones: PE_APROBADOR1 Asociado al rol PE_APROBADORES_UNO, PE_APROBADOR2 Asociado al rol PE_APROBADORES_DOS y PE_APROBADOR3 Asociado al rol PE_APROBADORES_TRES.  El siguiente paso es asociar a esos 3 usuarios al perfil control ruta "EQUIPOS LATAM" para que estos usuarios queden habilitados solo para aprobar los equipos asociados a países de Suramérica. Esto lo hacemos en Inicio > Peopletools > Seguridad > Perfiles de Usuario > Perfiles de Usuario, buscamos cada uno de los usuarios y vamos al rol de aprobación que tiene asociado cada uno de ellos.   Para cada rol asociado al usuario existe un link llamado Control Ruta,  en ese link podemos asociar el perfil control ruta "EQUIPOS LATAM". 

Para los aprobadores de los equipos de la NBA debemos crear 3 usuario más. PE_APROBADOR1_1 Asociado al rol PE_APROBADORES_UNO, PE_APROBADOR2_1 Asociado al rol PE_APROBADORES_DOS y PE_APROBADOR3_1 Asociado al rol PE_APROBADORES_TRES y en el control ruta asociamos el perfil "EQUIPOS NBA". 

Ahora debemos crear las consultas que nos permitirán enlazar el control ruta con el Application Workflow Engine.  Por cada rol que manejamos debemos crear 1 consulta por el gestor de consultas de Peoplesoft. De esta forma para nuestro ejemplo las consultas quedarán de la siguiente forma:

- PE_APROBADORES_UNO_QRY:


SELECT A.ROLEUSER 
  FROM PSROLEUSER A, PS_RTE_CNTL_RUSER B, PS_RTE_CNTL_LN C, PS_PE_EQUIPOS D
  WHERE A.ROLEUSER = B.ROLEUSER
     AND A.ROLENAME = B.ROLENAME
     AND B.RTE_CNTL_PROFILE = C.RTE_CNTL_PROFILE
     AND C.RTE_CNTL_TYPE = 'PAISES'
     AND D.COUNTRY BETWEEN C.RTE_FROM_VALUE AND C.RTE_TO_VALUE
     AND D.PE_ID_EQUIPO = :1
     AND A.ROLENAME = 'PE_APROBADORES_UNO'



- PE_APROBADORES_DOS_QRY:

SELECT A.ROLEUSER 
  FROM PSROLEUSER A, PS_RTE_CNTL_RUSER B, PS_RTE_CNTL_LN C, PS_PE_EQUIPOS D
  WHERE A.ROLEUSER = B.ROLEUSER
     AND A.ROLENAME = B.ROLENAME
     AND B.RTE_CNTL_PROFILE = C.RTE_CNTL_PROFILE
     AND C.RTE_CNTL_TYPE = 'PAISES'
     AND D.COUNTRY BETWEEN C.RTE_FROM_VALUE AND C.RTE_TO_VALUE
     AND D.PE_ID_EQUIPO = :1
     AND A.ROLENAME = 'PE_APROBADORES_DOS'

- PE_APROBADORES_TRES_QRY:

SELECT A.ROLEUSER 
  FROM PSROLEUSER A, PS_RTE_CNTL_RUSER B, PS_RTE_CNTL_LN C, PS_PE_EQUIPOS D
  WHERE A.ROLEUSER = B.ROLEUSER
     AND A.ROLENAME = B.ROLENAME
     AND B.RTE_CNTL_PROFILE = C.RTE_CNTL_PROFILE
     AND C.RTE_CNTL_TYPE = 'PAISES'
     AND D.COUNTRY BETWEEN C.RTE_FROM_VALUE AND C.RTE_TO_VALUE
     AND D.PE_ID_EQUIPO = :1
     AND A.ROLENAME = 'PE_APROBADORES_TRES'

Como podemos ver, cada consulta está asociada a un rol y además está enlazada al tipo de control ruta PAISES. La consulta adicionalmente tiene un PROMPT que filtra el ID del equipo que se está procesando, esto lo usa el sistema para poder filtrar los usuarios aprobadores que corresponden para cada equipo dependiendo del país que hayamos seleccionado. 

El último paso es modificar las listas de usuario que creamos en la entrada anterior,  para que ahora apunten a las consultas que acabamos de crear y no directamente a los roles. Las listas de usuarios modificadas deben verse de la siguiente forma: 


Y listo de está forma hemos modificado nuestro workflow para que tenga en cuenta el control ruta, solo queda hacer las pruebas y terminamos. 

Espero que está entrada les sea de utilidad, hasta la siguiente publicación. 


sábado, 29 de octubre de 2011

AWE: Application Workflow Engine

En está entrada vamos a ver el nuevo (Nuevo desde la versión 8.48) framework para incluir Workflows dentro de nuestras aplicaciones.  

La gran ventaja que presenta este framework frente a la anterior tecnología de workflow presente en PEOPLESOFT,  es que después de implantar el workflow dentro de nuestro aplicativo, no es necesario realizar trabajo técnico adicional, ya que todo el mantenimiento y parametrización adicional del workflow se puede realizar a través de la PIA. 

Lo primero que vamos a hacer son los pasos que se deben realizar por un técnico para poder incluir el worflow dentro del aplicativo.   

Para nuestro ejemplo necesitamos una página donde vamos a registrar los diferentes equipos deportivos, supongamos que ya tenemos la página y se ve de la siguiente forma:






Digamos que a este componente deseamos agregarle la funcionalidad del workflow, que para cada equipo que se adicione a la base de datos debe pasar por un proceso de aprobación. 


El primer paso es agregar al registro principal de nuestro componente (PE_EQUIPOS) un campo que represente el estado del equipo. El campo debe permitir 3 valores:


P: Pendiente
A: Aprobado
R: Rechazado


Adicionalmente debemos crear un registro derived que contendrá 4 campos:


- Un campo que representará el botón Enviar el que permitira iniciar el proceso de aprobación del registro


- Un segundo campo que representará el botón de Aprobar que permitirá al aprobador aprobar el registro. 

- Un tercer campo que representará el botón de rechazar que permitirá al aprobador rechazar el registro. 

- Y un último campo que permitirá ingresar comentarios al momento de aprobar o rechazar un registro. 

Después de agregar estos campos la página de nuestro componente debe lucir de la siguiente forma:





El siguiente paso es crear el registro que permitirá llevar el control de los procesos de aprobación de cada uno de los equipos,  en este record (de tipo SQL TABLE) se almacena el rastreo del workflow que estamos definiendo.  El registro debe ser construido de la siguiente forma:

1. Debe tener todos los campos llave del record principal de nuestro componente (en nuestro caso PE_EQUIPOS y la llave sería PE_ID_EQUIPO)

2. Debe incluir el subrecord EOAW_XREF_SBR.



Ahora debemos crear un Application Class que permita manejar el proceso del workflow.  Esta clase permite básicamente controlar la actualización del estado del registro, o sea permite actualizarlo a Pendiente, Aprobado o Rechazado según sea el caso. 


Está clase debe ser una clase derivada de la clase base EOAW_CORE:ApprovalEventHandler y debe implementar los siguientes métodos:


1. OnProcessLaunch: En este método se actualiza el estado del registro a P para indicar que el proceso acaba de iniciar y que el registro se encuentra pendiente de aprobación 


2. OnHeaderApprove: En este método se actualiza el estado del registro a A para indicar que el registro se encuentra aprobado

3. OnHeaderDeny: En este método se actualiza el estado del registro a R para indicar que el registro se encuentra rechazado

La clase padre posee una propiedad llamada &appInst que nos permite acceder a algunos métodos y propiedades para distintos procesos, en nuestra clase ejemplo utilizamos el método &appInst.thread.SetAppKeys para obtener la llaves del registro para el cual se está realizando el proceso de Workflow, en nuestro caso PE_EQUIPOS y la llave PE_ID_EQUIPO. 

Por último la clase tiene un método privado cuya finalidad es ejecutar el UPDATE sobre el campo estado del registro PE_EQUIPOS el cual es utilizado desde los otros 3 métodos. 

A continuación podemos ver el código completo de la clase:

import EOAW_CORE:*;
import EOAW_CORE:ENGINE:*;

class PE_APPROVAL_HANDLER extends EOAW_CORE:ApprovalEventHandler
   method PE_APPROVAL_HANDLER();
   method OnProcessLaunch(&appInst As EOAW_CORE:ENGINE:AppInst);
   method OnHeaderApprove(&appinst As EOAW_CORE:ENGINE:AppInst);
   method OnHeaderDeny(&userinst As EOAW_CORE:ENGINE:UserStepInst);
private
   method updateProcessFlag(&status As string);
   instance Record &RecordEquipo;
   Constant &APPROVED = "A";
   Constant &DENIED = "D";
   Constant &PENDING = "P";
end-class;

method PE_APPROVAL_HANDLER
   %Super = create EOAW_CORE:ApprovalEventHandler();
   &RecordEquipo = CreateRecord(Record.PE_EQUIPOS);
end-method;

method updateProcessFlag
   /+ &status as String +/
   Local string &IdEquipo;
   
   &IdEquipo = &RecordEquipo.PE_ID_EQUIPO.Value;
   
   SQLExec(SQL.PE_EQUI_UPD_APPR_FLAG, &status, &IdEquipo);
   
end-method;

method OnProcessLaunch
   /+ &appInst as EOAW_CORE:ENGINE:AppInst +/
   /+ Extends/implements EOAW_CORE:ApprovalEventHandler.OnProcessLaunch +/
   
   &appInst.thread.SetAppKeys(&RecordEquipo);
   %This.updateProcessFlag(&PENDING);
end-method;

method OnHeaderApprove
   /+ &appinst as EOAW_CORE:ENGINE:AppInst +/
   /+ Extends/implements EOAW_CORE:ApprovalEventHandler.OnHeaderApprove +/
   
   &appinst.thread.SetAppKeys(&RecordEquipo);
   %This.updateProcessFlag(&APPROVED);
end-method;

method OnHeaderDeny
   /+ &userinst as EOAW_CORE:ENGINE:UserStepInst +/
   /+ Extends/implements EOAW_CORE:ApprovalEventHandler.OnHeaderDeny +/
   
   &userinst.thread.SetAppKeys(&RecordEquipo);
   %This.updateProcessFlag(&DENIED);
end-method;

El siguiente paso dentro de nuestra implantación del workflow es escribir el código que inicia los objetos necesarios para el proceso,  esto se debe realizar el el evento POSTBUILD del componente. Antes de eso vamos a escribir una clase que nos permite controlar la visualización de los botones de Aprobación, Rechazo y Envío del componente.  

Esta clase debe permitir a partir de una instancia de la clase EOAW_CORE:LaunchManager y otra de la clase EOAW_CORE:ApprovalManager determinar si el usuario que está accediendo al componente es aprobador, si es así habilitará los botones de aprobación y rechazo.  Si el proceso está pendiente de ser iniciado la clase habilitará el botón Enviar. 

Para determinar si un usuario está habilitado para aprobar o rechazar la transacción se utilizá la propiedad hasPending del objeto EOAW_CORE:ApprovalManager la cual indica si el usuario que ingreso al objeto tiene pendiente realizar una aprobación sobre el registro. 

Para determinar si el registro está pendiente de iniciar el proceso de Workflow se usa la propiedad submitEnabled del objeto  EOAW_CORE:LaunchManager.  

La clase quedaría de la siguiente forma:  

import EOAW_CORE:LaunchManager;
import EOAW_CORE:ApprovalManager;

class PE_UTILIDADES
   method PE_UTILIDADES();
   method pe_ActualizarWF(&rs0 As Rowset, &obj_launch As EOAW_CORE:LaunchManager, &obj_approval As EOAW_CORE:ApprovalManager);
private
end-class;

method PE_UTILIDADES
end-method;

method pe_ActualizarWF
   /+ &rs0 as Rowset, +/
   /+ &obj_launch as EOAW_CORE:LaunchManager, +/
   /+ &obj_approval as EOAW_CORE:ApprovalManager +/
   Local Row &row1 = &rs0.GetRow(1);
   Local Record &rec_approval = &row1.GetRecord(Record.PE_EQUIPOS_WRK);
   Local boolean &isApprover = False;
   
   If &obj_approval.hasAppInst Then
      &isApprover = &obj_approval.hasPending;
   End-If;
   &rec_approval.PE_ENVIAR_BTN.Visible = &obj_launch.submitEnabled;
   &rec_approval.PE_APROBAR_BTN.Visible = &isApprover;
   &rec_approval.PE_RECHAZAR_BTN.Visible = &isApprover;
end-method;

Ya habiendo construida está clase podemos escribir el código del evento postbuild del componente:

Importamos las clases propias del AWE que necesitamos

import EOAW_CORE:LaunchManager;
import EOAW_CORE:ApprovalManager;


Importamos la clase que acabamos de crear para habilitar los botones según el estado del proceso y el usuario que esté ingresando al registro:


import PE_WF_EJEMPLO:PE_UTILIDADES;


Declaramos las variables de componente que se utilizan durante todo el proceso de Workflow:

Component EOAW_CORE:LaunchManager &obj_launch;
Component EOAW_CORE:ApprovalManager &obj_approval;


Creamos la variable para almacenar el nombre del proceso que vamos a ejecutar. Este es el nombre con el cual se va a identificar el proceso y el que se debe parametrizar en las páginas de parametrización d workflow. 

Local string &str_process = "EQUIPOS";


Creamos la variable record para el cual se ejecutará el workflow.

Local Record &rec_equipos = GetRecord(Record.PE_EQUIPOS);



Inicializamos las variables de componente que se utilizan durante todo el proceso de Workflow, los parámetros identifican el proceso de workflow que se va a ejecutar para el registro y con que usuario se está ejecutando ese proceso. 

&obj_launch = create EOAW_CORE:LaunchManager(&str_process, &rec_equipos, %OperatorId);
&obj_approval = create EOAW_CORE:ApprovalManager(&str_process, &rec_equipos, %OperatorId);



Actualizamos el estado de los botones de aprobación, rechazo y envío. 

Local PE_WF_EJEMPLO:PE_UTILIDADES &obj_utilidades = create PE_WF_EJEMPLO:PE_UTILIDADES();;
&obj_utilidades.pe_ActualizarWF(GetLevel0(), &obj_launch, &obj_approval);

El siguiente paso es escribir el código que ejecutará cada uno de los botones. El código que se colocará directamente en los bonotes solo activará una bandera y salvará el componente,  esto con el fin de ejecutar el código principal en el evento SavePostChange del componente.

Botón Aprobar:

PE_EQUIPOS_WRK.PE_APROBAR_BTN.Value = "Y";

&XSTATUS = PE_EQUIPOS.PE_ESTADO_EQUIPO;
PE_EQUIPOS.PE_ESTADO_EQUIPO = "";
PE_EQUIPOS.PE_ESTADO_EQUIPO = &XSTATUS;
DoSaveNow();

Botón Rechazar:

PE_EQUIPOS_WRK.PE_APROBAR_BTN.Value = "Y";

&XSTATUS = PE_EQUIPOS.PE_ESTADO_EQUIPO;
PE_EQUIPOS.PE_ESTADO_EQUIPO = "";
PE_EQUIPOS.PE_ESTADO_EQUIPO = &XSTATUS;
DoSaveNow();


Botón Enviar:

PE_EQUIPOS_WRK.PE_ENVIAR_BTN.Value = "Y";

&XSTATUS = PE_EQUIPOS.PE_ESTADO_EQUIPO;
PE_EQUIPOS.PE_ESTADO_EQUIPO = "";
PE_EQUIPOS.PE_ESTADO_EQUIPO = &XSTATUS;
DoSaveNow();

Ahora si vamos a revisar el código que debemos colocar en el evento SavePostChange del Componente:


Importamos las clases propias del AWE que necesitamos


import EOAW_CORE:LaunchManager;
import EOAW_CORE:ApprovalManager;

Importamos la clase que acabamos de crear para habilitar los botones según el estado del proceso y el usuario que esté ingresando al registro:


import PE_WF_EJEMPLO:PE_UTILIDADES;


Declaramos las variables de componente que se utilizan durante todo el proceso de Workflow:

Component EOAW_CORE:LaunchManager &obj_launch;
Component EOAW_CORE:ApprovalManager &obj_approval;

Local PE_WF_EJEMPLO:PE_UTILIDADES &obj_utilidades = create PE_WF_EJEMPLO:PE_UTILIDADES();;



Creamos la variable record para el cual se ejecutará el workflow.


Local Record &rec_equipos = GetRecord(Record.PE_EQUIPOS);



Creamos la variable record donde se encuentran los campos referentes a los botones:


Local Record &rec_wrk = GetRecord(Record.PE_EQUIPOS_WRK);


Validamos si se oprimió el botón enviar y si es así inicializa el proceso de workflow utilizando el método  DoSubmit. Siempre que se inicie o se reinicie un proceso de workflow la variable EOAW_CORE:ApprobalManger debe ser reiniciada. La propiedad hasAppInst indica si el proceso fue iniciado correctamente. 

If &rec_wrk.PE_ENVIAR_BTN.Value = "Y" Then
   &obj_launch.DoSubmit();
   If &obj_launch.hasAppInst Then
      &obj_approval = create EOAW_CORE:ApprovalManager(&obj_launch.txn.awprcs_id, &rec_equipos, %OperatorId);
   End-If;
Else

Validamos si se oprimió el botón aprobar y si es así se llama el método DoApprove para realizar la aprobación del registro. Si el usuario que está aprobando no es el aprobador final y existen  otros usuario que deben aprobar antes de que el registro quede en estado aprobado, el métdo DoApprove dejará el estado en pendiente y remitirá la transacción al siguiente aprobador

   If &rec_wrk.PE_APROBAR_BTN.Value = "Y" Then
      &obj_approval.DoApprove(&rec_equipos);
   Else

Validamos si se oprimió el botón rechazar y si es así se llama el método DoDeny para realizar el rechazo del registro.

      If &rec_wrk.PE_RECHAZAR_BTN.Value = "Y" Then
         &obj_approval.DoDeny(&rec_equipos);
      Else


Si el proceso de workflow ya se ha iniciado pero alguien le hizo alguna modificación al registro, es necesario volver a iniciar el proceso de aprobación, para eso se utiliza el método DoResubmit el cual resetea el proceso y lo inicia nuevamente. 



         If &obj_approval.hasAppInst And
               &obj_launch.resubmitEnabled Then
            &obj_launch.DoResubmit();
         End-If;


      End-If;
   End-If;
End-If;

Por último actualizamos el estado de los botones de workflow:

&obj_utilidades.pe_ActualizarWF(GetLevel0(), &obj_launch, &obj_approval);
&rec_wrk.PE_ENVIAR_BTN.Value = "N";
&rec_wrk.PE_APROBAR_BTN.Value = "N";
&rec_wrk.PE_RECHAZAR_BTN.Value = "N";

Con lo anterior finalizamos los pasos técnicos que son necesarios para habilitar el workflow dentro de un componente especifico,  el siguiente paso es parametrizar el proceso dentro del aplicativo. 


Las parametrizaciones del AWE se realiza por el menú Inicio > Componentes de Empresa > Aprobaciones > Aprobaciones.  En esta ruta encontramos todas las opciones que nos permiten parametrizar el workflow.  


1. Primero se debe registrar la trasacción,  por la opción de Registro de Transacciones. El id de proceso que debemos registrar para nuestro ejemplo es EQUIPOS,  el cual es el que hemos estado utilizando dentro del PeopleCode. 


Lo principal que se debe parametrizar en este punto son:



  • El registro de referencia (El registro que creamos utilizando el subrecord EOAW_XREF_SBR).
  • La opción de Activar notificaciones, donde definimos si queremos informar a los interesados en el workflow (Solicitantes, aprobadores, etc.) por correo electrónico,  por lista de trabajo o por ambos. 
  • El menú y el componente donde se realizan las aprobaciones.  Hacia este menú y componente redireccionará el motor de workflow a los interesados (Aprobadores) para continuar con el proceso. 
  • Clase de Gestión de Aprobación,  acá seleccionamos la clase que administrará el estado de los registros que se encuentren en proceso de aprobación.  En nuestro caso será PE_APPROVAL_HANDLER
  • En Niveles de Aprobación seleccionamos el registro al que le vamos a realizar el proceso de aprobación. 
A continuación podemos visualizar como quedaría la definición del proceso EQUIPOS:



2. La segunda configuración que se debe hacer es la que se refiere a las notificaciones que se deben enviar cada vez que ocurra un evento dentro del proceso de workflow.  Para esto se debe definir:



  • La vista de usuarios que se va a utilizar (Usuarios Aprob): a menos que suceda algo extraordinario o que se necesite información adicional de los usuarios la vista que siempre se debe usar es PSOPRDEFN_VW
  • El evento para el cual se realizará la notificación: Básicamente se deben parametrizar 3 eventos: En Aprobación Final, En Rechazo Final y Ruta para Aprobación.
  • Luego se define el menú, componente y página al cual se debe dirigir el notificado para realizar el proceso de workflow (Aprobar, Rechazar o Revisar según el caso)
  • Por último se define el perfíl al cual se debe notificar: En el caso de Rechazo y Aprobación Final se debe informar al solicitante para que este revise por que se rechazo o por que se aprobó. En caso de Runta para Aprobación se le debe informar al usuario que debe aprobar o rechazar la transacción o sea a los aprobadores. 
A continuación se pueden visualizar las parametrizaciones que se realizaron para los 3 eventos dentro de nuestro ejemplo. 








3. El siguiente paso es definir las listas de usuarios que realizarán aprobaciones dentro del workflow. Estás listas se pueden definir con base en un App Package, un Objeto SQL, un Rol   o una consulta realizada por el gestor de consultas.  Las listas de usuarios se definen por la opción Definición Lista Usuarios.  

Para nuestro ejemplo definiremos 3 listas de usuarios, cada una apuntando a un rol diferente. Utilizaremos 3 listas de usuarios por que tendremos 3 niveles de aprobación, o sea que un registro debe ser aprobado por 3 usuarios antes de cambiar de estado a Aprobado. 

A continuación podemos ver la primer lista de usuarios que se parametrizó (En entradas posteriores revisaremos los diferentes tipos de listas de usuario, y la forma de usar el control ruta para las listas de usuarios definidas como roles.):






4. El último paso a la hora de definir un workflow es definir el proceso de aprobación. Cada proceso consta de:

  • Fases: Todas las fases se deben ejecutar dentro de un proceso y  son secuenciales una detrás de otra. 
  • Las fases están compuestas por Rutas:  dentro de una fase las diferentes rutas son paralelas. A diferencia de las fases las rutas no necesariamente deben ser ejecutadas. Para que una ruta sea ejecutada debe cumplir los criterios definidos sino será ignorada. 
  • Por último las rutas están compuestos por pasos que deben ser ejecutados de forma secuencial y que a su vez pueden ser ignorados si no cumplen los criterios definidos para el paso. Dentro del paso se define la lista de usuarios que debe realizar la aprobación del paso. 
Para nuestro ejemplo se debe ingresar en cada uno de los criterios de las rutas, los pasos y el proceso y definirlo como se ve en la siguiente imagen:




La definición del proceso de nuestro ejemplo debe quedar como la siguiente figura:



5. Se me olvidaba, si deseamos poder hacer seguimiento a cada una de nuestras transacciones dentro del proceso de aprobación utilizando la opción Control de Aprobaciones, debemos agregar nuestro Id de Proceso a la opción Configuración del Monitor y debe quedar de la siguiente forma:



Listo ahora solo nos queda probar nuestro workflow, para lo anterior debemos crear los siguientes usuarios:

  • Solicitante: Usuario con permisos sobre el componente de equipos. 
  • Aprobador 1: Usuario con permisos sobre el componente de equipos y adicionalmente con el rol asociado a la lista de usuarios PE_EQUIPOS_UNO
  • Aprobador 2: Usuario con permisos sobre el componente de equipos y adicionalmente con el rol asociado a la lista de usuarios PE_EQUIPOS_DOS
  • Aprobador 3: Usuario con permisos sobre el componente de equipos y adicionalmente con el rol asociado a la lista de usuarios PE_EQUIPOS_TRES. 
1. El primer paso de nuestra prueba es ingresar un registro en el componente de equipos,  para eso ingresamos con el usuario Solicitante y creamos un equpo:




Como podemos ver al usuario solicitante se le habilita el botón enviar. Al dar clic en el botón enviar se iniciará el proceso de aprobación y se le ocultará el botón enviar para el usuario Solicitante.

Si vamos a la opción Inicio > Componentes de Empresa > Aprobaciones > Aprobaciones > Control de aprobaciones y buscamos por el Proceso "EQUIPOS" podemos ver que el equipo se encuentra en estado pendiente. Si ingresamos al registro podremos ver graficamente como va el proceso de aprobación,  incluso los pasos que faltan antes de la aprobación final. 

2. El segundo paso en nuestro ejemplo es ingresar al aplicativo con el usuario Aprobador 1 e ir a revisar nuestra lista de trabajo. Como podemos ver en la imagen tenemos pendiente una aprobación. 




3. Mediante el link que aparece en la lista de trabajo nos dirigimos hacia el componente de equipos al registro pendiente por aprobación. En la imagen se ve que ahora se encuentran habilitados el botón Aprobar y Rechazar.




4. El siguiente paso es aprobar la transacción e ingresar con los usuarios Aprobador 2 y Aprobador 3 y terminar el proceso completo hasta que el estado del registro quede aprobado. 

Con esto hemos terminado nuestra entraba básica sobre el manejo del AWE. En futuras entradas estaremos estudiando características adicionales de está tecnología, como son:

  • Control Ruta
  • Criterios de Selección de Rutas y Pasos
Bueno espero que les sea de utilidad está información y nos vemos en futuras publicaciones.