Cómo usar una versión de Odoo para vender y otra para facturar
Sincronización entre dos sistemas Odoo
Durante el desarrollo de un proyecto vigente nos surgió una problemática a solucionar. Un entorno montado en un Odoo 11, con un sistema de venta personalizado muy complejo (una serie de módulos personalizados, con un archivo base de Python de más de 1000 líneas de códigos y unos 15 reportes, sin contar las vistas y configuraciones) debía cambiar su sistema de facturación para poder armar una contabilidad más dedicada. El punto es que este Odoo 11 tenía instalado una localización parcial, muy distinta a las ventajas que ofrece Odoo 13 y la nueva Localización Argentina. ¿Solución? Seguramente para más de una sea la ardua tarea de migrar, pero hablamos de una tarea titánica, con mucho riesgo y sin el sistema funcionando durante el tiempo que demore dicha migración. En este caso no se pueden permitir un tiempo tan extenso de desarrollo (más los desvíos) y pidieron una solución más rápida a fin de encarar la migración con mayor tranquilidad. Entonces, ¿solución? Vender con un Odoo, facturar con el otro (aunque la facturación es solo para llevar a cabo la contabilidad interna, podría usarse con Factura Electrónica).
Así como el Sega CD fue una especie de "segunda vida" para la Sega Génesis, este tipo de conexiones es una manera de darle un par de años más de vida a una implementación vieja de Odoo sin necesidad de migrar.
Conectar dos Odoo
Lo primero es lo primero: comunicar dos versiones de Odoo. Para esto hay varias opciones, pero teniendo en cuenta que la forma nativa de comunicación de Odoo es XMLRPC, yo no me lo pensaría mucho. Dentro del modelo en cuestión (en este caso es account.invoice de Odoo 11 porque pretendemos ir al account.move de Odoo 13) vamos a armar la conexión a Odoo 13:
import xmlrpclib
username = 'admin'
pwd = 'demo_ar'
db = 'demo_ar'
url = 'http://odoo13:8069'
common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url))
uid = common.authenticate(db, username, pwd, {})
models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url))
El import, claramente, irá por fuera de la clase. El resto del código debe ir dentro de una función de tipo api.multi (en Odoo 11 todavía existía el api.multi). Con esta conexión lo que vamos a generar es conseguir la ID del usuario logueado (es la variable uid, que idealmente es el usuario admin de Odoo 13). Y para hacer el login simplemente necesitamos la URL donde se encuentra alojado el Odoo (con todo y puerto si fuese necesario), el usuario (username), la contraseña (el pwd) y la base de datos sobre la cual se hará la conexión (la db). Finalmente, sobre la variable models se guarda la conexión los objetos de XMLRPC, lo usaremos más adelante.
Enviar una Factura de Odoo 11 a Odoo 13
Debo hacer un punto a parte sobre como se pasaron las facturas en esta implementación en particular, ya que no crearon las facturas en sí sino que se crearon los asientos contables. Como sabrán, a partir de Odoo 13 una factura y un asiento contable es lo mismo, perteneciendo al mismo modelo. Por tal motivo, lo ideal para la sincronización es realizar la factura en Odoo 11 (o la orden de ventas) y enviar esos datos a Odoo 13 para llevar la contabilidad. Por tal motivo, nos vamos a trabajar con líneas de productos, sino con apuntes contables. ¿Es posible hacerlo con líneas de productos para hacer una factura en su totalidad en Odoo 13 con datos de Odoo 11? Sí, pero lleva más trabajo y no es el tema de este post.
El primer paso es básico: obtener el cliente (partner). Y para esto hay un sinfin de opciones. Una muy habitual es, en caso de utilizar un porcentaje importante de los mismos clientes todo el tiempo, tener un campo de ID que conecte el cliente de Odoo 13 con el de Odoo 11. De esa manera podremos realizar un search. O, mejor todavía, realizar un search sobre el CUIT del cliente (despues de todo, si estamos queriendo realizar una factura es lógico que el cliente debe tener CUIT):
partner_id = models.execute_kw(db,uid,pwd,'res.partner','search',[[['vat', '=', self.partner_id.cuit]]], {'limit': 1})
Debo hacer una aclaración respecto a esto, la búsqueda la hago sobre el vat ya que en Odoo 13 suele ser el campo por defecto donde están todos los números de identificación tributaria de la mayoría de las localizaciones del mundo. Pero puede darse el caso de tener un campo distinto, por la razón que sea. Del mismo modo, siempre que en el código se vea "self" es porque hace referencia al modelo de Odoo 11, ya sea sale.order, account.invoice o uno personalizado. Por lo tanto, la ubicación exacta del campo puede variar en cada código. Por favor usar estos códigos como una guía para construir sus script, no los copien y peguen porque lo más probable es que no les funcione. Finalmente, coloco un limit: 1 para que solo traiga un registro (idealmente Odoo 13 no debería permitir registrar un cliente con el mismo número de identificación tributaria, pero no es tan estricto y puede pasar). En caso de no encontrar al cliente, tenemos dos opciones: o bien tiramos una advertencia con un ValidationError para frenar el script, o bien creamos al cliente. Dejo el código que utilicé para dar de alta un cliente en caso de no encontrarlo, insisto en que debe ser adaptado a cada circunstancia:
if not partner_id:
afip = sock.execute_kw(dbname, uid, pwd, 'l10n_ar.afip.responsibility.type', 'search', [[['name', '=', self.partner_id.condicion]]], {'limit': 1})
if afip:
partner_id = sock.execute(dbname, uid, pwd, 'res.partner', 'create', {'name': self.partner_id.name,'vat': vat,'l10n_ar_afip_responsibility_type_id': afip[0],'l10n_latam_identification_type_id': type[0]})
else:
raise ValidationError('No se encontró la responsabilidad fiscal. Contacte con su administrador.')
Como pueden ver, es un código muy específico para una implementación muy específica. La responsabilidad fiscal en el Odoo 11 fue puesta con un campo tipo Select (mal hecho) pero en las localización de Odoo 13 (y en la de Odoo 11 también, solo que no estaba bien armada) es un campo relacional Many2one. Así que la solución es buscarlo (mediante la variable afip) viendo si los nombres coinciden. No es la mejor forma, pero por suerte los nombres se respetan en su mayoría y sirven como base. Luego, si encuentra la responsabilidad, puede crear al cliente en Odoo 13 con los datos de Odoo 11. Si no lo cuentra, dispara un ValidationError para advertir un problema.
Para crear el asiento contable vamos a empezar por crear la cabecera:
vals = {
'date': self.date,
'partner_id': partner_id,
'journal_id': 28,
'company_id': 1,
'ref': self.origin,
'type': 'entry'
}
account_move_id = models.execute_kw(db, uid, pwd, 'account.move', 'create', [vals], {'context' :{'check_move_validity': False}})
Hay un par de detalles importantes, vamos línea por línea. Creamos una variable llamada vals para poner los valores (esto es una cuestión opcional, de orden). Los valor que vamos a enviar son obligatorios o útiles por alguna razón. El campo date será la fecha del asiento, y en este caso la sacamos el propio modelo de Odoo 11. El campo partner_id es el cliente que acabamos de obtener / crear; el campo journal_id será el ID del diario en Odoo 13. Acá hago una observación, podemos tener este campo casi como una constante (como en este caso) si es que siempre vamos a usar el mismo diario; caso contrario hay que hacer un search usando el nombre del diario. El campo company_id será el ID de la compañía (si trabajamos solo con una, casi con seguridad será 1). En ref decidí poner una referencia que relacione el asiento de Odoo 13 con "lo que sea" de Odoo 11, por eso le estoy mandando el origin. Y, finalmente, en type debemos mandar que sea tipo entry, ya que no haremos una factura de venta o de compra; sino un asiento.
Por otra parte, la creación del mismo por medio del create no tiene mayor misterio, salvo el context (es fundamental mandar ese valor en context sino vamos a tener problemas. Y ahora sí, finalmente, agregamos los movimientos:
vals_debit = {
'account_id': 307,
'partner_id': partner_id,
'name': 'DEBITO ARS',
'debit': abs(self.amount),
'credit': 0,
'move_id': account_move_id,
'company_id': 1
}
debit_id = models.execute_kw(db, uid, pwd, 'account.move.line', 'create', [vals_debit], {'context' :{'check_move_validity': False}})
vals_credit = {
'account_id': 382,
'partner_id': partner_id,
'name': 'CREDITO ARS',
'credit': abs(self.amount),
'debit': 0,
'move_id': account_move_id,
'company_id': 1
}
credit_id = models.execute_kw(db, uid, pwd, 'account.move.line', 'create', [vals_credit], {'context' :{'check_move_validity': False}})
Con esto ya estaríamos. Hago mención solamente en el account_id, ya que es el ID de la cuenta contable. Lo mismo que con el diario, si no lo vamos a dejar codeado vamos a tener que ahcer un search (usando el código de la cuenta es una buena opción). Lo demás no tiene mayor misterio, se pone el monto a facturar en debit y en credit así se generan ambos asientos. Es un ejemplo básico, ya que puede ser que tengamos el monto completo en una línea de débito y ese mismo monto dividido en 3 o más líneas de crédito. Al fin y al cabo, es lo mismo, siempre y cuando el balance quede en 0. Para validar el asiento hacemos lo siguiente:
try:
return_id = models.execute_kw(db, uid, pwd, 'account.move','action_post', [account_move_id], {})
except:
raise ValidationError('Algo salió mal al validar. Es probable que el asiento se encuentre creado pero por alguna razón no fue validado.')
Y con esto ya estaríamos, es la base, no es un sistema tan inteligente como podría ser, pero para conectar dos Odoo servirá.
Acerca de:
Ignacio Buioli
Degree on Multimedia Arts. He has developed numerous Multimedia projects as well as written articles and translated texts of the mentioned subject. In Moldeo Interactive, He is a Partner and Programmer; also taking care of a large part of the online networks and courses.