Importando pagos en Odoo por medio de archivos de Excel

Gustavo Orrillo
- 13/05/2022 - 4 min. de lectura

En muchas empresas, sobre todo las de servicios, se deben importar en forma diaria archivos de Pago. Porque la realidad así lo dicta. Archivos con los pagos de MercadoPago, de PagoFacil, RapiPago, la lista es larga... Lo cierto es que dicha situación es un caso de uso bastante frecuente. Y por lo general hay tres herramientas para hacerlo: la herramienta de importación de archivos de Odoo (que puede hacerlo, pero tiene sus límites, como por ejemplo distinguir líneas de header de líneas de detalle), módulos custom de Odoo, o scripts de xmlrpc. En este post vamos a hablar de esta última posibilidad.

Basicamente cada pago en Odoo es actualizado en el modelo account.payment. Entonces lo que tenemos que hacer es procesar un archivo de Excel (puede ser CSV, pero a modo de ejemplo vamos a hacerlo con Excel) y por cada línea del pago, vamos a crear un pago en Odoo. Basicamente, dicho archivo debe poseer las siguientes columnas: tipo de pago, cliente, monto, fecha, memo, diario.


No todas las columnas son necesarias, pero el propósito de este post es dar un ejemplo de como crear pagos a partir de un archivo. No de la estructura de los pagos. El código para procesar este archivo de Excel es como se va continuación

#!/usr/bin/python3
# import xmlrpc and openpyxl modules
from xmlrpc import client
import openpyxl
from datetime import datetime
url = 'http://localhost:8069'
common = client.ServerProxy('{}/xmlrpc/2/common'.format(url))
res = common.version()
dbname = 'mrputilsv1'
user = 'admin'
pwd = 'admin'
uid = common.authenticate(dbname, user, pwd, {})
# prints Odoo version and UID to make sure we are connected
print(res)
print(uid)
models = client.ServerProxy('{}/xmlrpc/2/object'.format(url))
# Define la variable para leer el workbook
workbook = openpyxl.load_workbook("demo_payments.xlsx")
# Define variable para la planilla activa
worksheet = workbook.active
# Itera las filas para leer los contenidos de cada celda
rows = worksheet.rows
for x,row in enumerate(rows):
    # Saltea la primer fila porque tiene el nombre de las columnas
    if x == 0:
        continue
    # Lee cada una de las celdas en la fila
    vals = {}
    for i,cell in enumerate(row):
        # saltea registros con valores many2one vacios
        if cell.value == None:
            continue
        print(i,cell.value)
        ref = ''
        if i == 0:
            col = 'payment_type' 
        if i == 1:
            col = 'partner_id' 
        if i == 2:
            col = 'amount' 
        if i == 3:
            col = 'date' 
        if i == 4:
            col = 'ref'
            ref = cell.value
        if i == 5:
            col = 'journal_id'
        if i not in [1,5]:
            vals[col] = cell.value
            # convierte las celdas de tipo date a string
            if type(vals[col]) == datetime:
                vals[col] = str(vals[col])
        else:
            if i == 1:
                many2one_model = 'res.partner'
            else:
                many2one_model = 'account.journal'
        
            res_id = models.execute_kw(dbname,uid,pwd,many2one_model,'search',[[['name','=',cell.value]]])
            # Si no encontramos el registro, pasamos al siguiente
            if not res_id:
                continue
            vals[col] = res_id[0]
    # saltea lineas en blanco
    if vals.get('ref') == None:
        continue
    payment_id = models.execute_kw(dbname,uid,pwd,'account.payment','search',[[['ref','=',vals.get('ref')]]])
    if not payment_id:
        payment_id = models.execute_kw(dbname,uid,pwd,'account.payment','create',[vals])
        return_id = payment_id
    else:
        return_id = models.execute_kw(dbname,uid,pwd,'account.payment','write',[payment_id,vals])
    print(return_id)
    try:
        post_id = models.execute_kw(dbname,uid,pwd,'account.payment','action_post',[payment_id])
        print(post_id)
    except:
        pass

Como podrá verse, se realizan transformaciones propias de la lógica de procesamiento. Como por ejemplo saltear líneas que vienen vacias (suele suceder). O transformar celdas con fechas a formato datetime (se chequea que el tipo de dato de la celda sea datetime para aplicarle la función str a dicho valor

if type(vals[col]) == datetime:
                vals[col] = str(vals[col])

Por último, tambien se buscan los nombres de clientes y diarios, para obtener los IDs de dichas columnas.


Como se maneja la confirmación los pagos

Se pueden confirmar los pagos, solo se necesita agregar la línea

post_id = models.execute_kw(dbname,uid,pwd,'account.payment','account_post',[payment_id])

a cada uno de los pagos que estan siendo publicados. El problema es que al ejecutarse, despues de postearse el pago se ve el siguiente mensaje de error:

xmlrpc.client.Fault: <Fault 1: 'Traceback (most recent call last):\n  File "/opt/odoo15/odoo/addons/base/controllers/rpc.py", line 95, in xmlrpc_2\n    response = self._xmlrpc(service)\n  File "/opt/odoo15/odoo/addons/base/controllers/rpc.py", line 75, in _xmlrpc\n    return dumps((result,), methodresponse=1, allow_none=False)\n  File "/usr/lib/python3.7/xmlrpc/client.py", line 971, in dumps\n    data = m.dumps(params)\n  File "/usr/lib/python3.7/xmlrpc/client.py", line 502, in dumps\n    dump(v, write)\n  File "/usr/lib/python3.7/xmlrpc/client.py", line 524, in __dump\n    f(self, value, write)\n  File "/usr/lib/python3.7/xmlrpc/client.py", line 528, in dump_nil\n    raise TypeError("cannot marshal None unless allow_none is enabled")\nTypeError: cannot marshal None unless allow_none is enabled\n'>


En el cual se indica que al ejecutar action_post, Odoo devuelve el valor None (lo cual hace ruido con las llamadas de RPC, por eso el mensaje de error). Hay dos maneras de manejar esta situación, una es modificando el core de Odoo y agregar un valor al retorno de la función action_post como se ve a continuación:

def action_post(self):
    ''' draft -> posted '''
    self.move_id._post(soft=False)
    self.filtered(
        lambda pay: pay.is_internal_transfer and not pay.paired_internal_transfer_payment_id
            )._create_paired_internal_transfer_payment()
    return True

Ahora, no es aconsejable modificar el core. Si se puede crear un módulo que extienda dicho método. Pero en nuestro caso preferimos manejar el error con un try... except

try:
    post_id = models.execute_kw(dbname,uid,pwd,'account.payment','action_post',[payment_id])
    print(post_id)
except:
    pass

Esto no daña a nadie (el error surge una vez que se postea la transacción, ademas evita el mensaje de error que surge cuando uno intenta validar un pago validado anteriormente lo que sucede al actualizarse un pago).

El código junto con el archivo de ejemplo se encuentran en nuestro github, en el repositorio tutorial_xmlrpc

https://github.com/ctmil/tutorial_xmlrpc/blob/master/import_payments.py



Acerca de:

Gustavo Orrillo

Apasionado de la programación, implementa Odoo para distintos tipos de negocios desde el año 2010. En Moldeo Interactive es Socio fundador y Programador; además de escribir en el Blog sobre distintos temas relacionados a los desarrollos que realiza.