Importar pagos con archivos de Excel

20 de julio de 2023 por
Gustavo Orrillo
| Sin comentarios aún
 

Uno de los motivos por los cuales se implementa Odoo es la automatización de la contabilidad. Y por eso entendemos muchas veces la automatización de la generación de facturas, y la captura automática de pagos. Esto último, sobre todo las empresas de servicios, se deben importar en forma diaria. Porque la realidad así lo dicta. Y estamos hablando de archivos, no de web-services ya que todos los medios de pago como mínimo ofrecen archivos con los pagos. 

Estos archivos en Argentina pueden ser archivos con los pagos de MercadoPago, de PagoFacil, RapiPago, Siro, la lista es larga... Lo cierto es que dicha situación es un caso de uso bastante frecuente. Y por lo general Odoo provee tres herramientas para hacerlo: la herramienta de importación de archivos de Odoo (que tiene sus límites, como por ejemplo distinguir líneas de header de líneas de detalle, no es viable hacerlo en un ambiente de producción), módulos custom de Odoo, o scripts de xmlrpc (que es lo que vamos a ilustrar en este post).

Cada pago en Odoo es un registro en el modelo account.payment. Entonces supongamos que tenemos que hacer es procesar un archivo de Excel con pagos (puede ser CSV, pero a modo de ejemplo vamos a hacerlo con Excel) y por cada línea del archivo, vamos a crear un pago en Odoo (en el modelo account.payment). Supongamos que el archivo debe poseer las siguientes columnas: tipo de pago, cliente, monto, fecha, memo, diario (los datos mínimos, quiza el diario uno lo pueda inducir en la lógica del script).


Todas estas columnas no 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 del archivo de los pagos (ya que los mismos siempre cambian, por ejemplo tienen header y footer). 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 se puede ver, se hacen transformaciones propias de la lógica de procesamiento de un archivo. Como por ejemplo saltear líneas que vienen vacias (suele suceder) o transformar celdas con fechas a formato datetime. Para ello hacemos esto (se chequea si la celda tiene un formato datetime entonces la transformamos a string):

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

Tambien se buscan los nombres de clientes y diarios, para obtener los IDs de dichas columnas.

Que hacer con 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])


para cada uno de los pagos que necesita ser confirmado (o publicado). 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'>

Este error indica que al ejecutar action_post, Odoo devuelve el valor None (lo cual origina el mensaje de error en las llamadas de xmlrpc). Odoo publica el pago, pero da mensaje de error, lo cual es un poco incómodo.

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. Este método tendría la siguiente forma:

def action_post(self):
​res = super(AccountPayment, self).action_post()
​if not res:
​return True
​else:
​return res

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/a2systems/tutorial_xmlrpc/blob/master/import_payments.py


Gustavo Orrillo 20 de julio de 2023
Compartir
Archivar
Identificarse dejar un comentario