Importando miles de registros en Odoo - usando la funcion load

Gustavo Orrillo
- 31/01/2022 - 7 min. de lectura

          Odoo no es precisamente rápido. Esta bien para operaciones transaccionales de una PyME pero convengamos que su ORM no es precisamente el más rápido. Y cuando llega el momento de cargar miles de registros puede ser desesperantemente lento. Es por eso que en su momento hablamos del tema, en el posteo "Importando miles de registros en Odoo - usando csv_import". 

          Extendiendo ese posteo, vamos a hablar que sucede behind the scenes. Odoo posee un mecanismo para importar y actualizar miles de registros en forma rápida. Por ejemplo, acabo de probar la creación de 1,000 contactos (res.partner) y la inserción solo tardó 13 segundos. Rapidísimo. Con uso del ORM tambien. Para ello se usa el método load. Si se fijan en la definición del método:

              @api.model
              def load(self, fields, data):
                  """
                  Attempts to load the data matrix, and returns a list of ids (or
                  ``False`` if there was an error and no id could be generated) and a
                  list of messages.
                  The ids are those of the records created and saved (in database), in
                  the same order they were extracted from the file. They can be passed
                  directly to :meth:`~read`
                  :param fields: list of fields to import, at the same index as the corresponding data
                  :type fields: list(str)
                  :param data: row-major matrix of data to import
                  :type data: list(list(str))
                  :returns: {ids: list(int)|False, messages: [Message][, lastrow: int]}
                  """
          Como se lo llama? es sencillo. Supongamos que queremos crear productos

          fields = ['name','default_code','responsible_id','categ_id']
          data = [['Product 1', 'product-1','Admin','All'],['Product 2', 'product-2','Admin','All']
          create_ids = self.env['product.template'].load(fields,data)

          Como veran, solo hay que invocar el método con los campos y una lista con los datos formateados para su actualización. Para los campos del tipo many2one tambien se les puede pasar el external ID (mucho mejor para la actualización). Y se lo hace de la siguiente manera:

          fields = ['name','default_code','responsible_id','categ_id/id']
          data = [['Product 1', 'product-1','Admin','All'],['Product 2', 'product-2','Admin','product.product_category_consumable']
          create_ids = self.env['product.template'].load(fields,data)
          Como ven... solo hay que modificar la definición del campo y los datos mismos. Pero es algo sencillo. Solo hay que descular como hacer que cada uno de sus datos en los campos many2one tenga un external ID. Tambien la función tiene la posibilidad de actualizar los datos, lo cual es un gran plus.  

          Ejemplo de carga de contactos


          Vamos a ver un ejemplo en el que cargamos 1,000 contactos de un archivo Excel. Dicho archivo se encuentra en el directorio tmp y tiene la siguiente estructura:



          Como se puede ver tenemos varios campos: id (con el external id), el nombre, la referencia, la ciudad y el pais (campo Many2one, en ese caso se debe proveer el external ID del registro relacionado). El código para insertar/actualizar estos registros es el que se ve a continuación:


          fields = ['id','name','ref','city','country_id/id']
          data_lines = []
          workbook = openpyxl.load_workbook("/tmp/partners.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     data = []     for i,cell in enumerate(row):         # saltea registros con valores vacios         if cell.value == None:
                      continue
                  data.append(cell.value)
              data_lines.append(data)
          res = self.env['res.partner'].load(fields,data_lines)
          Este método es muy rápido para el insert como para el update. Por ejemplo, para insertar 1,000 contactos con un campo many2one (el país) se tardó 28 segundos. Para actualizar el país de dichos contactos, se tardó solo 32 segundos. Rapidísimo.

          El método load devuelve un diccionario con los siguientes elementos: ids (con los IDs de los registros insertados/actualizados, messages con los mensajes de error, y nextrow. Supongamos que queremos actualizar el campo country_id a un valor inexistente en la base de datos. En ese caso por cada registro que tiene un error, se obtendrá un mensaje como el siguiente:

          {'rows': {'from': 1, 'to': 1}, 'type': 'error', 'record': 1, 'field': 'country_id', 
          'message': "No matching record found for external id 'base.ut' in field 'Country'",
          'moreinfo': {'name': 'Possible Values', 'type': 'ir.actions.act_window', 'target': 'new', 'view_mode': 'tree,form',
          'views': [(False, 'list'), (False, 'form')], 'context': {'create': False},
          'help': 'See all possible values', 'res_model': 'ir.model.data', 'domain': [('model', '=', 'res.country')]}, 'field_name': 'Country'} 
          Como se puede ver, se lista por registro (clave rows) que error hay. Lo mismo el campo y el mensaje de error. Eso va a servir para listar a los usuarios los errores obtenidos durante el proceso. Como se puede ver, el proceso de los errores tiene su propia lógica que debe ser desarrollada. Pero es posible hacerlo. Por lo general uno va procesando los errores a medida que se va procesando los registros. Con el método load es diferente, se procesan todos los registros y luego se listan los errores obtenidos.


          Actualizando campos many2many


          El método load tambien contempla actualizar campos many2many. Por lo pronto, supongamos que queremos actualizar un contact (a2_query_sales.REF0001) y asignarle la categoría Employees (base.res_partner_category_3). En ese caso el valor de fields y el formato de los datos es el siguiente:

          fields = ['id','category_id/id']
          data = ['a2_query_sales.REF0001','base.res_partner_category_3']
          res = self.env['res.partner'].load(fields,[data])

          Utilizando el contexto

          Utilizar el contexto es fundamental. Por ejemplo, si queremos actualizar las facturas (modelo account.move) necesitamos siempre utilizar el contexto check_move_validity seteado en falso. Supongamos, si queremos actualizar el modelo account.move.line, deberíamos invocar el método load de la siguiente forma:

          res = self.env['account.move.line'].with_context({'check_move_validity': False}).load(fields,[data])

          Otro valor de contexto interesante es tracking_disable. Cuando el mismo tiene un valor True, no se crean los mensajes de seguimiento de cambios en los registros. Esto puede llegar a acortar los tiempos del proceso de carga de datos en un 15%.

          Actualizando la metadata

          El método load es un método del ORM, por ende al momento de actualizar hará los chequeos de seguridad pertinentes, chequeará los constraints definidos y seteará los valores default definidos en los modelos. No invocará las funciones onchange debido a que las mismas son invocadas solo por el cliente web de Odoo. 

          Ahora, al ser un método del ORM, automaticamente actualiza la metadata de Odoo (campos create_date, create_uid, write_date y write_uid). Y cuando migramos la información en Odoo debemos muchas veces actualizarla con la información original. Es por ello que podemos utilizar el muy util módulo import_metadata de Thibault Francois. Dicho modulo permite actualizar la metadata durante la creación y escritura de registros. Sería util que se instale previo a la migración de datos.

          Actualizando binary fields

          Muchas veces se necesitan actualizar binary fields. Por ejemplo en el caso de attachments. Para ello el método load es muy rápido; probé crear 1,000 attachments para 1,000 ordenes de compra y lo hizo en menos de 15 segundos. Lo que se debe hacer es actualizar el campo de tipo binary con el contenido del archivo convertido a base64. Por ejemplo; para cargar los attachments a las ordenes que tenía creada:

          @api.model
          def load_attachments(self):
              fields = ['id','res_model','res_id','type','mimetype','datas','name','res_name']
              orders = self.env['purchase.order'].search([])
              data_lines = []
              test_file = open('/tmp/stephen_king.png','rb')
              data_file = base64.b64encode(test_file.read())
              for order in orders:
                  data = [
                      'a2_query_sales.attachment_purchase_order_' + str(order.id),
                      'purchase.order',
                      order.id,
                      'binary',
                      'application/octet-stream',
                      data_file,
                      order.name,
                      order.name,
                  ]
                  data_lines.append(data)
                  res = self.env['ir.attachment'].with_context({'tracking_disable': True}).load(fields,data_lines) 

          Lo que da el siguiente resultado:


          Conclusiones


          El método load es el método más rápido para insertar y actualizar datos en Odoo. No solo inserta, sino tambien actualiza. Es más rápido que hacer inserts y updates usando el ORM. Si bien no hice benchmarks con el ORM ni con SQL, a simple vista es más rápido que el ORM. Personalmente creo que es un método poco documentado, pero que cada desarrollador de Odoo necesita conocer. Ya que resuelve dos grandes limitaciones en las migraciones. La velocidad de procesamiento y la actualización de los registros (esto no es menor en una migración, tener la posiblidad de actualizar registros durante la migración le brinda mucha flexibilidad al proceso de migración).

          Solo algunos tiempos despues de probarlo: cargar y actualizar 1000 contactos, 32 segundos. Crear y actualizar 1000 ordenes de compra: un minuto. Insertar y actualizar 1000 líneas en las ordenes de compra: 30 segundos. Importar 1,000 attachments con una imagen de 400Kb? 15 segundos. Definitivamente el método load merece ser tenido en cuenta.

          El método load tiene su complejidad de uso derivado de la falta de documentación y porque los datos deben ser transformados para su correcto uso (por ejemplo, todos los registros referenciados en los campos de tipo many2one deben tener un external ID). Pero no es dificil una vez que uno comprende como funciona. Se necesita pre-procesar los datos y transformarlos previo a su carga. Pero eso es necesario en todos los contextos. Como sabemos para todas las migraciones se deben hacer tres pasos (el famoso ETL): Extracción, Transformación y Loading. Por lo pronto el método load resuelve los problemas de performance en la última etapa. Lo cual no es menor.

          Los procesos de migración son complejos. Solo por motivos de marketing se los muestra como trabajos sencillos, pero para llevar a cabo una migración exitosa se necesita mucho trabajo y conocimiento. Es por ello que los procesos de migración deben ser encarados con múltiples herramientas y en múltiples pasos. Nunca como uno solo procesos monolítico en el que se apreta un botón y está todo resuelto. Suena como una buena idea, pero es impracticable en una instalación real. Una de las múltiples herramientas es el método load, el cual resuelve muchos problemas. Lamentablemente lo conozco muy tarde, me gustaría haberlo conocido años atrás.


          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.