Tags: openerp

(Spanish version) “Retocando” el modulo de activos…

08 Nov 2012

Módulo de Vacaciones OpenERP 6.1

30 Oct 2012

Una vez elegida OpenERP como la herramienta que mejor se adapta a nuestras necesidades, con frecuencia nos encontraremos con la necesidad de adaptar alguno de los módulos disponibles a los requisitos específicos de nuestra empresa.

Haciendo un resumen rápido de lo que tenemos que tener en cuenta antes de elegir qué herramienta se adecua mejor a nuestra empresa y que “verdaderamente” necesitamos para cumplir todos los procesos de nuestro negocio ( esta ultima frase por la que hemos pasado casi de puntillas, quizás sea la fundamental o una de ellas para llevar a cabo una instalación de la herramienta con éxito y sin demasiados quebraderos de cabeza ), tenemos que tener asimilado que los objetivos de cualquier ERP son :

  • Optimización de los procesos empresariales.
  • Acceso a la información.
  • Posibilidad de compartir información entre todos los componentes de la organización.
  • Eliminación de datos y operaciones innecesarias de reingeniería

Además, un ERP como tiene que ser configurable y modular. Una vez elegida OpenERP como la herramienta que mejor se adapta a nuestras necesidades, con frecuencia nos encontraremos con la necesidad de adaptar alguno de los módulos disponibles a los requisitos específicos de nuestra empresa.

Con todo ello, deberemos probar la herramienta para tener claros las siguientes ideas fundamentales:

  • Qué módulos necesitaremos y cuáles no ( y por tanto no deberemos de instalarlos)
  • Realizar o pedir sólo los desarrollos de los módulos que verdaderamente necesitemos. En este apartado tenemos que hacernos a la idea que en muchos casos deberemos adaptar los procesos a la manera-estructura de OpenERP y no a la inversa, porque se trata de “configurar”-“customizar” la herramienta y no “crear” una nueva.

Al hilo de lo escrito, comentaremos el desarrollo de un modulo para gestionar vacaciones a petición de las necesidades de un cliente.

El módulo que desarrollamos fue una extensión al módulo vacaciones “hr_holidays” cuyo autor es OPENERP S.A. , que gestiona las vacaciones de los empleados del sistema. Este modulo basa su comportamiento en la asignación de días de ausencia aprobados por los responsables del departamento de RRHH y que los empleados van gastando en forma de “crédito”, es decir, por cada tipo de ausencia el empleado tiene derecho a un numero determinados de días que se podrá ir incrementando o disminuyendo en función del caso. Aún con la pobre definición de la funcionalidad que acabamos de hacer del módulo, refleja de una manera bastante cercana a como se comporta realmente ya que deja muchas lagunas para una mínima gestión de cualquier departamento de RRHH.

En una empresa pequeña de no mas de 5 empleados podría bastar el módulo para el personal pero a partir de un número mayor ya el gestionar vacaciones con dicho módulo no sería eficiente debido a:

  • No existen calendarios laborales ni festivos, en el momento que dos empleados o mas que puedan pertenecer a lugares de trabajo distinto (con diferente calendario laboral) sería un problema.
  • La gestión de las vacaciones en el cambio de año tampoco existe y corregir de año en año las vacaciones de cada empleado si éste es alto, tambien conllevaria un tiempo ineficiente.
  • En relación con el primer punto sólo existe la posibilidad de contar los días como naturales, dejando atrás la posibilidad que en dos puntos de trabajos distintos se cuentes de una manera u otra (O incluso como hemos desarrollado nosotros, el conteo natural o laboral va asociado al tipo de ausencia y dependiendo de ella, contara dias naturales o laborales).

A parte de esta situación inicial fue básico a la hora de hacer el desarrollo (y en verdad también para cualquier otro) saber manejar bien las fechas en python, concretamente la clase time y datetime y el comportamiento de sus dos metodos mas utlizados : strftime() y strptime(). También para la problemática del conteo de los días hábiles hicimos uso del método isoweekday() de la clase datetime para saber que dia es de la semana dada una fecha..

Con esta problemática, el punto de partida de nuestro desarrollo es el siguiente; creamos dos nuevas identidades calendario_laboral y festivo. Calendario_laboral tendrá una relación one2many con hr_employee y también tendrá una relación many2many con la entidad festivo. Con ello podremos crear múltiples calendarios laborales que podrán poseer los festivos correspondientes, cada año sólo deberemos meter los festivos una vez para luego asociarlos a calendarios sucesivos. A su vez un mismo calendario podré asociarlo también a los empleados que correspondan, así podremos contear las vacaciones en función del calendario que tenga el empleado asignado.

En una primera versión el conteo de los días hábiles o no, lo asociamos a los calendarios laborales pero luego decidimos asociárselo al tipo de ausencia de manera que crearemos un nuevo campo check “dias_habiles” que si esta marcado descontará los días hábiles que hay en el intervalo de tiempo que ha pedido las vacaciones el empleado. En el mismo objeto, crearemos también un checkbox “lanzar_fin” que será el que controle si en el wizard que crearemos para gestionar el cambio de año y la compensación de vacaciones, se creen o no las nuevas asiganaciones de vacaciones para los empleados.

    def dias_habiles(self, date_from, date_to):

        res = 0
        DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
        date_from = datetime.datetime.strptime(date_from, DATETIME_FORMAT)
        date_to = datetime.datetime.strptime(date_to, DATETIME_FORMAT)
        date_from = datetime.datetime.date(date_from)
        date_to = datetime.datetime.date(date_to)
        while( date_from <= date_to):

            if(datetime.datetime.isoweekday(date_from)== 7 or datetime.datetime.isoweekday(date_from)== 6 ):
                res = res + 1
            date_from = date_from + relativedelta(days=+1)
        return res

Y con esto contaremos las vacaciones con el siguiente método:

    def _get_number_of_days(self, cr, uid, id, date_from, date_to, employee_id):
        """Returns a float equals to the timedelta between two dates given as string."""

        DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
        from_dt = datetime.datetime.strptime(date_from, DATETIME_FORMAT)
        to_dt = datetime.datetime.strptime(date_to, DATETIME_FORMAT)
        timedelta = to_dt - from_dt
        diff_day = timedelta.days
        calendario_obj = self.pool.get("hr.holidays.calendario")
        id_cal = calendario_obj.search(cr, uid,[('employee_ids', '=',employee_id)])
        if id_cal:
            calendario = calendario_obj.browse(cr, uid,[id_cal[0]])[0]
            val_intervalo = []
            for festivos in calendario.festivos:
                val_intervalo.append(datetime.datetime.date(datetime.datetime.strptime(festivos.fecha,"%Y-%m-%d")))
            d1 = datetime.datetime.date(from_dt)
            d2 = datetime.datetime.date(to_dt)
            for elem in val_intervalo:
                if elem >= d1 and elem <= d2:
                            diff_day = round(diff_day) - 1
        hol = self.pool.get("hr.holidays.status")
        hol_obj = hol.browse(cr, uid, id)
        if hol_obj.id != False:
            if hol_obj.laborales == True:
                res = self.dias_habiles(date_from, date_to)
                diff_day = diff_day - res

        return diff_day

La idea del asistente de fin de año es regularizar las vacaciones de manera correcta de un año para otro. Para ello una vez que se hubiesen creado las vacaciones de la empresa con sus días correspondientes, si marcamos “lanzar_fin” el asistente creado volverá a reponer los días de disfrute hasta los marcados de nuevo. Resaltar en este apartado, que la única problemática que se nos planteó fue con las Vacaciones, ya que podrían darse 3 casos (supongamos que nuestra empresa tiene derecho a 30 días naturales)

  • Un empleado gasta 30 días —————-> se le asignaran de nuevo 30
  • Un empleado gasta 27 días —————-> se le debe asignar 33
  • Un empleado gasta 34 días —————-> se le debe asignar 26

En un primer acercamiento, se planteó el método de manera que en función de las vacaciones disfrutadas del año anterior se le creaba una nueva asignación con ese número de días. Pero esto solo será válido para el nuevo cambio de año y no en sucesivos, ya que el sistema de esta manera no puede diferenciar si un empleado gasta más o menos vacaciones porque le corresponde (de años anteriores) o porque los ha adelantado, y no actuará en consecuencia. Por tanto el sistema siempre asociará 30 días ( o los correspondientes) y controlará un nuevo tipo de ausencia creada por el modulo, “compensación de vacaciones” de manera que si alguien quiere gastar días de más del año que viene se le deberá asociar a este tipo de ausencia y en consecuencia creará una asignación negativa en los 30 días de vacaciones ( y por tanto se le descontarán las vacaciones anticipadas)

    def regulariza_vacaciones(self,cr,uid,ids,context):

        mes_actual=datetime.datetime.today().strftime("%m")
        dia_actual=datetime.datetime.today().strftime("%d")
        anio_actual=datetime.date.today().strftime("%Y")
        anio_anterior = str(int(anio_actual) - 1)

            # Year change can only be executed first 31 days in January
        if int(mes_actual) == 10 and int(dia_actual)<31:

                holidays = self.pool.get('hr.holidays')
                holidays_obj = self.pool.get('hr.holidays.status')
                holidays_ids = holidays_obj.search(cr, uid,[('lanzar_fin','=',True)])

                category_obj = self.pool.get('hr.employee.category')
                categoria = category_obj.search(cr,uid,[('name','=','Todos')])

                obj_emp = self.pool.get("hr.employee")
                emp_ids = obj_emp.search(cr, uid, [('category_ids',"=" , categoria[0])])

                for hol in holidays_obj.browse(cr, uid, holidays_ids):

                    for emp in emp_ids:

                        resto = holidays._get_remaining_days_by_holidays(cr, uid, emp, hol.id, anio_anterior)
                        if resto < 0:
                            resto = -resto
#                            if resto > hol.ndias:
#                                exc = resto - hol.ndias
#                                resto = hol.ndias - exc
                            if hol.name == "Vacaciones":
                                resto = hol.ndias
                            if hol.name == "Compensacion Vacaciones":
                                resto = -resto
                                hol.name == "Vacaciones"
                                hol.id = holidays_obj.search(cr, uid,[('name','=',"Vacaciones")])[0]

                        values = {
                                  'name' : hol.name,
                                  'holiday_type' : 'employee',
                                  'employee_id': emp,
                                  'type':'add',
                                  'holiday_status_id': hol.id,
                                  'state' : 'validate',
                                  'number_of_days_temp': resto,
                                   }
                        if(resto != 0):
                            holidays.create(cr, uid, values)

        else:
            raise osv.except_osv(_('Warning'),
                            _('La regularizacion de vacaciones en el cambio de año solo se puede realizar en el mes de Enero')) 

        return {}

cambio_anio_wizard()

Ya casi para acabar, deberemos crear las vistas correspondientes para dibujar los menús de la aplicación y colocarlos en los sitios que nos interesen; en nuestro caso “colgaremos” todo desde el menu de configuración de vacaciones, aprovechando el filtrado que trae por defecto el sistema que solo permite acceso a este menú al grupo “HR Manager”.

Para acabar este punto y todo el post, un pequeño apunte en cuanto a filtrados y permisos; el modulo por defecto como vemos a continuación trae los derechos de accesos configurados en el csv correspondiente en la carpeta security, en el que daremos acceso a las nuevas entidades creadas según grupos. Esta tarea en el desarrollo de cualquier modulo podemos encontrar varias veces que “no esta hecha”, ¿ que quiero decir con estas comillas? Que no es que no este hecha, es mas, considero que si no es un desarrollo muy concreto para un determinado proyecto debería de dejarse “a gusto del consumidor” que gestione permisos y accesos a sus necesidades ( una de las esencias del ERP).

"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_calendario_manager","hr.holidays.calendario manager","model_hr_holidays_calendario","base.group_hr_manager",1,1,1,1
"access_festivos_manager","hr.holidays.festivos manager","model_hr_holidays_festivos","base.group_hr_manager",1,1,1,1
"access_calendario_user","hr.holidays.calendario user","model_hr_holidays_calendario","base.group_user",1,0,0,0
"access_festivos_user","hr.holidays.festivos user","model_hr_holidays_festivos","base.group_user",1,0,0,0

Para quien quiera echarle un vistazo mas detenido o hacer uso del modulo que hemos comentado, está disponible para la comunidad en launchpad.

Cálculo de amortización de activos en OpenERP (I) (Spanish version)

23 Oct 2012

Un aspecto importante dentro de la contabilidad de una empresa son las amortizaciones. Es un proceso que se realiza al final del ejercicio contable, pero no por ello, menos importante. La solución OpenERP de la que hemos hablado en varias ocasiones en xnoccio, gestiona las amortizaciones mediante el Módulo de Activos (account_assets). Sin embargo, al realizar la implantación de este ERP en pymes españolas, nos hemos encontrado con que su funcionalidad resulta insuficiente. Esto no es obstáculo, ya que no resulta difícil adaptar los módulos actuales a necesidades específicas o incluso crear módulos nuevos. En el caso que nos ocupa, Viavansi ha ampliado las funcionalidades del módulo de activos de OpenERP 6.1.

En este artículo nos ocuparemos de explicar funcionalmente la mejora y en uno próximo detallaremos cuales han sido las modificaciones técnicas realizadas.

Los inmovilizados se van depreciando poco a poco, ya sea por su uso o por el paso del tiempo. Esta depreciación debe reflejarse contablemente, y su cálculo viene marcado por las tablas de amortización que publica la Agencia Tributaria. Los métodos que utiliza, son el coeficiente máximo en porcentaje y el periodo máximo en años.

Como decía, actualmente, los métodos que propone el módulo account_assets son insuficientes. Por eso, hemos diseñado uno nuevo método que cubra las siguientes necesidades:

  • Cálculo de la depreciación en porcentaje.
  • Cálculo de las amortizaciones al final del periodo indicado. Ej. si es mensual al final de cada mes, si es trimestral al final del trimestre, si es anual a 31/12.
  • Calculo parcial de la amortización. Tomará como referencia la fecha de compra para el inicio de la de amortización, y como fecha final el 31/12 si el periodo es anual.

Al igual que indicamos en el post anterior sobre órdenes de pago en OpenERP, tendremos instalada la versión 6.1 con la contabilidad española.

El funcionamiento del nuevo método es el siguiente:

1-. Definición de la categoría de activo

Todos los miembros de esa categoría tendrán definido los mismos parámetros. En nuestro ejemplo, será “Mobiliario”.

En Contabilidad → Configuración → Contabilidad financiera → Activos → Categoría de activos → Crear

Categoría de activo de mobiliario

Categoría de activos

2-. Creación del producto Mesa de oficina

Ese producto será acorde a la categoría de activo creada previamente.

En Ventas → Productos → Productos → Crear

Producto mesa de oficina

Cuando compras un inmovilizado, es importante definir la cuenta de ingresos y gastos que le corresponde según el Plan General Contable. En nuestro ejemplo, la cuenta 21600000000 Mobiliario.

3-. Creación del proveedor de inmovilizado

En Contabilidad → Proveedores → Proveedores → Crear

Proveedor de inmovilizado

Proveedor de inmovilizado

Para poder seleccionar la cuenta 52300000000 Proveedores de inmovilizado a corto plazo como Cuenta a pagar es necesario cambiar la configuración de la misma.

En Contabilidad → Configuración → Contabilidad financiera → Cuentas →En código ponemos 5230 y la editamos como se muestra a continuación:

Cuenta proveedor de inmovilizado a corto plazo

Cuenta proveedor de inmovilizado a corto plazo

Hemos cambiado el Tipo interno Regular por A pagar, el Tipo de cuenta Financieras por Terceros – A pagar y hemos clickeado en Permitir conciliación.

4-. Realización de la factura de compra

En Contabilidad→ Proveedores → Facturas de proveedor → Crear

Seleccionaremos el proveedor que hemos creado previamente, la fecha de factura y de vencimiento. Cuando añadamos el producto tendremos que asignar la categoría de activo “Mobiliario” como se muestra en la siguiente pantalla:

Producto con categoría de activo

Producto con Categoría de activo

Una vez añadido el producto en la factura, pulsaremos en Aprobar. Con esta acción conseguiremos que éste se convierta en un activo

Factura compra de inmovilizado

Factura compra de inmovilizado

5-. Confirmación del activo

En Contabilidad → Activos → Activos

Se ha creado el activo “Mesa de oficina”, lo abriremos para confirmarlo o para modificarlo si fuera necesario.

Activo mesa de oficina. General

Activo mesa de oficina. general

A continuación haremos una breve aclaración sobre los campos que se muestran en la pestaña General anterior.

  • El valor bruto coincide con el precio de coste que definimos en el producto.
  • El valor contable será el mismo que el anterior si el inmovilizado no tiene valor residual.
  • La fecha de compra se rellenará manualmente a tipo informativo, y se utilizará para activos que ya estén en la empresa.
  • La fecha de la última amortización tiene un doble sentido. Para activos nuevos, será la fecha de compra y para aquellos que ya existían en la empresa, será la fecha en la que se realizó la última amortización.
  • El campo Primera amortización, será la fecha en la cual se realizará la primera amortización.
  • El resto de campos de esta pestaña viene marcado por los valores definidos en la categoría de activos, pero pueden ser modificados si fuera necesario.

En la pestaña Tabla de amortización se muestra la evolución que irá sufriendo el inmovilizado.

activo mesa de oficina. tabla de amortización

Activo mesa de oficina. Tabla de amortización

Previamente, habíamos definido que íbamos a amortizar un 10% anual. Si observamos la líneas de depreciación, la primera y la última son diferentes. Ello es debido a que el importe a amortizar no es un año completo, si no el periodo que va desde la fecha de compra hasta el 31/12. Marcamos esa fecha porque en la mayoría de las empresas el año fiscal y el natural coincide. Por ello, debemos reflejar sólo la parte de gasto que corresponde a ese ejercicio.

Una vez confirmado el activo, podremos calcular los asientos de depreciación. Pulsaremos en Calcular asientos de la línea de depreciación si solo es un activo, o seguiremos el siguiente enlace si queremos calcular el de todos los inmovilizados de la empresa a la vez.

En Contabilidad → Procesamiento periódico → Asientos recurrentes → Calcular amortizaciones → Buscamos el periodo 12/2012 y pulsaremos en Calcular. El asiento que se crea es el siguiente:

Asiento de amortización

Asiento amortización

Para terminar todo el proceso pulsaremos en Enviar para que el asiento quede como Asentado. A partir de ahora, podremos cambiar el porcentaje de amortización y la longitud del periodo pulsando en Cambiar duración, en la pestaña General del activo.

Cambiar duración de activo

Cambiar duración de activo

De OrangeHRM a OpenERP con OpenETL

18 Oct 2012

Después de mis últimos post Migración de datos entre distintas instancias de OpenERP usando OpenETL y Carga de datos en OpenERP usando OpenETL y aprovechando que estamos realizando una migración en la empresa desde OrangeHRM a OpenERP que mejor forma de cerrar el círculo que presentar en forma de post el proceso de migración que hemos seguido.

El escenario:

Para migrar los datos hemos partido de los archivos .csv que se generan desde OrangeHRM. Como dichos archivos contienen información de la empresa no los voy a adjuntar con el post, simplemente me limitaré a describir los campos. También partimos de un OpenERP que ya tiene cargados datos como países, provincias, y empresa. Para realizar las llamadas al xmlrpc he usado al usuario admin, el cual ya pertenece a la empresa. De esta forma el campo company_id se ha ajustado automáticamente.

Los archivos .csv contienen los siguientes campos:

Empleados.csv:

  • empID: Identificación del empleado en Orange.
  • lastName: Apellidos del empleado.
  • firstName: Primer nombre del empleado.
  • middleName: Segundo nombre del empleado.
  • street1: Dirección del empleado.
  • street2: Campo de apoyo para street1.
  • city: Municipio.
  • state: Provincia.
  • zip: Código postal.
  • gender: Género.
  • birthDate: Fecha de nacimiento.
  • ssn: Número de la seguridad social.
  • workStation: Departamento al que pertenece el empleado.

Los campos middleName, street1, street2, city, state, zip, workStation,birthDate pueden estar vacíos en el archivo .csv, por lo que hay que controlar estos casos.


dptos.csv:

  • workStation: Nombre del departamento en el sistema.


cargos.csv:

  • empId: Identificación del empleado en Orange.
  • empStatus: Cargo del empleado en la empresa.

El modelo de datos relacionados afectado de OpenERP se presenta a continuación. Lo he simplificado bastante y sólo he puesto los campos de los objetos que se van a cargar con los valores de los .csv.

Si te alejas de la pantalla y te pones bizco, verás a un tio bailando. En realidad es el diagrama de clases simplificado de objetos OpenERP.

Si te alejas de la pantalla y te pones bizco, verás a un tio bailando. En realidad es el diagrama de clases simplificado de objetos OpenERP.

A tener en cuenta:

Observando el modelo de datos se puede ver que muchos objetos están relacionados entre sí. Esto implica que si queremos cargar los empleados, antes tenemos que tener cargados en el sistema las direcciones. Este mismo comportamiento nos sucede con los departamentos, puestos de trabajo, etc.

Para solucionar este inconveniente he usado subtareas de OpenETL. El archivo subjob_example.py contiene un ejemplo para el uso de subtareas con OpenETL. El funcionamiento es bastante sencillo. Simplemente en vez de ejecutar la tarea que queremos convertir en subtarea, crearemos un nuevo componente de tipo subtarea con ella. Después a la tarea padre le pasamos como parámetro dicha subtarea.

En el código:

job_ = openetl.job([csv_in1,datos_ajustados,openobject_out2])  # Para poder relacionar direcciones con personas, las direcciones deben estar cargadas
subjob = openetl.component.transform.subjob(job_)              # en el sistema. Las cargo previamente en una subtarea.

job1=openetl.job([subjob_cargos,subjob_dptos,subjob_paises,subjob,csv_in1,datos_ajustados,openobject_out1])

job1.run()

Las subtareas implicadas son son:

  • subjob_cargos: Carga las categorías de los empleados.
  • subjob_dptos: Carga los departamentos de la empresa.
  • subjo_paises: Realiza correspondencia de países de OpenERP con Orange.
  • subjob: Carga las direcciones de los empleados.

Y los diagramas de cada subtarea:

Diagramas de subjob_cargos y subjob_dptos.

Diagramas de subjob_cargos y subjob_dptos.

Diagramas de subjob_paises y subjob.

Diagramas de subjob_paises y subjob.

Para relacionar los objetos de las subtareas con la carga final, la de los empleados, he usado un pequeño truco. Vamos a fijarnos en la lista de categorías de empleados (subjob_cargos).

Al leer las categorías desde el csv inicial, las he pasado por una transformación que ejecuta una función (preprocess_cargos):

lista_cargos = {}
def preprocess_cargos(self, channels):
    for trans in channels['carga_cargos']:
        for d in trans:
            lista_cargos[d['empId']] = d['empStatus']
    return None

pres_cargos=openetl.component.transform.map({},preprocess_cargos)

Dicha función lo único que hace es cargar en un diccionario una relación empId-empStatus, es decir, relaciona id de empleado con su categoría.

Más adelante en el código, al realizar la carga de los empleados, consulto dicho diccionario:

def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:
            .
            .
            .
            # Ajuste de cargo
            d['cargo'] = lista_cargos[d['empId']]

    return {'resultado':cdict}

Y por último en el mapeado del objeto, antes de cargarlo en OpenERP:

openobject_out1 = openetl.component.output.openobject_out(
     ooconnector,
     'hr.employee',
     {
      .
      .
      .
      'job_id':'cargo',
      }
    )

En el diagrama también aparece una paso previo por el componente unique. Dicho componente quita los elementos duplicados antes de cargarlos en el sistema. Hay un ejemplo de uso de dicho componente en el fichero unique_csv_data.py.

Otra cosa interesante de esta migración es como se han mapeado los datos del csv a los objetos. El fichero join_example.py contiene un ejemplo que usa map_keys. Dicho ejemplo está muy bien y funciona siempre y cuando se usen componentes “openetl.component.input.data” definidos en el propio archivo de script. El problema es que cuando se lee un archivo de csv no se está usando una entrada “estática”, sino secuencial. De modo que el map_keys es ignorado. La solución en este caso ha sido pasar por parámetro un map_key vacío y realizar el mapeo de datos desde el propio código de la función preprocess.

En el caso de países:

pre_paises=openetl.component.transform.map({},preprocess_paises)
def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:

           .
           .
           .
            # Ajuste de paises
            if d['state'] == "Santo Domingo":
                d['state'] = lista_paises[62]  # Codigo de Republica Dominicana
            elif d['state'] == "Distrito Nacional":
                d['state'] = lista_paises[62]
            else:
                d['state'] = lista_paises[69] # Codigo de Espagna
            .
            .
            .    

    return {'resultado':cdict}

El resultado:

Como en mis anteriores post presento el código completo de la solución obtenida. Evidentemente este script de migración cubre nuestras necesidades concretas, pero es fácil adaptarlo si se necesitan migrar datos diferentes. También se podría haber realizado un script de migración atacando directamente a la base de datos de Orange, aunque por seguir con el ejemplo planteado en el primer post de OpenETL se han usado archivos .csv. En cualquier caso OpenETL también contiene conectores para consultas SQL. El archivo sql_in_example.py contiene un ejemplo con el que se podrían sustituir las llamadas a los csv con consultas sql.

import sys
sys.path.append('..')

import openetl

#===============================================================================
# Conectores
#===============================================================================
fileconnector_orange=openetl.connector.localfile('/home/carlos/Escritorio/Orange/Empleados.csv')
fileconnector_orange_dptos=openetl.connector.localfile('/home/carlos/Escritorio/Orange/dptos.csv') # Con tratamiento previo
fileconnector_orange_cargos=openetl.connector.localfile('/home/carlos/Escritorio/Orange/cargos.csv') # Con tratamiento previo
ooconnector = openetl.connector.openobject_connector('http://localhost:8069', 'master_viavansi', 'admin', 'admin', con_type='xmlrpc')

#===============================================================================
# Componentes
#===============================================================================
csv_in1= openetl.component.input.csv_in(fileconnector_orange,name='Datos de Orange')
csv_in_dptos= openetl.component.input.csv_in(fileconnector_orange_dptos,name='Departamentos')
csv_in_cargos= openetl.component.input.csv_in(fileconnector_orange_cargos,name='Cargos')

openobject_out1 = openetl.component.output.openobject_out(
     ooconnector,
     'hr.employee',
     {
      'name':'name_csv',
      'ssnid':'ssn',
      'gender':'gender',
      'birthday':'birthDate',
      'address_home_id':'name_csv', # Nombre de la relacion
      'department_id':'workStation',
      'job_id':'cargo',
      }
    )

openobject_out2 = openetl.component.output.openobject_out(
     ooconnector,
     'res.partner.address',
     {
      'name':'name_csv',
      'street':'street1',
      'street2':'street2',
      'zip':'zip',
      'city':'city',
      'country_id':'state',
      }
    )

openobject_out3 = openetl.component.output.openobject_out(
     ooconnector,
     'hr.department',
     {
      'name':'workStation',
      }
    )

# Soporte para carga de datos de cargo de empleado. El Diccionario se carga en subtarea previa
lista_cargos = {}
openobject_out4 = openetl.component.output.openobject_out(
     ooconnector,
     'hr.job',
     {
      'name':'empStatus',
      }
    )

def preprocess_cargos(self, channels):
    for trans in channels['carga_cargos']:
        for d in trans:
            lista_cargos[d['empId']] = d['empStatus']
    return None

pres_cargos=openetl.component.transform.map({},preprocess_cargos)

# Soporte para carga de datos de paises. El Diccionario se carga en subtarea previa
lista_paises = {}

openobject_in1 = openetl.component.input.openobject_in(
                 ooconnector,'res.country',
                 fields=['id','name'],
                 )

def preprocess_paises(self, channels):
    for trans in channels['carga_paises']:
        for d in trans:
            lista_paises[d['id']] = d['name']
    return None

pre_paises=openetl.component.transform.map({},preprocess_paises)

# Soporte transformaciones y componentes

def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:
            # name: no existia,lo creo yo con la suma de los campos 

            if d['middleName'] == "":  # En OpenERP, no se separan los campos, hay un unico campo name
                d["name_csv"] = d["firstName"] + str(" ")+ d["lastName"]
            else:
                d["name_csv"] = d["firstName"] + str(" ")+ d["middleName"] +str(" ")+ d["lastName"]

            if d['gender'] == "M":     # Adaptacion de nomencaltura de datos de Orange a OpenERP
                d['gender'] = 'male'
            else:
                d['gender'] ='female'

            # Ajuste de paises
            if d['state'] == "Santo Domingo":
                d['state'] = lista_paises[62]  # Codigo de Republica Dominicana
            elif d['state'] == "Distrito Nacional":
                d['state'] = lista_paises[62]
            else:
                d['state'] = lista_paises[69] # Codigo de Espagna

            # Ajuste de cargo
            d['cargo'] = lista_cargos[d['empId']]

    return {'resultado':cdict}            

datos_ajustados=openetl.component.transform.map({},preprocess)  # Como leo un flujo de datos, no hay key_map. key_maps es para diccionarios

#===============================================================================
# Transiciones, Definicion de trabajo y ejecucion. Operaciones de Carga
#===============================================================================

log_cargos=openetl.component.transform.logger(name='Log de cargos')
unique_job = openetl.component.transform.unique()
openetl.transition(csv_in_cargos,pres_cargos,channel_destination='carga_cargos')
openetl.transition(pres_cargos,log_cargos)
openetl.transition(csv_in_cargos,unique_job)
openetl.transition(unique_job,openobject_out4)
job_cargos=openetl.job([csv_in_cargos,unique_job,openobject_out4,log_cargos])
subjob_cargos = openetl.component.transform.subjob(job_cargos)  

unique = openetl.component.transform.unique()
log_dptos=openetl.component.transform.logger(name='Log departamentos')

openetl.transition(csv_in_dptos,unique)
openetl.transition(unique,log_dptos,channel_source='main')
openetl.transition(unique,openobject_out3)
job_dptos=openetl.job([log_dptos,openobject_out3])
subjob_dptos = openetl.component.transform.subjob(job_dptos)  

openetl.transition(openobject_in1,pre_paises, channel_destination='carga_paises')
job_paises = openetl.job([openobject_in1,pre_paises])
subjob_paises = openetl.component.transform.subjob(job_paises)  

openetl.transition(csv_in1,datos_ajustados, channel_destination='modificacion') # Leo datos aplicando preprocesamiento
openetl.transition(csv_in1,openobject_out2) # Direcciones
openetl.transition(csv_in1,openobject_out1) # Personas

job_ = openetl.job([csv_in1,datos_ajustados,openobject_out2])  # Para poder relacionar direcciones con personas, las direcciones deben estar cargadas
subjob = openetl.component.transform.subjob(job_)              # en el sistema. Las cargo previamente en una subtarea.

job1=openetl.job([subjob_cargos,subjob_dptos,subjob_paises,subjob,csv_in1,datos_ajustados,openobject_out1])
job1.run()

Con esto concluye la parte técnica del post. Creo que OpenETL es una tecnología muy interesante, que permite realizar trabajos de ETL de forma bastante cómoda e intuitiva. También os comento que he echado en falta algo más de documentación técnica sobre OpenETL, ya que he tenido que recurrir al código fuente de muchos componentes, transformaciones, etc. para averiguar que es lo que hacían.

A pesar de ello la línea de aprendizaje de esta tecnología es bastante sencilla una vez que sabes que hay que hacer, y se pueden lograr grandes cosas en poco tiempo.

Para finalizar os comentaré que mi impresión final sobre OpenETL es muy buena. No sólo porque se adapte perfectamente a operaciones ETL sobre OpenERP, sino porque tiene un amplio abanico de conectores (sql, facebook, xmlrpc,csv, gdoc, gcalendar, etc) que permiten usar OpenETL en muchos proyectos con distintas tecnologías.

Órdenes de pago OpenERP (Spanish version)

11 Oct 2012

Hoy en día, la relación existente entre una empresa y sus bancos es muy estrecha. Cada vez son menores los pagos en efectivo, en favor de otros tipos de pago como transferencias, recibos domiciliados, cheques, pagarés, nóminas, efectos, etc.

En este artículo describiremos la manera de configurar OpenERP para poder efectuar las órdenes de pago. Para ello, será necesario tener instalada la versión 6.1 con la contabilidad española.

OpenERP utiliza el módulo de la localización española l10n_es_payment_order para la exportación de ficheros bancarias según las normas CSB 19 (recibos domiciliados), CSB 32 (descuentos comerciales), CSB 34.1 (emisión de transferencias, cheques bancarios, nóminas, pagarés y pagos certificados internacionales) y CSB 58 (anticipos y gestión de cobro de créditos) para poder ser enviados a la entidad bancaria.

Como hemos comentado en anteriores artículos, OpenERP tiene una estructura modular, de ahí que para el correcto funcionamiento de algunas acciones sea necesario configurar otras. Antes de generar las órdenes de pago/cobro deberemos realizar los siguientes pasos:

En Administración → Compañías → Compañías → Pestaña Información general

  • Rellenar la dirección y el CIF/NIF de la compañía. El CIF estará compuesto por el código del país (ES), la letra y el número correspondiente. Por ejemplo, ESB12345678.

En Administración → Configuración → Asistentes de configuración → Asistentes de configuración → Configurar sus cuentas bancarias

  • Lanzar el asistente de configuración “Configurar sus cuentas bancarias”. Aquí crearemos las cuentas que tiene la compañía y que utilizará para realizar sus movimientos bancarios.

En Contabilidad → Proveedores → Proveedores → Crear

  • Crear un proveedor. En la pestaña General rellenaremos la dirección. En Contabilidad definiremos el tipo de pago Transferencia CSB, el CIF/NIF de la empresa y crearemos la cuenta bancaria del proveedor.

Una vez realizadas estas configuraciones, vamos a explicar los pasos que hay que seguir para crear una remesa de pago de transferencias bancarias según la norma CSB 34.1.

Entraremos en Contabilidad → Configuración → Varios → Modo de pago → Crear

  • Daremos un nombre al modo de pago que sea suficientemente representativo, ya que posteriormente lo seleccionaremos en la orden de pago.
  • Seleccionaremos el tipo de pago y remesa.
  • En Datos del presentador, seleccionaremos nuestra empresa, la cuenta bancaria que utilizaremos para la remesa y pondremos el nombre de la compañía que se reflejará en el archivo.
  • En Opciones CSB 34, detallaremos si es una transferencia, un cheque, un pagaré o un pago certificado. Para nuestro ejemplo será una transferencia bancaria.
  • En Datos adicionales, podemos definir si lo gastos corren por cuenta del ordenante o del beneficiario, así como el concepto de la transferencia.
OpenERP, modo de pago

OpenERP, modo de pago

Una vez definido el modo de pago crearemos la factura del proveedor. Antes de aprobarla deberemos comprobar que el tipo de pago sea Transferencia CSB, que tenga estipulado una fecha de vencimiento y que la cuenta bancaria del proveedor esté seleccionada en la pestaña Otra información.

A continuación nos iremos a Contabilidad → Pago → Órdenes de pago → Crear

  • Seleccionaremos el modo de pago que hemos creado previamente.
  • Elegiremos crear los asientos contables por pago directo o por extracto bancario.
  • Pulsaremos en Seleccionar facturas a pagar/cobrar. Nos mostrará una pantalla dónde deberemos introducir una fecha posterior a la del vencimiento y un importe superior al de la factura. Se mostrarán las facturas que cumplan todas las condiciones.
  • Seleccionaremos aquellas que queramos incluir en la remesa de pago y pulsaremos en Añadir a la orden de pago.
  • A continuación, pulsaremos en la acción Export file que aparece en el menú de la derecha. Nos generará un archivo que guardaremos y que posteriormente utilizaremos para importarlo en el programa de remesas del banco.
OpenERP, órdenes de pago

OpenERP, órdenes de pago

Por último, para poder importar el archivo generado por OpenERP será necesario estar dado de alta en el banco en la norma 34.1, para nuestro ejemplo, en las Transferencias 34.1. El resultado obtenido es el siguiente:

Simulador de remesas de banco

Simulador de remesas de banco

Espero que lo hayáis encontrado útil. Para más información y/o ayuda personalizada sobre OpenERP podéis contactarnos sin ningún compromiso.

Migración de datos entre diferentes instancias de OpenERP usando OpenETL

25 Sep 2012

En mi anterior post realicé una introducción a OpenETL. También desarrollé un ejemplo de carga de datos desde una archivo .csv a OpenERP.

En este post voy a profundizar un poco más, realizando una migración de datos de OpenERP a OpenERP en los cuales hay tablas relacionadas.

El escenario

Nuestro entorno de migración constará de las siguientes características:

  • BD_inicial contiene los datos que queremos migrar. Los datos serán los clientes y proveedores con sus direcciones.
  • No todos los clientes o proveedores tienen una dirección asociada, por lo que hay que controlar la excepción.
  • Para simplificar el ejemplo voy a migrar sólo el contenido de los campos name, title y partner_id, siendo title el campo que contiene la relación con la tabla res_partner_title y partner_id el campo relacionado con la tabla res_partner.
Diagrama de relación de clases simplificado de res_partner_address con res_partner y res_partner_titulo

Relación simplificada de relaciones de objetos res_partner_address, res_partner_title y res_partner.

A tener en cuenta

Cuando migras un contenido desde OpenERP a un archivo .csv el sistema suele funcionar sin complicaciones. Sin embargo cuando se realiza la migración de OpenERP a OpenERP es fácil obtener excepciones tal como

File "/usr/lib/python2.6/xmlrpclib.py", line 838, in close
raise Fault(**self._stack[0])
xmlrpclib.Fault: <Fault 'bool' object has no attribute 'lower': 'Traceback (most recent call last):n  File … … … …  /openerp/osv/orm.py", line 1380, in process_linessn    res = line[i].lower() not in ('0', 'false', 'off')nAttributeError: 'bool' object has no attribute 'lower'n'>

Esto ocurre porque OpenERP al leer un campo sin valor le asigna por defecto un booleano inicializado a False.

En el ejemplo data_map.py después de leer los valores de ejemplo desde el .csv el autor realiza una transformación de los mismos antes de mostrarlos en el log. Basándose en ese ejemplo, es fácil inicializar los campos con valores adecuados:

def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:
            if d["name"] == False:
                d["name"] = ""
            cdict[d['id']] = d
    return {'resultado':cdict}

Otra cosa que también puede producir muchos quebraderos de cabeza es que en los campos relacionados no se va a poner el id de la tupla relacionada, sino el valor de la misma. Nuestra función quedaría así:

def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:
            if d["title"] == False: # Es una relacion, ej: 'title': [5, 'Sir'] , con res_partner_title
                d["title"] = '' # Si quiero dejarlo sin valor, le dejo las comillas
            else:
                d["title"] = d["title"][1]  # No se coge el 0, que es el id, sino el valor. El id se ajusta automatico :/
            if d["name"] == False:
                d["name"] = ""

            if d["partner_id"] == False:
                d["partner_id"] = ""
            else:
                d["partner_id"] = d["partner_id"][1]

            cdict[d['id']] = d
    return {'resultado':cdict}

El resultado

El código completo, con conectores, componentes, transiciones, etc. se muestra a continuación. Nótese que la función de procesamiento es llamada desde una transición openetl.component.transform.map(map_keys,preprocess), en la que se pasa también un parámetro map. Hay más ejemplos parecidos en data_map.py y m2m_into_oo.py.

#!/usr/bin/python

import sys
sys.path.append('..')

import openetl
from openetl import transformer

# Conectores
ooconnector_in = openetl.connector.openobject_connector('http://localhost:8069', 'BD_inicial', 'admin', 'admin', con_type='xmlrpc')
ooconnector_out = openetl.connector.openobject_connector('http://localhost:8069', 'BD_final', 'admin', 'admin', con_type='xmlrpc')

# Componentes
openobject_in1 = openetl.component.input.openobject_in(
                 ooconnector_in,'res.partner.address',
                 fields=['id','title','name','partner_id'],
                 )

openobject_in2 = openetl.component.input.openobject_in(
                 ooconnector_in,'res.partner',
                 fields=['id','name'],
                 )

openobject_out1 = openetl.component.output.openobject_out(
     ooconnector_out,
     'res.partner.address',
     {'name':'name','title':'title','partner_id':'partner_id'}
    )

openobject_out2 = openetl.component.output.openobject_out(
     ooconnector_out,
     'res.partner',
     {'name':'name'}
    )

log=openetl.component.transform.logger(name='Recien leido:Read Partner File ')

# Soporte transformaciones

map_keys = {'main': {
    'name': "resultado[main['id']]['name']",
    'title': "resultado[main['id']]['title']",
    'partner_id': "resultado[main['id']]['partner_id']",
}}

def preprocess(self, channels):
    cdict = {}
    for trans in channels['modificacion']:
        for d in trans:
            if d["title"] == False: # Es una relacion, ej: 'title': [5, 'Sir'] , con res_partner_title
                d["title"] = '' # Si quiero dejarlo sin valor, le dejo las comillas
            else:
                d["title"] = d["title"][1]  # No se coge el 0, que es el id, sino el valor. El id se ajusta automatico :/
            if d["name"] == False:
                d["name"] = ""

            if d["partner_id"] == False:
                d["partner_id"] = ""
            else:
                d["partner_id"] = d["partner_id"][1]

            cdict[d['id']] = d
    return {'resultado':cdict}               

map=openetl.component.transform.map(map_keys,preprocess)

# Transiciones
tran1=openetl.transition(openobject_in1,map, channel_destination='modificacion')
tran3=openetl.transition(openobject_in1,log)

tran_res_partner01=openetl.transition(openobject_in2, openobject_out2)

tran4=openetl.transition(openobject_in1, map)
tran4=openetl.transition(map, openobject_out1)

# Definicion de trabajo y ejecucion
job1=openetl.job([openobject_in1,map,openobject_out1,openobject_in2,openobject_out2])
job1.run()

Este código funciona y realiza la migración de datos sin ningún problema siempre que en las tablas relacionadas no haya ningún dato con igual campo “valor relacionado” repetido. ¿Y qué pasa si el “valor relacionado” sí está repetido? Lo que ocurre en este caso es que el sistema creará la relación con la tupla con id más pequeño. Para corregir esta situación bastaría con añadir alguna condición más a la función preprocess, ayudarnos de alguna otra función en python auxiliar, etc. Si se diera ese caso los ejemplos sql_in_example.py, csv_diff_example.py, join_example.py, podrían servir como base en función del tratamiento que quisiéramos hacer.

ERP Pymes (V): Gestión de clientes (CRM) en OpenERP

13 Sep 2012

En estos tiempos de zozobra económica, el reto de encontrar un modelo equilibrado que permita la reducción de costes y el aumento de la eficiencia, resulta algo así como encontrar el Santo Grial, que todo el mundo querría poseer sin conseguirlo.

Indy ofreciendo el Santo Grial a su papi moribundo

No hay que esperar a estar moribundo para tomar ciertas medidas empresariales

Si el lector espera encontrar aquí la panacea empresarial definitiva puede dejar de leer en este punto. Lo que sí podemos ofrecer, como venimos haciendo desde que empezamos esta serie de artículos, es una herramienta que, con perseverancia, sí puede con una inversión modesta al alcance de las pymes, apoyar de manera decisiva su negocio: OpenERP. En esta ocasión hablaremos de la gestión de la relación con los clientes o CRM (Customer relationship management).

Los CRMs gestionan todas las actividades del negocio relacionadas directamente con el cliente: desde que se detecta una oportunidad hasta que se realiza un presupuesto. La mayoría de los CRMs incluyen un módulo de ventas, que completa el flujo general.

Los ERPs pueden no incluir un CRM, pero resultan imprescindibles para la gestión de cualquier negocio, por lo que aunque no lo incluya, necesitaremos instalar los dos sistemas e intentar que resulten lo más compatibles e integrados que sea posible. Si no, comenzarán los problemas de multiples entradas de datos, desactualización de los mismos, imposibilidad de informes fiables…

Afortunadamente, OpenERP cuenta con un potente CRM totalmente integrado en el sistema. Esto significa poder disponer con facilidad de cualquier información relacionada con el cliente: el estado de sus facturas, de las incidencias (postventa), de ofertas, productos, segmentación, campañas de marketing… Repito que se integra con TODO el sistema. Esto tiene efectos colaterales insospechados, que multiplican exponencialmente la potencia de gestión y análisis del sistema al poder cruzar de manera inmediata y trasparente los datos de los clientes con producción, proyectos, empleados, proveedores (SRM)

Toda esta información cruzada puede analizarse por diversos procedimientos (visualizar mediante gráficos, etc) que permiten inferir el grado de eficacia de nuestra actividad comercial, planificar mejoras e incluso programar en el sistema nuevas reglas en los flujos comerciales.

Haciendo un recorrido rápido, las funcionalidades del CRM/gestión de ventas de OpenERP son:

  • Iniciativas: suele ser el primer paso del ciclo de ventas. Permite gestionar y hacer el seguimiento de de todos los contactos iniciales, y guardar un historial asociado a cada una de ellas. OpenERP puede convertir de manera automática un formulario de petición de información en el portal web o un simple correo en una iniciativa a seguir, importar una base de datos con posibles clientes o hacer el seguimiento de una tarjeta de visita.
  • Oportunidades: permite el seguimiento de la cartera de sus mejores ofertas: el historial de la comunicación (reuniones, llamadas), los ingresos esperados, la etapa en la que la oportunidad se encuentra, los cierres previstos, etc. Conectado con una pasarela de correo electrónico le permite mantener la historia de los correos intercambiados con el cliente. Cada equipo comercial puede programar sus reuniones y llamadas telefónicas, convertir las oportunidades en las citas, gestionar los documentos relacionados y en definitiva seguir todas las actividades relacionadas con este cliente.
  • Pedidos de venta: los pedidos de ventas le ayudan a gestionar presupuestos y pedidos de sus clientes. Puede crear un presupuesto. Una vez esté confirmado, el presupuesto se convertirá en un pedido de venta. OpenERP puede gestionar varios tipos de productos de forma que un pedido de venta puede generar tareas, órdenes de entrega, órdenes de fabricación, compras, etc. Según la configuración del pedido de venta, se generará una factura en borrador de manera que sólo hay que confirmarla cuando se quiera facturar a su cliente. Es compatible con varios métodos de facturación de acuerdo a su configuración.
  • Libreta de direcciones: administra su lista de todos sus contactos, clientes y también proveedores. Le permite registrar información sobre sus clientes (direcciones, contactos, lista de precios, etc.) Reúne en la ficha Contabilidad toda la información económica y financiera relacionada con el cliente (datos bancarios, cuentas, tipo de pago, estado de pagos, crédito, etc.) En la ficha Historial, puede seguir todos los movimientos y transacciones relacionadas con un cliente un proveedor, como pedidos, reclamaciones, llamadas, emails, reuniones, etc.
  • Reuniones: El calendario de reuniones se comparte entre los equipos de ventas y está totalmente integrado con las otras aplicaciones como las vacaciones o ausencias de los empleados. También puede sincronizar las reuniones con su teléfono móvil a través de la interfaz de CalDAV.
  • Llamadas telefónicas: registro y gestión de llamadas entrantes y salientes. Se puede fácilmente modificar o agregar un nuevo registro de llamada, convertir un cliente potencial en una oportunidad o planificar una reunión o planificar una nueva llamada. Los botones de acción le permiten hacer evolucionar su estado de llamada para un mejor seguimiento de sus llamadas programadas. Durante las llamadas, convertir una iniciativa en oportunidad, planificar una reunión o cancelarla.
  • Facturación: en las líneas de factura se abre una vista de búsqueda de líneas de pedidos de venta y su estado. Puede utilizar este menú para crear facturas de líneas de la orden de venta que ya están entregadas pero no facturadas todavía.
  • Servicio de Post Venta (reclamaciones): pueden ser clasificadas por clientes, tipos, estado y nivel de prioridad. Una reclamación también puede desembocar en una acción preventiva o reparación. También puede estar relacionada con una referencia como un pedido de cliente o producto concreto; enviar correos electrónicos con archivos adjuntos directamente desde OpenERP. Se completa con un historial minucioso del tratamiento reclamación (correos electrónicos enviados, el tipo de intervenciones, etc.)
  • Servicio de Post Venta (helpdesk): al igual que en las reclamaciones, permite el seguimiento de sus intervenciones. Seleccione un cliente, añada notas y categorize las intervenciones con terceros en caso necesario. Puede asignar un nivel de prioridad. Utilice el sistema de incidencias de OpenERP para gestionar sus actividades de soporte. Helpdesk puede conectarse con la pasarela de correo: los correos nuevos podrán crear incidencias, cada una de las cuales se rellenará automáticamente con el historial de comunicación con el cliente.
  • Productos: categoría de productos abre una vista de estructura de árbol de los productos de su categoría. En OpenERP, un producto es algo que se puede comprar y vender. Puede ser una materia prima, un producto inventariable, un consumible o un servicio. El formulario del producto contiene información detallada sobre los productos, como la logística, adquisición, precio de venta, la categoría de productos, proveedores, etc
  • Documentos: repositorio de todos los documentos adjuntos (mails, documentos asociados a un contrato, proyecto, etc)
  • FAQ: página wiki para compartir preguntas más frecuentes.
  • Informes:
    • Análisis de Iniciativas
    • Análisis de Oportunidades
    • Análisis de Ventas
    • Análisis de llamadas
    • Análisis de reclamaciones
    • Análisis de incidencias
    • Análisis de envíos

OpenERP data load using OpenETL

08 Ago 2012

Introduction:

OpenETL is a library in python for OpenERP S.A Data Migration. This tool allows us to perform all typical ETL operations (extract, transform and load) with the added value of being very well integrated with OpenERP.

The website of the library is https://launchpad.net/openetl

There is also a graphical interface packaged as a module (etl_interface) for OpenERP. This module is an extra add-ons. Although since the interface is easier to handle, this post will focus on the use of python library.

Installing:

To download the library you must install bazaar.

The specific command:

bzr branch lp:openetl

After downloading the openerp branch, copy the folder openetl/lib/openetl to your system libraries folder. In my case /usr/lib/python2.6/.

Although in my eclipse with PyDev I already have set the route, Eclipse Indigo seems not to “catch” the first library. To refresh the accessible libraries in eclipse, go to PyDevInterpreter – Python, click on Restore Defaults (if we already had configured the system) and then click on Apply.

Performance:

The library is divided into work, components, connectors, and transitions.

  • Jobs: Processes that can run, pause and stop.
  • Components: Inputs, outputs and transformation components. They allow us to get data and store them into external systems.The transformation components will be those that fit the data before the final charge.
  • Connectors: The connectors define connections with the external systems. They are used by the components. The current version of the library has connectors to treat local files, Openobjects, different databases (postgres, mysql, oracle), URLs (http, ftp, https, gopher), xmlrpc web services , SugarCRM, other google services (GDocs , GCalendar, gblog) and facebook.
  • Transitions:  Transitions are the flow the data between component is passing through.

The programmer must define as many input conectors as outputs are needed, at least one component for each connector, and a minimal transition to pass data from one component to another. Connectors are linked with the components, so for writting data into the component, they are written into the external system.

Instance:

We have a OrangeHRM system installed in the company with the HR tabs we want to migrate to OpenERP. The HR staff has exported the data to a .Csv format and asks us to perform the OpenERP data loading.

What we have to do is create two connectors, one will connect locally against the .csv file and the other type of XMLRPC will connect to OpenERP. Then define the components that are going to store the information, create the transitions (one for writing data to the final component and one for sorting the data) which are going to run and finally we launch the task. In the OpenObject component, we’ll define the csv fields mapping to the erp object fields . Transitions are executed sequentially as you have defined them in the .py .

Diagrama de carga de datos con OpenETL con 2 transiciones y 2 conectores

Diagrama de carga de datos con OpenETL con 2 transiciones y 2 conectores

The associated source code:

import sys
sys.path.append('..')

import openetl

# Conectors
fileconnector_orange=openetl.connector.localfile('/home/carlos/Escritorio/csvRaquel/DatosRRHHOrangeHRM.csv')

ooconnector = openetl.connector.openobject_connector('http://localhost:8069', 'testProject', 'admin', 'admin', con_type='xmlrpc')

# Components
csv_in1= openetl.component.input.csv_in(fileconnector_orange,name='Datos de Orange')

oo_out_employee = openetl.component.output.openobject_out(
     ooconnector,
     'hr.employee',
     {'name':'firstName'}
    )

sort1=openetl.component.transform.sort('firstName')

# Transitions

tran1=openetl.transition(csv_in1,sort1)

tran2=openetl.transition(sort1, oo_out_employee)

# Work and run definition
job1=openetl.job([csv_in1,sort1,oo_out_employee])
job1.run()

To make the example easier, I have simplified the number of fields to load from the .csv, fields with related tables, etc.. But the load can be as much complicated as necessary so that all data is migrated correctly.

In the folder openetlexamples there are examples of all the things we will need in the migrating data process , going from examples with multiple inputs and outputs (csv_in_out.py) to data load with related tables (m2m_into_oo.py). This example is quite interesting, as it reads users and csv files groups and load them directly into OpenERP. Let’s note also the example of migrating data from SugarCRM, facebook, GCalendar, etc..

Processes that can run, pause and stop.

ERP y Pymes (IV): Contabilidad Analítica

25 Jul 2012

Durante más de un decenio, tuve una pequeña empresa en la que teníamos una plantilla que fluctuaba entre mi socia y yo mismo y hasta varias docenas de trabajadores en los picos de trabajo. Como muchos pequeños empresarios sin formación económica alguna, deposité mi confianza en una gestoría, que se ocupaba de casi todas las cuestiones contables, fiscales y laborales. A mi socia y a mi tan sólo nos ocupaba que la empresa prestase sus servicios lo mejor posible y que la cuenta del banco tuviera suficiente cada mes para todos los gastos y para podernos ir de vacaciones de vez en cuando. Esta inocente manera de actuar funcionó durante dos o tres proyectos. Fuimos observando que nuestro negocio, con la administración pública como principal cliente, estaba ligado a los ciclos de gestión de esta, con un calendario de aprobación de presupuestos, periodos de licitación, etc. Cuando teníamos la fortuna de conseguir la adjudicación de varios proyectos a la vez, nos encontrábamos con que en el inicio de dichos proyectos debíamos realizar fuertes desembolsos iniciales de arranque. Era raro que tuviéramos algún plan de facturación, ni certificaciones parciales, por lo que el pago de los proyectos se efectuaba tras ejecutar satisfactoriamente el mismo, y pasado cierto tiempo de trámites. O sea, que nuestra cuenta en el banco durante casi todo el proyecto bajaba y bajaba hasta que llegaba el momento del pago de las facturas en el que subía como la espuma de la cerveza. Los dos primeros cuatrimestres del año eran siempre una cuesta arriba interminable y el último nos confundían con Scrooge McDuck (el tío Gilito).

Dibujo del Tío Gilito en su baño diario de monedas

Así que ya no bastaba con ir viendo como evolucionaba la cuenta del banco. Porque nos llevamos algún que otro susto importante por falta de cash. Teníamos que prever que iba a suceder el mes que viene, y el otro, y el otro. Y comenzamos a realizar unas primitivas previsiones de tesorería en excel. Fuimos identificando costes directos e indirectos, repercutiendo los primeros de forma adecuada en las ofertas, estudiando al rentabilidad real de cada proyecto, previendo fuentes de financiación… el abc económico de una pyme, vaya.

Retrato de Julio Verne (fotografo: Félix Nadar)

Todas estas cuestiones las abordábamos de modo muy rudimentario: apenas unas excel y algo de sentido común eran nuestras únicas herramientas para la toma de decisiones. Con el tiempo, nos pasamos a las TICs para la gestión de presupuestos y de proyectos, pero nunca se nos pasó por la cabeza tener una herramienta contable de la que poder extraer información actualizada de costes, previsiones financieras, etc. Desconocíamos lo que era un ERP, y si lo hubiéramos sabido, nunca podría haber estado a nuestro alcance. Y si me hubieran dicho que algún día existiría una herramienta de gestión integral de mi negocio, que incluía la gestión de proyectos, la contabilidad analítica y mucho más, sin coste de licencia, me hubiera reído de quien me lo dijese. Lo del “Anillo Único“, que gobernase todos los procesos, era simplemente pura fantasía del genial Tolkien. Pero como decía Julio Verne: “Si un hombre se imagina una cosa, otro la tornará en realidad“. Y hoy en día disponemos de los ERP opensource, al alcance de cualquier pyme. Y tenemos entre ellos OpenERP y su módulo de contabilidad analítica.

Ya hablamos en el artículo anterior de la capacidad de los módulos de contabilidad financiera de OpenERP. Pero la contabilidad financiera exige legalmente una estructura de cuentas de la que resulta difícil extraer información para tomar decisiones. Así que si usamos únicamente estos módulos, podremos estar tranquilos a la hora de cumplir con nuestras obligaciones tributarias, pero nos será extremadamente complicado saber si estamos en condiciones de bajar un poco los precios de la próxima oferta para ser más competitivos, o si podemos permitirnos comprar esa fantástica máquina que resolverá todos nuestros problemas de producción.

La contabilidad analítica o contabilidad de costes no tiene las restricciones legales de la contabilidad financiera, pues es de uso interno de la empresa, por tanto, se puede realizar cualquier Plan Analítico, con el grado de complejidad que se quiera. OpenERP permite esa libertad a la hora de elaborar la estructura analítica. Evidentemente, el grado de esfuerzo requerido en la parametrización del módulo será equivalente a la profundidad de análisis requerida. Pero en el éxito de esta tarea tiene más que ver la pericia de análisis del usuario que el conocimiento de la herramienta, que resulta relativamente fácil.

La estructura del plan analitico es tipo árbol y no está limitada, pudiéndose realizar distintos planes simultáneamente, dependiendo de distintos enfoques. Se pueden hacer análisis de facturas, de asientos, de tesorería, de activos, etc. Se puede asociar un gasto o un ingreso de la contabilidad financiera a una o varias cuenta analíticas, a través de las distribuciones analíticas.

Pero la aplicación de cuentas analíticas no se ciñe a los procesos de contabilidad. También se pueden aplicar para analizar ventas, compras o proyectos, donde resulta imprescindible para poder gestionar economícamente los mismos (con campos tan interesantes como el de “límite máximo de presupuesto”). Se puede detectar con facilidad rentabilidad, desviaciones, ect. Y poder tener esos datos que con tanto trabajo extraía con mis modestos excel, con mucha más rapidez y garantía.

Volviendo a una de las características fundamentales de los ERPs, el dato único, la enorme potencia analítica de OpenERP consiste precisamente en que los datos que analiza pueden abarcar de manera instantánea, todos los procesos del negocio. No es necesario cruzar datos, añadir, modificar o actualizar información. Toda la información económica requerida está en tiempo real, para poder tomar decisiones de manera ágil y confiada. El “inconveniente” es que luego no podremos echarle al culpa al sistema de elegir el camino equivocado 😉

ERP and SMEs (III): financial accounting (Spanish version)

16 Jul 2012

Aunque en el devenir de una pyme lo primero que se suele necesitar es la gestión de proveedores (SRM) y, sobre todo, clientes (CRM), nos la vamos a saltar de momento para ir a lo que a mí me parece el corazón de OpenErp: el control de la pasta. En los tiempos que corren, el minucioso control económico y financiero se ha convertido en una actividad crítica, desde los gobiernos hasta el más pequeño de los negocios. Así que le pediremos al sistema lo que Rod Tidwell a Jerry Maguire:

Casi cualquier actividad de OpenErp tiene repercusión contable. Indirectamente, todas. Una venta que se formaliza con su factura, el pago de las nóminas, una reposición de stock, los gastos del comercial, una compensación a un cliente, la limpieza de la oficina, la liquidación tributaria… Una de las enormes ventajas de OpenERP es que, se produzca donde se produzca la actividad, compras, ventas, marketing, producción… estas áreas NO se tiene que poner en contacto con contabilidad para informar de la novedad contable, porque simultáneamente a cualquiera de estas tareas YA se está produciendo su repercusión contable. No se produce comunicación entre distintas bases de datos y sistemas: sólo hay un sistema y una única bases de datos. El dato se introduce una única vez y repercute inmediatamente en cuantas áreas se haya configurado según los flujos de negocio. No hay tiempos de espera de comunicación entre áreas, ni pérdida de datos, ni introducción repetida de los mismos. Y mejora la calidad de vida de los contables 🙂

Al ser un sistema general, adaptable a cualquier tipo de negocio, la configuración inicial es fundamental y requiere un esfuerzo importante en el inicio, esfuerzo que se ve enseguida amortizado por la fluidez y la simplificación de tareas contables. Como ya adelantábamos en el artículo anterior, lo primero será elegir la localización contable necesaria, es decir, la configuración contable adaptada a la legislación del país donde la empresa realice su actividad. Es importante observar aquí que los módulos certificados de OpenERP contemplan una configuración muy básica de algunas localizaciones, y que para usar como en nuestro caso, la contabilidad según la normativa de España, deberemos instalar y configurar adecuadamente un conjunto de módulos no certificados de la localización española. Afortunadamente, la comunidad de desarrollo de dicha localización es uno de los más activos de OpenERP, por lo que las posibilidades funcionales de estos módulos son muy amplias.

Como decía, una vez instalados los módulos necesarios, es esencial un trabajo de configuración inicial importante, que en gran medida irá personalizado en función de las características de cada empresa: régimen fiscal, plan contable, etc. Lo ideal es haber realizado previamente un estudio de los procesos de la empresa, en particular de los procesos económico-financieros, ya que en la medida en que esta primera configuración se realice con precisión se obtendrá un sistema más o menos eficiente y eficaz. Si hubiese conflicto entre los procesos de la empresa y los de OpenERP, lo recomendable es optar por estos últimos modificando donde sea necesario los propios, ya que los modelos de OpenERP están basados en las mejores soluciones contrastadas. No vamos a ocultar que en este cambio, sobre todo en empresa de larga trayectoria, con “vicios procedimentales” fuertemente arraigados, se pueden encontrar serias actitudes de rechazo al cambio, que deben ser previstas y gestionadas con inteligencia si queremos llevar el proyecto a buen puerto. Esta dificultad, que se puede encontrar en cualquier departamento de la empresa, se encontrará especialmente en el área de contabilidad, habitualmente la más reacia a cualquier cambio.

Sin embargo, el esfuerzo bien vale la pena. En cuanto pasa el periodo de adaptación, los usuarios, tanto los generadores de actividad contable, como los gestores de la misma, ven una notable mejoría en esta actividad, ya que el registro contable se hace de forma rápida y sencilla.

Como muestra para contables reacios, en el área de contabilidad financiera de OpenERP, concretamente en la localización española podemos, entre muchas otras, encontrar las siguientes funcionalidades:

  • Importación de extractos bancarios. Consigues tener registrado todos los movimientos de tus cuentas bancarias y con ello poder conciliar los movimientos de las facturas que tienes pendiente de cobro o pago.
  • Registro de caja. Permite gestionar los movimientos diarios de dinero en efectivo y que esto quede reflejado al final del día en la cuenta de Caja.
  • Realización de asientos contables de forma manual.
  • Gestión de remesas de cobro y de pago según las normas CSB 19 (recibos domiciliados), CBS 32 (descuento comercial), CSB 58 (anticipos de créditos) y CSB 34 (emisión de transferencias, nóminas, cheques, pagarés y pagos certificados) para poder ser enviados a la entidad bancaria.
  • Presupuestos basados en las cuentas analíticas.
  • Conciliación de asientos manual y automáticamente. En la primera opción te permite seleccionar las cuentas que queremos conciliar y en la segunda el programa busca por importes coincidentes y los concilia.
  • Realización de asientos recurrentes. Permite realizar asientos iguales que se sucedan todos los meses, con los mismos importes y las mismas cuentas. Por ejemplo, el pago de un préstamo.
  • Re-numerar asientos contables. Permite ordenar los asientos por fechas para su posterior presentación en el Registro Mercantil.
  • Cerrar periodo. Una vez cuadrado un período es posible cerrarlo para que no se hagan otros movimientos que puedan alterar los datos contables.
  • Cerrar un ejercicio fiscal. Con esta función se comprueban si algún asiento no está asentado y una vez verificado te crea el asiento de Pérdidas y Ganancias, Cierre del ejercicio y Apertura del ejercicio siguiente.
  • Amortizaciones. Permite calcular la depreciación que va sufriendo un inmovilizado a lo largo de su vida útil.
  • Genera los informes más importantes como son el Balance de situación, la cuenta de Pérdidas y Ganancias y el informe de impuestos.
  • Genera los ficheros necesarios para las Declaraciones con la Agencia Tributaria como son el modelo 340, 347 y 349.

En resumen, el control económico sobre toda la actividad de la empresa es ágil, exacto, exhaustivo e inmediato. Como consecuencia inmediata, la información del estado económico y financiero para la oportuna toma de decisiones no tiene que esperar a la preparación de informes, muchas veces incompletos u obsoletos. Esto disminuye la incertidumbre y mejora por tanto la confianza en los momentos en los que estas cuestiones cobran vital importancia.

De la información del estado económico y financiero necesaria para la correcta toma de decisiones hablaremos en el próximo artículo, dedicado a la contabilidad analítica.