Introducción
El sistema xPay (ex Payroll) surge de la necesidad del área de la Dirección de Factor Humano de consolidar información de los empleados del organismo. La inflexibilidad del sistema Payroll motivó la necesidad de una mejora sustancial en el manejo de la información como así también integrar los datos del empleados para poder administrar los legajos en forma eficiente. xPay también corrigió muchos errores de diseño de la herramienta original que era muy limitada en cuanto a la integración de sus módulos. Una de estas deficiencias era que los datos no estaban temporalizados, es decir, que no constaban con fechas desde y hasta y resultaba imposible recrear la información del empleado a un período en particular.
La incorporación del xPay agregó mayor cantidad de atributos a las distintas herramientas como ser el módulo de Carrera que permitió consolidar toda la información sobre la trayectoria laboral del empleado, como así también producir un informe de liquidaciones para el área que consume esta información.
xPay se implementa como una gran extensión del Payroll siendo enteramente compatible con este desde la base de datos. La estrategia fue entonces la de obtener datos desde la base de datos de Payroll, incorporarlos al xPay y así hacer una transición sin mayores inconvenientes dado que los usuarios del sistema podían verificar los mismos datos que en la herramienta original.
El desarrollo del xPay se basó en la estructura de base de datos del xPay y entonces hubo que recrear toda la lógica subyacente. El mayor desafío fue la implementación del módulo de licencias que debió recrear todos los cálculos de las licencias compensatorias detalladas.
xPay supera ampliamente la muestra de datos del sistema original incorporando varios módulos como ser Discapacidad, Subsidios, entre otras herramientas. xPay también absorbe el sistema de visor de documentos digitales pudiendo integrar el legajo en papel directamente al perfil del empleado.
La incorporación del portal Mi Portal resulta estratégica ya que proveerá a los empleados la facilidad de acceder a sus datos personales, como así también solicitar licencias en línea eliminando la necesidad de presentar esta información en papel y liberando al área de licencias de la gestión de un trámite engorroso. Para esto xPay incorpora un flujo de aprobación de licencias que permite validar los datos solicitados por el usuario
Por otra parte la aplicación xPay muestra cómo es posible tener dos templates o “skins” de la misma herramienta: uno el xPay basado en ExtJS y el otro MiPortal basado en Boostrap, permitiendo dos caras de la misma aplicación donde residen las mismas reglas de negocio para dos interfaces distintas. Es decir que la validación de las licencias es realizada por la misma porción de código que será descrita más adelante.
Configuración de xPay: config.xml
El archivo de configuración del xPay muestra en general cómo se configuran las herramientas de la red del organismo. Además de los datos de la base de datos en este archivo destaca:
- la información del caché de la aplicación y del ACL (Access Control List)
- la configuración del servidor del LDAP para las claves de usuario
- la configuración del servidor de correo y los remitentes para el flujo de aprobación de licencias
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
<?xml version="1.0" encoding="UTF-8"?> <config> <db_instance> <host>mysql1.jusbaires.gov.ar</host> <database>xpay</database> <user>xpay</user> <password>xxxx</password> <implementation>mysqli</implementation> <!-- <encoding>ISO-8859-1</encoding> --> </db_instance> <!-- caches de la aplicacion --> <app_cache_time type="int">0</app_cache_time> <gacl_cache_time type="int">600</gacl_cache_time> <!-- esquema de autentificacion --> <!-- <login_fn>_login</login_fn> --> <login_fn>_login_ldap</login_fn> <!-- para la ejecucion de comandos, el trusted host --> <anonymous_user_id>58</anonymous_user_id> <trusted_host_user_id>1</trusted_host_user_id> <trusted_host>127.0.0.1</trusted_host> <!-- al momento de crear un usuario, el rol por default --> <create_user_default_role>empleado</create_user_default_role> <!-- no mostrar la accion de login en la auditoria --> <audit_silent_action>/(login)/s</audit_silent_action> <!-- log sql --> <log_sql type="bool">false</log_sql> <!-- configuracion LDAP --> <ldap_allow_login type="bool">true</ldap_allow_login> <ldap_host>LDAP://WEBLDAP.JUSBAIRES.GOV.AR</ldap_host> <ldap_port>389</ldap_port> <ldap_version>3</ldap_version> <ldap_base_dn>OU=PEOPLE,DC=JUSBAIRES,DC=GOV,DC=AR</ldap_base_dn> <ldap_search_user/> <ldap_search_pass/> <ldap_user_filter>(UID=%USERNAME%)</ldap_user_filter> <ADD_images_path>/var/www/ADD/Legajos/</ADD_images_path> <img_cert_check_md5 type="bool">false</img_cert_check_md5> <!-- configuracion notificaciones via email --> <mailer_charset>UTF-8</mailer_charset> <mailer_smtp_debug type="int">0</mailer_smtp_debug> <mailer_smtp_auth type="bool">false</mailer_smtp_auth> <mailer_smtp_port>25</mailer_smtp_port> <mailer_host>localhost</mailer_host> <!-- remitentes de las notificaciones --> <!-- <mailer_username></mailer_username> <mailer_password></mailer_password> --> <!-- <test_email>espotorno@jusbaires.gov.ar</test_email> --> <email_licencias>espotorno@jusbaires.gov.ar</email_licencias> <email_presidencia>espotorno@jusbaires.gov.ar</email_presidencia> </config> |
Inclusión de la serialización del proyecto Payroll e inclusión en el xPay:
xPay serializa la estructura de la base de datos de Payroll, permitiéndo poder acceder en línea a los datos de esta aplicación. Para ello incluye la serialización con el comando “include” sin la necesidad de reescribir las estructuras. Es modo de refrerencia, la aplicación Payroll (llamada xPayroll) no tiene lógica en sí ni tampoco puede ser accedida en forma autónoma, solamente a través del xPay:
1 2 3 4 5 6 7 8 9 10 11 12 |
<!-- includes --> <include path="projects/plugins/_common"/> <include path="projects/plugins/_acl"/> <include path="../payroll"/> <include path="projects/plugins/_audit"/> <include path="projects/plugins/_messages"/> <include path="projects/plugins/_users"/> <include path="projects/plugins/_sessions"/> <include path="projects/plugins/_file"/> <include path="projects/plugins/_home"/> <include path="projects/plugins/_imagen"/> |
Sincronización periódica con Payroll
Para facilitar la migración, acostumbrar al usuario a los mismos datos pero a una herramienta diferente y reducir la curva de aprendizaje, xPay toma los datos de Payroll desde la base MSSQL en la cual está desarrollado y los sincroniza incorporándolos a la base de datos MySQL. Hubo que identificar tabla por tabla que datos eran necesarios de la herramienta orginal y a medida que se implementaban estos módulos en xPay la sincronización era desafectada tabla por tabla.
El script a continuación muestra la forma que tiene la herramienta de poder sincronizar desde el CRON del servidor, los parámetros de filtro para no sincronizar toda la tabla siempre (para casos de tablas muy grandes como ser RESUMEN) o bien ejecutar procesos de notificaciones cada 5 minutos. Requere de la configuración del crontab en el servidor donde reside la aplicación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<file name="cron/sync_payroll_daily"> #!/bin/sh # acordarse que si aplica un filtro con --s, tiene que ser la tabla source, no la destino /usr/bin/xpotronix --path=/var/www/sites/xpotronix/xpay/ --m=_clasifica --a=process --p=sync_data /usr/bin/xpotronix --path=/var/www/sites/xpotronix/xpay/ --m=_feriado --a=process --p=sync_data /usr/bin/xpotronix --path=/var/www/sites/xpotronix/xpay/ --m=_empresa --a=process --p=sync_data /usr/bin/xpotronix --path=/var/www/sites/xpotronix/xpay/ --m=_empleado --a=process --p=sync_data /usr/bin/xpotronix --path=/var/www/sites/xpotronix/xpay/ --m=_grupofam --a=process --p=sync_data /usr/bin/xpotronix --path=/var/www/sites/xpotronix/xpay/ --m=_fichah --a=process --p=sync_data --s[REMPLESH][Ames]=`date +%Y%m` /usr/bin/xpotronix --path=/var/www/sites/xpotronix/xpay/ --m=_resumen --a=process --p=sync_data --s[RESUMEN][Ames]=`date +%Y%m` </file> <file name="cron/enviar_notificaciones"><![CDATA[#!/bin/sh /usr/bin/xpotronix --path=/var/www/sites/xpotronix/xpay/ --m=notificacion --a=process --p=enviar_notificaciones ]]></file> |
Jerarquía y acceso a los módulos con menú de perfiles de acceso
Una de las características principales del xPay es la riqueza de perfiles de usuarios que enlazan a toda la organización en forma transversal. Tanto los empleados como los integrantes del las áreas de recursos humanos, auditoría y el plenario en sí pueden acceder a la herramienta, esta riqueza se observa en la forma que está organizado el menú de la aplicación. Este archivo permitirá mostrar y ocultar distintos módulos, a su vez cada módulo posee las restricciones correspondientes de acceso tanto en la configuración del ACL como así también a través del método main_sql() donde se pueden aplicar filtros particulares. En el fragmento de código que sigue, la estructura del menú en menu.xml:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
<?xml version="1.0" encoding="utf-8"?> <menu n="xPay"> <item n="Mis Datos Personales" h="?m=_empleado" acl="role:empleado"/> <!--<item n="Mis Trámites" h="?m=_mesa" acl="role:empleado"/> --> <item n="Legajo Digital" h="?m=imagen" acl="role:empleado"/> <item n="Cumpleaños" h="?m=_cumples" acl="role:empleado"/> <item n="Agenda del Personal" h="?m=v_agenda" acl="role:empleado"/> <menu n="Empleados" acl="role:admin,mesa,licencias,altas,responsable,audit,consejero,sec_plenario,lectura,mp"> <item n="Empleados" h="?m=_empleado" acl="access:_empleado"/> <item n="Legajo Digital" h="?m=imagen" acl="access:imagen"/> <item n="Agenda del Personal" h="?m=v_agenda" acl="access:v_agenda"/> <item n="Cumpleaños" h="?m=_cumples" acl="access:_cumples"/> </menu> <menu n="Altas" acl="role:admin,mesa,licencias,responsable,audit,altas,lectura,mp"> <item n="Hoja de Vida" h="?m=_hojadevida" acl="access:_hojadevida"/> <item n="Carrera Laboral" h="?m=_intersub" acl="access:_intersub"/> <item n="Datos Adicionales" h="?m=datos_ad" acl="access:datos_ad"/> <item n="Permanencia en el Cargo" h="?m=permanencia" acl="access:permanencia"/> <item n="Antigüedad" h="?m=antiguedad" acl="add:antiguedad"/> <menu n="Discapacidad" acl="add:disca"> <item n="Discapacidad" h="?m=disca" acl="add:disca"/> <item n="Tipos de Discapacidades" h="?m=t_disca" acl="add:t_disca"/> </menu> <item n="Título Académico" h="?m=titulo" acl="add:titulo"/> <menu n="Subsidios" acl="role:admin,responsable,altas,audit,consejero,sec_plenario,lectura,mp"> <item n="Subsidios Jardines" h="?m=subsidio_jardin" acl="access:subsidio_jardin"/> <item n="Subsidios por Períodos" h="?m=subsidio_periodo" acl="access:subsidio_periodo"/> <item n="Jardines" h="?m=jardin" acl="access:jardin"/> </menu> <item n="Adicionales" h="?m=adicional" acl="access:adicional"/> <item n="Domicilios" h="?m=domicilio" acl="access:domicilio"/> <item n="Liquidaciones (Informe Altas)" h="?m=liquidacion" acl="access:liquidacion"/> <item n="Ley 572" h="?m=ley572" acl="access:ley572"/> </menu> <menu n="Licencias e Inasistencias" acl="role:admin,mesa,licencias,responsable,audit,altas,empleado"> <item n="Licencias" h="?m=v_empleado_min" acl="add:licencia"/> <item n="Buscar en Licencias" h="?m=_licencia" acl="role:licencias,admin"/> <item n="Carga Masiva de Licencias" h="?m=cm_licencia" acl="add:cm_licencia"/> <item n="Tipos de Licencias" h="?m=_t_licencia"/> <item n="Inasistencias" h="?m=inasistencia" acl="role:licencias,altas,mesa,responsable,audit"/> <item n="Feriados" h="?m=_feriado"/> <item n="Ferias Laborales" h="?m=_feria"/> <item n="Períodos" h="?m=_periodo"/> </menu> <menu n="Mesa de Entradas" acl="role:admin,mesa,licencias,responsable,audit"> <item n="Mesa de Entradas" h="?m=_mesa"/> <item n="Tipos de Trámite" h="?m=_t_tramite"/> <item n="Contadores Notas y Dictámenes" h="?m=contador"/> </menu> <!-- <menu n="Estadísticas" acl="role:admin,altas,responsable,audit,consejero,sec_plenario,lectura,mp">--> <menu n="Estadísticas" acl="role:admin,altas,responsable"> <item n="Permanencia en el Cargo" h="?m=v_permanencia" acl="access:v_permanencia"/> <item n="Cargos por Año" h="?m=v_cargosxanio"/> <item n="Cargos por Unidad" h="?m=v_cuenta_cargos"/> <item n="Cargos Actuales" h="?m=v_intersub"/> </menu> <menu n="Configuración" acl="role:admin,mesa,licencias,responsable,altas,audit,consejero,sec_plenario,lectura,mp"> <item n="Tablas Clasificadoras" h="?m=_clasifica"/> </menu> </menu> |
Cálculo de compensatorias vía SQL
El saldo de compensatorias se obtiene de realizar un JOIN entre la licencia, la compensatoria y la licencia que provee los días a compensar. Luego de una sumatoria agrupada por el grupo de la licencia denominado “compensatorias”, xPay provee la solución para obtener los saldos restantes. Este mecanismo permite identificar unívocamente que compensatoria compensa qué licencia. El llamado a la función SUM() provee los saldos como escalares:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
-- Licencia a compensar SELECT l.legajo as legajo, (l.dias * tl.valor) - IFNULL(SUM(comp.dias),0) as restantes, l.ID AS comp_ID, f.ID as feria_ID, concat(f.anio,'/',f.periodo) as feria, tl.descrip as licencia_label, l.fec_desde AS orden, @fecha_base as fecha_base, l.t_licencia_ID as t_licencia_ID, l.dias * tl.valor as total, sum(ifnull(comp.dias,0)) as tomados, l.dias * tl.valor - sum(ifnull(comp.dias,0)) as restantes FROM _licencia AS l JOIN _t_licencia AS tl ON l.t_licencia_ID = tl.ID LEFT JOIN _feria AS f ON l.feria_ID = f.ID -- JOIN con la licencia compensatoria que suministra los días LEFT JOIN ( SELECT licencia_comp_ID, c.dias FROM _compensatoria as c JOIN _licencia as lc on c.licencia_ID=lc.ID and lc.legajo = @legajo JOIN _t_licencia as tlc on tlc.ID = lc.t_licencia_ID AND COALESCE(lc.estado,'') not in ('anulada','rech_inmediato','rech_superior','rech_plenario') WHERE ( DATE_ADD( lc.fec_desde, interval tlc.tope_anual year) >= @fecha_base ) AND lc.fec_desde <= @fecha_base ) AS comp on l.ID = comp.licencia_comp_ID WHERE l.legajo = @legajo AND tl.grupo IS NOT NULL AND tl.valor > 0 AND IFNULL(l.estado,'') not in ('anulada','rech_inmediato','rech_superior','rech_plenario') AND ( DATE_ADD( l.fec_desde, interval tl.tope_anual year) >= @fecha_base OR l.vencimiento >= @fecha_base ) AND l.fec_desde <= @fecha_base -- agrupa las compensatorias por licencia GROUP BY l.ID -- sólo los saldos positivos HAVING restantes > 0 |
Las licencias pueden ser cargadas interactivamente por el usuario, y estas a su vez pueden estar pendientes de aprobación o bien que hayan sido rechazadas o anuladas, es por ello que deben ser filtradas para que no afecten el resultado final de la sumatoria de compensatorias restantes:
1 |
not in ('anulada','rech_inmediato','rech_superior','rech_plenario') |
Análisis detallado de compensatorias restantes para el área de Licencias a través del xPay
Adjunto un video explicativo para el usuario final y descriptivo para ver la operación del sistema:
Validación de Licencias
La carga de licencias tanto sea por el operador en el área administrativa correspondiente como el usuario que carga las licencia a través de miPortal deben estar previamente validadas. Hay una gran cantidad de validaciones que deben realizarse, en principio saldos restantes, límites de cantidad de días tomados al mes, al año o de una sola vez. La enumeración de las validaciones que se realizan son:
- Que tenga un tipo válido de licencia
- Que las fechas no estén invertidas
- Si son guardias o licencias que deban aplicar a guardias existentes
- Que las guardias se correspondan a días de feria
- Que no esté repetida la licencia para un mismo período, del mismo tipo y del mismo empleado
- Que sean superponibles, es decir, que puedan cohexistir en el mismo período de tiempo (con el campo “superponible” = sí)
- Que sean igual o menor a los días restantes
- Que estén dentro de los topes válidos
Estas validaciones son realizadas en el siguiente fragmento de código:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
function check() {/*{{{*/ global $xpdoc; $this->usuario or $this->usuario = $xpdoc->user->user_username; $tl = $xpdoc->get_instance('_t_licencia')->load( array( 'ID' => $this->t_licencia_ID ) ); if ( ! $tl ) { M()->user('ID tipo de licencia invalido'); M()->status( 'ERR' ); return; } $this->dias_habiles = $tl->dias_habiles; $this->fec_desde or $this->get_attr('fec_desde')->now(); /* la licencia puede no tener fec_hasta definido */ M()->info( "tl.sin_fin: $tl->sin_fin" ); if ( ! $this->fec_hasta and ( ! $tl->sin_fin ) ) { $this->fec_hasta = $this->fec_desde; M()->info( "fec_hasta vacio: asignando fec_hasta == fec_desde" ); } if ( $this->fec_hasta and ( $this->fec_desde > $this->fec_hasta ) ) { M()->user( "las fechas de la licencia están invertidas $this->fec_desde $this->fec_hasta, ID: $this->ID" ); M()->status( 'ERR' ); return; } /* chequeo si estan en un periodo valido */ if ( $this->t_licencia_ID == XPAY_GUARDIA or $this->t_licencia_ID == XPAY_INTERRUMPE_FERIA ) { $f = $xpdoc->get_instance('_feria'); if ( ! $f->load( array( 'fec_desde' => '<='.$this->fec_desde, 'fec_hasta' => '>='.$this->fec_hasta ) ) ) { M()->user("las fechas $this->fec_desde y $this->fec_hasta de la guardia no se corresponden con una feria existente"); M()->status('ERR'); return; } $this->feria_ID = $f->ID; } else $this->feria_ID = null; $this->dias = $this->dias_entre_fechas( $this->fec_desde, $this->fec_hasta, $this->dias_habiles ); /* para carga masiva de licencias los proximos chequeos no aplican */ if ( $this->class_name == 'cm_licencia' ) return true; $vlf = $xpdoc->get_instance('v_licencia_full'); /* verifico si repite la misma licencia */ if ( $vlf->load( array( 'legajo' => $this->legajo, 'fec_desde' => $this->fec_desde, 'fec_hasta' => $this->fec_hasta, 't_licencia_ID' => $this->t_licencia_ID, 'ID' => '!'.$this->ID ) ) ) { M()->user( "Ya existe una licencia idéntica en el mimso período, no se pueden repetir" ); M()->status( 'ERR' ); return; } /* verifico si no hay otra licencia no superponible en ese periodo */ if ( $vlf->load( array( 'legajo' => $this->legajo, 'fec_desde' => '<='.$this->fec_desde, 'fec_hasta' => '>='.$this->fec_hasta, 'superponible' => "''|0", 'ID' => '!'.$this->ID ) ) ) { M()->user("En el período entre $this->fec_desde y $this->fec_hasta ya hay una licencia y no es superponible"); M()->status( 'ERR' ); return; } /* topes */ $sl = $xpdoc->get_instance( 's_licencia' ); $sl->load( array( 'legajo' => $this->legajo, 't_licencia_ID' => $this->t_licencia_ID, 'fecha_base' => $this->fec_desde )); M()->debug( "Topes: legajo: $this->legajo, t_licencia_ID: $this->t_licencia_ID, valor: $tl->valor, fecha_base: $this->fec_desde, restan: $sl->restan, restan_mes: $sl->restan_mes" ); $modified = false; $fields = array( 't_licencia_ID', 'fec_desde', 'fec_hasta' ); foreach( $fields as $field ) if ( $this->get_attr( $field )->modified ) $modified = true; M()->info( "modified: ". ( $modified? 'true' : 'false' ) ); if ( $modified and $tl->valor < 0 ) { $dias = $this->dias; M()->info( "restan: $sl->restan" ); if ( ! $this->is_new() ) { /* si esta modificando debe restar el valor anterior para ajustar los topes correctamente */ $ll = $xpdoc->instance('_licencia'); $ll->load( $this->ID ); $dias_ant = $ll->dias; $dias = $dias - $dias_ant; } M()->info( "Dias diferencia: $dias" ); /* para que guarde aun sin estar validado */ $r = false; $msg = "La licencia solicitada de $this->dias dia(s)"; /* tope mensual */ if ( $tl->can_max_mes and $dias > $sl->restan_mes ) { M()->user( "$msg supera el tope mensual de $sl->restan_mes" ); M()->status( 'ERR' ); return $r; } if ( $sl->grupo ) { if ( $dias > $sl->restan ) { M()->user( "$msg supera los dias restantes disponibles $sl->restan" ); M()->status( 'ERR' ); return $r; } } else { if ( $tope->dias and $dias > $sl->restan ) { M()->user( "$msg supera los dias restantes disponibles $sl->restan" ); M()->status( 'ERR' ); return $r; } } if ( $tl->tope_dias and ( $dias > $tl->tope_dias ) ) { M()->user( "$msg supera los dias de tope máximo por vez para tomar ($tl->tope_dias)" ); M()->status( 'ERR' ); return $r; } if ( $tl->topexvez and ( $dias > $tl->topexvez ) ) { M()->user( "$msg supera la cantidad maxima de dias por vez de $tl->topexvez" ); M()->status( 'ERR' ); return $r; } if ( $tl->min_anios_antiguedad or $tl->min_meses_antiguedad ) { $e = $xpdoc->instance('_empleado')->load( $this->legajo ); if ( $e->categoria == XPAY_MAGISTRADO ) { M()->info( "Categoria magistrado, exceptuado de tope de antigüedad" ); } else { $a_f_ingreso = $e->get_attr( 'f_ingreso' ); $f_ingreso = $a_f_ingreso->create( $e->f_ingreso ); $ahora = $a_f_ingreso->create(); $diff = date_diff( $ahora, $f_ingreso ); M()->info( "meses antiguedad: ". ( $en_meses = $diff->y * 12 + $diff->m ) ); M()->info( "minimo meses antiguedad ". ( $min_en_meses = $tl->min_anios_antiguedad * 12 + $tl->min_meses_antiguedad ) ); if ( $en_meses < $min_en_meses ) { M()->user( "$msg requiere de un mínimo de antigüedad de $tl->min_anios_antiguedad años y $tl->min_meses_antiguedad meses" ); M()->status( 'ERR' ); return $r; } } } } return true; }/*}}}*/ |
Los topes son provistos por la consulta “s_licencia” con los totales de las compensatorias o los saldos por tipo de licencia.
Post proceso de las licencias
Una vez validadas y guardadas, en método post_check() se ejecutan nuevas acciones, en principio, las siguientes:
Si se está cargando una licencia compensatoria, se realizará un loop hasta que sean agotadas las compensatorias restantes por su saldo total (que era menor o igual a los días restantes se validó antes en el check() de la función. El listado de las compensatorias restantes es provisto por un lookup sobre la consulta “v_compensatoria” ordenado por fecha ascendente, así tomará las compensatorias más antiguas y proveer el cálculo adecuado.
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
function post_check( $xml = null, $batch = false ) {/*{{{*/ global $xpdoc; if ( $xml ) { $this->load( $xml['ID'] ); M()->debug( "cargando licencia con ID ". $xml['ID'] ); } /* calculo de compensatorias */ if ( $this->t_licencia_ID == XPAY_LICENCIA or $this->t_licencia_ID == XPAY_RAZONES_PART_GUARDIA ) { $batch or M()->debug( "calculando compensatorias para el tipo de licencia con t_licencia_ID $this->t_licencia_ID con dias $this->dias" ); $c = $xpdoc->get_instance( '_compensatoria' ); $batch or $this->borra_comp( $this->ID ); $vcs = $xpdoc->get_instance( 'v_compensatoria' )->load_set( array( 'legajo' => $this->legajo, 'fecha_base' => $this->fec_desde ), null, array( 'orden' => 'ASC' ) ); $restantes = $this->dias; $c->push_privileges( array( 'add' => 1 ) ); if ( ! $vcs->count() ) $batch or M()->warn( "No hay compensatorias restantes para la licencia con ID: $this->ID y t_licencia_ID: $this->t_licencia_ID" ); else { foreach ( $vcs as $vc ) { $batch or M()->debug( "compensatoria ID: $vc->comp_ID, fecha: $vc->orden, dias: $vc->restantes" ); $c->reset(); $c->fill_primary_key(); $c->licencia_ID = $this->ID; $c->licencia_comp_ID = $vc->comp_ID; $c->feria_ID = $vc->feria_ID; $break = false; if ( $restantes <= $vc->restantes ) { $c->dias = $restantes; $break = true; } else { $c->dias = $vc->restantes; $restantes -= $vc->restantes; } /* GE: si no hay feria asociada, es una guardia especial */ $c->descrip = ($vc->feria ? $vc->feria : 'GE') . ( ( $vc->t_licencia_ID == XPAY_INTERRUMPE_FERIA ) ? '/IF' : '') .': '. $c->dias; $c->insert(); if ( $break ) break; } } } if ( $batch ) return; }/*}}}*/ |
Para las licencias que interrumpen feria ejecuta la funcion “consulta_interrumpe_feria()” que es un llamado a una consulta dentro de esa función. Esto agrega automáticamente un tipo de licencia que se llama “interrupción por feria” para ajustar los saldos en forma adecuada.
Por otra parte para las razones particulares en guardia, se debe descontar un día del stock de licencias compensatorias, ya que se efectúa la liquidación de esa licencia durante una feria, no computa entonces positivamente. Para ello se realiza un llamado a la función consulta_razones_particulares_en_guardia() que también posee una consulta SQL.
El uso de consultas SQL completas dentro del código PHP permite hacer las operaciones más rápidas y el código resulta independiente de la implementació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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
$tl = $xpdoc->get_instance('_t_licencia'); /* si no esta cargado el tipo de licencia, que la cargue */ $tl->ID == $this->t_licencia_ID or $tl->load( array( 'ID' => $this->t_licencia_ID ) ); /* interrumpe feria */ if ( $tl->inter_feria ) { $if = XPAY_INTERRUMPE_FERIA; $this->Execute( "DELETE from _licencia WHERE licencia_relac_ID = '$this->ID' AND t_licencia_ID = '$if'" ); $this->fec_hasta and $this->consulta_interrumpe_feria( $this->ID ); } /* razones particulares en guardia */ if ( $this->t_licencia_ID == XPAY_RAZONES_PART ) { $rpg = XPAY_RAZONES_PART_GUARDIA; $licencia_relac_ID = $this->ID; $this->Execute( "DELETE from _licencia WHERE licencia_relac_ID = '$licencia_relac_ID' AND t_licencia_ID = '$rpg'" ); $fd = $this->fec_desde; $fh = $this->fec_hasta; $l = $xpdoc->instance( '_licencia' ); if ( $l->load( array( 'legajo' => $this->legajo, 't_licencia_ID' => XPAY_GUARDIA ) , "( '$fd' BETWEEN _licencia.fec_desde AND _licencia.fec_hasta OR '$fh' BETWEEN _licencia.fec_desde AND _licencia.fec_hasta OR _licencia.fec_desde BETWEEN '$fd' AND '$fh' OR _licencia.fec_hasta BETWEEN '$fd' AND '$fh' )" ) ) { $guardia_ID = $l->ID; M()->info( "guardia_ID = $guardia_ID" ); $l->reset(); $l->fill_primary_key(); $l->consulta_razones_particulares_en_guardia( $l->ID, $licencia_relac_ID, $guardia_ID ); $l->load($l->ID) and $l->post_check(); } } |
en la opción “Tipos de Licencias” del xPay
http://xpay.jusbaires.gov.ar/xpay/?m=_t_licencia
con el campo “dias_prev” se configura con un número negativo cuantos días de plazo se tiene para solicitar una licencia ya tomada. El xPay permite especificar licencia por licencia cuantos días se permitirá solicitar una licencia ya tomada anterior.
El mantenimiento de estas variables resulta central para la operación de los límites y topes del sistema, deben gozar “de buena salud” y ser mantenidos cada vez que cambie la regulación marco de la solicitud de licencias.
Los días previos (dias_prev) son siempre negativos. Para los numeros positivos el sistema inhibira de cargar la licencia hacia el futuro.
Flujo de aprobación de licencias
El flujo de aprobación consiste en emular la cadena de autorizaciones que se da en un proceso manual ahora automático. El mecanismo en general consiste en sucesivas aprobaciones que son notificadas a través de correo electrónico. El usuario solicita la licencia a través del portal y se disparan los eventos que esto conlleva, en principio notificar al jefe inmediato superior que tiene una licencia para aprobar. EL sistema contempla varios niveles de aprobación:
- jefe inmediato
- jefe superior
- licencias (valida si la licencia es correcta)
- plenario (para aquellas licencias que lo requieran)
En los pasos está la posibilidad de aprobar el pedido como también de rechazar en todos los casos se notifica al interesado.
xPay tiene una función que se ocupa de implementar el flujo de aprobación del workflow. La función consiste básicamente en una secuencia if … else if … else, con una entrada por cada paso del workflow donde primero se valida (a través de la propia sentencia condición de if) y luego se asignan los valores correspondientes, se notifica y se guarda el nuevo estado de la licencia. Cada bloque tienen estructuras similares (de acuerdo al tipo de validación) y termina con la remisión del email para “empujar” el flujo a su nuevo estadío.
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
function flow( $accion, $licencia_ID = null ) {/*{{{*/ if ( false === in_array( $accion, array ( 'aprobar', 'rechazar', 'anulada' ) ) ) { M()->error( "Accion \"$accion\" no soportado" ); return; } /* quedo deshabilitado, lo dejo por las dudas: el workflow se mueve por aprobar o rechazar if ( false === in_array( $estado, array ( 'aprobada', 'anulada', 'rech_inmediato', 'rech_superior', 'rech_pelanario', 'pend_datos', 'pend_inmediato', 'pend_superior', 'pend_licencias', 'pend_plenario' ) ) ) { M()->error( "Estado \"$estado\" no soportado" ); return; } */ global $xpdoc; // $licencia_ID and ( $licencia_ID != $this->ID ) and $this->set_flag( 'main_sql', false ); $this->load( $licencia_ID ); if ( ! $this->ID ) { M()->user( "No se encuentra la licencia con el ID $licencia_ID" ); return; } $this->set_flag( 'main_sql', store ); $tl = $xpdoc->get_instance( '_t_licencia' ); if ( ! $tl->load( $this->t_licencia_ID ) ) { M()->user( "tipo de licencia invalido" ); return; } $vem = $xpdoc->instance('v_empleado_min'); M()->info( "accion: $accion, licencia ID: $this->ID, t_licencia_ID: $this->t_licencia_ID, estado actual: $this->estado, aprueba plenario: $tl->aprueba_plenario" ); /* se agregan al objeto actual, luego serializa y van a aparecer en la transformacion */ $this->attr('titulo')->set('virtual', true )->set('value','<< xPay/miPortal >> Novedades sobre la solicitud de Licencia'); $this->attr('link')->set('virtual', true )->set('value','http://miportal.jusbaires.gov.ar'); /* shortcuts */ $xul = $xpdoc->user->legajo; $e = null; // estado al que pasa la licencia $a = null; // destinatario de la notificacion $ac = $accion; /* inicialmente estos valores */ $d = $this->legajo; // ID // $dl = $this->legajo_label; // label $te = $this->estado; $params = array(); /* empleado */ if ( $te == '' and $ac == 'aprobar' and $this->legajo == $xul ) { $e = 'pend_inmediato'; $a = $vem->load( $this->autoriza_ID )->usuario; M()->info( "1: $ac -- $te >> $e, autoriza_ID: $a" ); /* jefe inmediato */ } else if ( $te == 'pend_inmediato' and $ac == 'rechazar' and $this->autoriza_ID == $xul ) { $e = 'rech_inmediato'; $a = $vem->load( $this->autoriza_ID )->usuario; M()->info( "2: $ac -- $te >> $e, autoriza_ID: $a" ); $params['nombre'] = $xpdoc->user->nombre; $params['legajo'] = $xpdoc->user->legajo; $params['email'] = $xpdoc->user->user_username; } else if ( $te == 'pend_inmediato' and $ac == 'aprobar' and $this->autoriza_ID == $xul ) { $e = 'pend_superior'; $a = $vem->load( $this->autoriza_sup_ID )->usuario; M()->info( "3: $te >> $e, autoriza_ID: $a" ); /* jefe superior */ } else if ( $te == 'pend_superior' and $ac == 'rechazar' and $this->autoriza_sup_ID == $xul ) { $e = 'rech_superior'; $a = $vem->load( $this->autoriza_sup_ID )->usuario; M()->info( "4: $te >> $e, autoriza_sup_ID: $a" ); } else if ( $te == 'pend_superior' and $ac == 'aprobar' and $this->autoriza_sup_ID == $xul ) { $e = 'pend_licencias'; /* no hay destinatario de email para licencias */ $a = $xpdoc->config->email_licencias; M()->info( "5: $te >> $e, autoriza_sup_ID: $xul" ); /* licencias */ } else if ( $te == 'pend_licencias' and $ac == 'aprobar' and $this->has_role( 'licencias' ) and ! $tl->aprueba_plenario ) { $e = 'aprobada'; $a = $xpdoc->config->email_licencias; $this->get_attr('fec_otorga')->now(); M()->info( "6: $te >> $e, rol: licencias, NO aprueba plenario" ); } else if ( $te == 'pend_licencias' and $ac == 'rechazar' and $this->has_role( 'licencias' ) ) { $e = 'pend_datos'; $a = $xpdoc->config->email_licencias; M()->info( "7: $te >> $e, rol: licencias" ); } else if ( $te == 'pend_licencias' and $ac == 'aprobar' and $this->has_role( 'licencias' ) ) { $e = 'pend_plenario'; $a = $xpdoc->config->email_presidencia; M()->info( "8: $te >> $e, autoriza_sup_ID: $xul" ); /* plenario */ } else if ( $te == 'pend_plenario' and $ac == 'aprobar' and $this->has_role( 'sec_plenario' ) and $tl->aprueba_plenario ) { $e = 'aprobada'; $a = $xpdoc->config->email_presidencia; $this->get_attr('fec_otorga')->now(); M()->info( "9: $te >> $e, aprueba plenario" ); } else if ( $te == 'pend_plenario' and $ac == 'rechazar' and $this->has_role( 'sec_plenario' ) and $tl->aprueba_plenario ) { $e = 'rech_plenario'; $a = $xpdoc->config->email_presidencia; M()->info( "10: $te >> $e, aprueba plenario" ); } else if ( strstr( $te, 'pend_' ) !== false and $ac == 'anulada' and $this->legajo == $xul ) { $e = 'anulada'; $a = $vem->load( $this->autoriza_ID )->usuario; M()->info( "11: $ac -- $te >> $e" ); } else { M()->error( "la accion de workflow $ac no macheo ninguna regla" ); if ( $e == $te ) M()->error( "La licencia ya se encuentra en ese estado: $te" ); else M()->error( "Cambio de estado de [$te] a [$e] no autorizado" ); return; } $this->estado = $e; $this->push_privileges( array( 'edit' => 1 ) ); $this->update(); M()->user( "Cambio realizado con exito" ); M()->status( 'OK' ); $params = array( 'email' => $xpdoc->user->user_username, 'seccion' => 'empleado' ); $this->genera_notificacion( $params ); /* agente adicional que es informado (jefe, superior, secretaria plenario, etc) */ if ( $a ) { $params = array( 'email' => $a, 'seccion' => 'responsable' ); $this->genera_notificacion( $params ); } return true; }/*}}}*/ |
Generación de Notificaciones para el Flujo de Aprobación de Licencias
La notificación en si es muy clara y fácil de mantener. Están separados los componentes para ofrecer flexibilidad al momento de realizar cambios. El código que sigue, parte del controlador ejecutándose es una función que genera la notificación, que será grabada en un registro hasta que un proceso en el CRON envíe las notificaciones (hay que configurar el sistema operativo, el archivo /etc/crontab o similar para poder activar esto) cada 5 minutos.
La función específica de generar la notificación serializa el objeto actual (una licencia) y la transforma con un template (miportal/licencia/email). Los parámetros adicionales quedan en la función $param donde se enlazan datos del PHP al XML; se asignan las variables $n de la notificación y luego se guarda para ser remitida luego:
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 26 27 28 29 30 31 32 |
function genera_notificacion( $params ) {/*{{{*/ global $xpdoc; if ( ! $params['email'] ) { M()->error( "no hay un email asocidado al destinatario, no se puede notificar" ); return false; } $n = $xpdoc->get_instance( 'notificacion' ); $n->reset(); $xdoc = $this->serialize_row( DS_ANY ); $n->xml_data = $xdoc->asXML(); $n->contenido = $xpdoc->transform( 'miportal/licencia/email', $xdoc, $params, 'bridge', false ); $n->module = $xpdoc->module; $n->key = $this->pack_primary_key(); $n->titulo = $this->titulo; $n->email = $params['email']; strstr( $n->email, '@' ) or $n->email .= '@jusbaires.gov.ar'; $n->get_attr('fecha_hora')->now(); $n->fill_primary_key(); $n->push_privileges( array( 'add' => 1 ) ); $n->store(); return $n; } /*}}}*/ |
Notificaciones en email vía XSL
La transformación se realiza tomando como raíz un objeto serializado de la tabla “licencia”: las entidades locales son atributos del la solicitud en proceso de aprobación. Los parámetros quedan entonces como variables iniciadas con $
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<xsl:template match="_licencia" mode="textos"><!--{{{--> <xsl:variable name="link_historial" select="concat('http://',$host,'/miportal/#','?m=_licencia&v=miportal/licencia/historial&f[include_dataset]=2&s[_licencia][legajo]=',legajo,'&o[_licencia][fec_desde]=desc')"/> <xsl:variable name="link_pendientes" select="concat('http://',$host,'/miportal/#?m=v_licencia_aprueba&v=miportal/licencia/pendientes&f[include_dataset]=2&o[_licencia][timestamp]=desc')"/> <p class="lead"> <xsl:choose> <xsl:when test="$seccion='responsable'">El empleado <xsl:value-of select="legajo/@label"/> ha solicitado una <strong><xsl:value-of select="t_licencia_ID/@label"/></strong> que se inicia en la fecha <strong><xsl:value-of select="fec_desde"/></strong> hasta la fecha <strong><xsl:value-of select="fec_hasta"/></strong> contabilizándose un total de <strong><xsl:value-of select="dias"/> día(s) <xsl:apply-templates select="dias_habiles"/></strong>. Informamos que se ha procesado su solicitud y que la misma se encuentra en el el estado de <strong><xsl:variable name="estado" select="estado"/><xsl:value-of select="document('estado_licencia.xml')//estado[@e=$estado]/@t"/></strong>. Para aprobar o rechazar esta solicitud, <a href="{$link_pendientes}">por favor haga click aquí</a></xsl:when> <xsl:otherwise>Ud. ha recibido el siguiente email en respuesta a la solicitud de <strong><xsl:value-of select="t_licencia_ID/@label"/></strong> que se inicia en la fecha <strong><xsl:value-of select="fec_desde"/></strong> hasta la fecha <strong><xsl:value-of select="fec_hasta"/></strong> contabilizándose un total de <strong><xsl:value-of select="dias"/> día(s) <xsl:apply-templates select="dias_habiles"/></strong>. Informamos que se ha procesado su solicitud y que la misma se encuentra en el el estado de <strong><xsl:variable name="estado" select="estado"/><xsl:value-of select="document('estado_licencia.xml')//estado[@e=$estado]/@t"/></strong>. Para ver el estado de sus licencias, <a href="{$link_historial}">por favor haga click aquí</a></xsl:otherwise> </xsl:choose> </p> </xsl:template><!--}}}--> |
Acceso al legajo digital del empleado
Para los legajos xPay migra los datos de las carpetas del software ADB utilizado por el organismo para la digitalización y acceso a los documentos digitales (aún se sigue haciendo la carga en un solo puesto de trabajo a través de este). El procedimiento de incorporación de los documentos visibles debe realizarse a través de la operación del método cargar_imágenes que busca en el filesystem de red los escaneos que resulta trivial para el usuario. El siguiente video explicativo para el usuario muestra cómo se accede, se busca en el sistema el empleado y se cargan sus imágenes o las últimas novedades (es necesario realizar esta operación si se ha actualizado el ADB se quieren reflejar esos nuevos documentos en el sistema):
Exportar Imágenes a PDF vía system()
Un legajo o expediente puede contener fácilmente más de 500 imágenes a escanear: esto desafía al sistema al responder inclusive con una respuesta rápida al usuario. Las liberías PHP que proveen herramientas para la generación de PDF lo hacen todo en memoria, saturando rápidamente el servidor. Para ello la mejor solución resulta hacer un llamado al sistema y proveer todos los parámetros de generación de expedientes. El comando “tiffcp” provee esta solución en forma adecuada al utilizar buffers en archivos o bien escribir directamente sobre el resultado final y así obtener un expediente con mucha cantidad de páginas. La colección de los nombres de archivos como parámetros de la instrucción se fabrica a partir de un group_concat() directamente sobre la consulta SQL. Seteando “group_concat_max_len” hace que los devuleva todos en un solo string. La constante XPAY_CAN_MAX_PDF_EXPORT limita la cantidad de archivos a exportar:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
function exportar_pdf( $xmls ) {/*{{{*/ define( "XPAY_CAN_MAX_PDF_EXPORT", 1000 ); /* se necesita instalar la libreria libtiff-tools para que funcione */ global $xpdoc; $legajo = $xpdoc->http->legajo; /* M()->user( "legajo: ". $legajo ); */ /* NFS mountpoint */ $base_path = $xpdoc->config->ADD_images_path; M()->info( "base_path: $base_path" ); $cmd = array( "tiffcp" ); $tmp_name = tempnam("/tmp", "FOO"); M()->info( "tmp_name: $tmp_name" ); /* files de 100 bytes x 1000 archivos */ $this->execute( "SET SESSION group_concat_max_len = 100000" ); if ( ( $t = current( $this->execute( "select count(*) from imagen where usuario_ID='$legajo'" ) ) ) > XPAY_CAN_MAX_PDF_EXPORT ) { M()->user( "La cantidad máxima de documentos a exportar es de ". XPAY_CAN_MAX_PDF_EXPORT. ", requiriendo $t: no se puede exportar" ); return null; } $cmd[] = current( $this->execute("select group_concat('$base_path', dirname, '/', basename order by filename separator ' ') from imagen where usuario_ID='$legajo'" )->fetch() ); $cmd[] = $tmp_name.".tiff"; $command = implode( " ", $cmd ); M()->user( $command ); system( $command ); $command = "tiff2pdf -o $tmp_name.pdf $tmp_name.tiff"; system( $command ); M()->info( "command: $command" ); /* debe anticipar el output del archivo */ header('Content-Type: application/pdf'); readfile( "$tmp_name.pdf" ); unlink( "$tmp_name.tiff" ); unlink( "$tmp_name.pdf" ); $xpdoc->set_view( "tiff" ); }/*}}}*/ |
Finalmente son borrados los archivos temporales..
Generación de PDF vía FOP
FOP provee una herramienta muy valiosa para la generación de PDF con un lenguaje XML. Para la información sobre FOP revisar http://xmlgraphics.apache.org/fop/quickstartguide.html. En el “<template match=”/”>” se definen las particularidades de la página, en este caso una hoja del tipo A4, los colores de las regiones y los márgenes. Con fo:flow se define el flujo de datos pricipal, aqui el template directamente sobre el objeto //c_/_licencia:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" version="2.0"> <xsl:template match="/"><!--{{{--> <fo:root> <fo:layout-master-set> <fo:simple-page-master master-name="A4"> <fo:region-body region-name="only_region" margin="0.5in" background-color="#FFFFFF"/> <fo:region-after region-name="footer-main" margin="0.5in" extent="2in"/> </fo:simple-page-master> </fo:layout-master-set> <fo:page-sequence master-reference="A4"> <fo:static-content flow-name="footer-main"> <fo:block> <!-- <fo:page-number/> --> </fo:block> </fo:static-content> <fo:flow flow-name="only_region" font-size="12pt"> <xsl:apply-templates select="//c_/_licencia"/> </fo:flow> </fo:page-sequence> </fo:root> </xsl:template><!--}}}--> <xsl:template match="_licencia"><!--{{{--> <fo:block border="1px solid black" page-break-before="always"> <xsl:call-template name="encabezado"/> <xsl:apply-templates select="." mode="firma"/> </fo:block> ... </xsl:stylesheet> |
El resultado será el siguiente, la planilla de licencias que el xPay y miPortal proveen para la firma y remisión a licencias como método manual alterntavo:
Llamado vía la interfaz ext a un documento PDF
Este ejemplo muestra como asociar un botón del objeto liquidación a un URL en donde se codifica el llamado al template FO
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 26 27 28 |
<table name="liquidacion" translate="Liquidaciones"><!--{{{--> <panel type="xpGrid"><config><![CDATA[listeners: { beforerender: function() { var panel = this; var tbar = panel.getTopToolbar(); if ( tbar ) { var button = new Ext.Toolbar.Button( { /* icon: '/ux/images/application_form_magnify.png', */ text: 'Imprimir Informe', cls: 'x-btn-text', menuAlign: 'tr?', tooltip: 'Presione aquí para imprimir', listeners:{click:{scope:panel, fn:function() { var sk = []; Ext.each( this.selModel.getSelections(), function( s ) { sk.push( s.get('ID') ); }); window.open('?m=liquidacion&f[transform]=fo&v=liquidacion&f[include_dataset]=6&s[liquidacion][ID]='+sk.join('|'), '_blank'); },buffer:200}} }); tbar.insert( tbar.items.length -2, '-' ); tbar.insert( tbar.items.length -2, button ); } }}]]></config> </panel> </table><br> |
MiPortal
La implementación del MiPortal es simplemente cambiar el parámetro del template a los xpotronix. Es un “skin” del xpay, un template, una nueva forma de mostrar la aplicación a través de los templates de bootstrap que proveen una interfaz moderna y apta para celulares. La implementación del template es la SmartAdmin, un completo formato para el desarrollo de paneles de control, muy útil para un portal con datos personales.
miPortal en celulares
Selección automática del template para mostrar MiPortal en base al URL
Para alternar entre un portal y el otro (el xPay y miPortal) en common.php dentro de code.xml se hace la elección en base al URL:
1 2 3 4 5 6 7 8 9 10 11 |
/* hace override de la variable 'v' para que tome el template de acuerdo al path Necesita que Alias este configurado en apache2.conf */ if ( ( ! $xpdoc->http->v ) and ( preg_match( '#miportal#si', $_SERVER['PHP_SELF'] ) or preg_match( '#miportal#si', $_SERVER['SERVER_NAME'] ) ) ) { $xpdoc->http->v = 'miportal/main'; } |
Template MiPortal
Grillas de Datos en XSL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<section id="widget-grid" class=""> <div class="row"> <article class="col-xs-12 col-sm-12 col-md-12 col-lg-12"> <xsl:apply-templates select="$metadata//obj[@name='_licencia']" mode="data-grid"> <xsl:with-param name="fields" select="'t_licencia_ID,estado,autoriza_ID,obs_empleado,ID'"/> <xsl:with-param name="id" select="'licencia_historial'"/> <xsl:with-param name="title" select="'Historial Licencias'"/> <xsl:with-param name="data" select="$dataset//obj[@name='_licencia']"/> </xsl:apply-templates> </article> </div> </section> |
Validación y envío de formularios para el workflow de aprobación
El template SmartAdmin provee una interfaz para la incorporación de formularios a través de la definición de un objeto y todas las funciones de overdrive. Estas últimas manejan la respuesta del ajaxSubmit:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
function runFormValidation() {/*{{{*/ var licencia_form = $("#licencia_form").validate({ rules: { fec_desde: { required: true }, fec_hasta: { required: true }, 'autoriza_ID': { required: true }, 'autoriza_sup_ID': { required: true } }, messages: { fec_desde: { required: 'Debe indicar la fecha de inicio de la licencia' }, fec_hasta: { required: 'Debe indicar la fecha de fin de la licencia' }, autoriza_ID: { required: 'Debe ingresar la persona que autoriza la licencia' }, autoriza_sup_ID: { required: 'Debe ingresar la persona que autoriza la licencia' } }, submitHandler : function(form) { // $("button[type=submit]").attr("disabled", "disabled"); $(form).ajaxSubmit({ beforeSend: function( xhr, settings ) { if ( ! $("#autoriza_ID").val() ) { $("#msgs_txt").html( "Debe especificar un Jefe Inmediato que autorice esta licencia" ); $('#msgs').removeClass('hide'); xhr.abort(); return false; } if ( ! $("#autoriza_sup_ID").val() ) { $("#msgs_txt").html( "Debe especificar un Jefe Superior que autorice esta licencia" ); $('#msgs').removeClass('hide'); xhr.abort(); return false; } // debugger; /* if ( $.inArray(parseInt($("#select\\.t_licencia_ID" ).val()), t_cert_IDs) != -1 ) { if ( ! $("#cert_img").val() ) { $("#msgs_txt").html( "Debe adjuntar un archivo o foto del certificado que adjunta" ); $('#msgs').removeClass('hide'); xhr.abort(); return false; } if ( ! $("#select\\.t_cert_ID").val() ) { $("#msgs_txt").html( "Debe especificar el tipo de certificado que adjunta" ); $('#msgs').removeClass('hide'); xhr.abort(); return false; } } */ }, success : function( a, b, c ) { // $("#licencia_form").addClass('submited'); if ( a.status[0] == 'ERR' ) { $("#msgs_txt").html( a.msg.join(', ') ); $('#msgs').removeClass('hide'); } else { $("#msgs_txt").html( '' ); $('#msgs').addClass('hide'); $.SmartMessageBox({ title: "<i class='fa fa-refresh' style='color:green'></i> " + ( a.status[0] == 'ERR' ? 'Error!' : 'OK!' ), content: a.msg.join(', '), buttons: '[Ok]' }, function (b) { if (b == "Ok") { var next_url = $("li#default a").attr("href"); window.location.hash = next_url; checkURL(); } }); } }, error: function( a, b, c ) { $.SmartMessageBox({ title: "<i class='fa fa-refresh' style='color:green'></i> Error", content: "Hubo un error en la comunicación con el servidor, por favor, intente nuevamente", buttons: '[Ok]' }, function (b) { if (b == "Ok") { /* alert("OK"); */ } }); } }); }, errorPlacement : function(error, element) { error.insertAfter(element.parent()); } }); }/*}}}*/ |
Claves foráneas en base a consultas para el Informe de Liquidaciones
Las consultas de liquidaciones requieren de claves foráneas complejas. Xpotronix provee una interfaz para poder generar fácilmente cuál es la consulta que proveerá el set de datos que será serializado y luego transformado un un PDF:
1 2 3 4 5 6 7 8 |
<foreign_key passive="1"> <ref expr="(((v_intersub_ca.liquidacion_ID IS NOT NULL OR v_intersub_ca.liquidacion_ID <> '') AND (v_intersub_ca.liquidacion_ID = '$liquidacion.ID')) OR ('$liquidacion.solo_periodo_explicito' <> '1' AND ((v_intersub_ca.desde BETWEEN '$liquidacion.fec_desde' AND '$liquidacion.fec_hasta') OR (v_intersub_ca.hasta BETWEEN '$liquidacion.fec_desde' AND '$liquidacion.fec_hasta')) ))"/> </foreign_key> |
Lista de deseos (wishlist) para xPay
- Migrar las tablas clasificadoras originales del Payroll y realizar la sincronización inversa para actualizar en xPay
- Completar el módulo de “Altas de Empleado” y sincronizar Payroll
- Mejor módulo de organigrama
- Liquidaciones en xPay y desafectar Payroll
- Brindar mejores estadísticas
- Calendario de licencias