Enable Dark Mode!
By: Renoj R

How to Add Views in Odoo 15 Using Python Code (Element Tree)

Technical Odoo 15

Odoo provides all fields that are required for specific functionalities. But in certain specific scenarios, we need to add fields and customize them according to our needs. From an end-user perspective, it is difficult to create such fields through code. In these types of situations, we can use dynamic field creation in Odoo.

In this blog, we are discussing  dynamic field creation in Odoo using Element Tree (E-Tree)

Here I am going to make a mass edit module to show as an example,

First, we will create the action button to appear when we select particular records,

For that, we have to create the basic model and also add the functionality via JS for us to pass the id of a particular field in case, it is a many2one field.

odoo.define('mass_editor.basic_model', function (require) {
"use strict";
var BasicModel = require('web.BasicModel');
   _parseServerData: function (fieldNames, element, data)
       var self = this;
       _.each(fieldNames, function (fieldName) {
           var field = element.fields[fieldName];
           var val = data[fieldName];
           if (field.type === 'many2one') {
               if (val) {
                   var r = self._makeDataPoint({
                       modelName: field.relation,
                       fields: {
                           display_name: {type: 'char'},
                           id: {type: 'integer'},
                       data: {
                           display_name: val[1],
                           id: val[0],
                       parentID: element.id,
                   data[fieldName] = r.id;
               else {
                   data[fieldName] = false;
           else {
               data[fieldName] = self._parseServerValue(field, val);

So hence we have set the basic model down, we can now move on to adding the mass edit button when selecting the records from the tree view

odoo.define('mass_editor.mass_edit_button', function (require) {
   "use strict";
   var core = require('web.core');
   var ListView = require('web.ListView');
   var ListController = require("web.ListController");
   var records = []
   var includeEdit = {
       _onSelectionChanged: function (event) {
           this._super.apply(this, arguments);
           if (this.$buttons) {
               records = this.getSelectedIds();
               if (records.length > 0) {
       renderButtons: function() {
           this._super.apply(this, arguments);
           if (this.$buttons) {
               var mass_edit_btn = this.$buttons.find('button.o_edit_btn');
       _mass_edit_option: function() {
           var action = {
               type: "ir.actions.act_window",
               name: "Mass Editor",
               res_model: "mass.editor.wizard",
               views: [[false,'form']],
               target: 'new',
               view_type : 'form',
               view_mode : 'form',
               context : {'active_model': this.modelName,
                          'selected_records': records},
               flags: {'form': {'action_buttons': true, 'options': {'mode': 'edit'}}}
           return this.do_action(action);

So here is the JS required to add an action button for those required fields.

This JS also allows us to pass the id of the active model and the selected record id thereby getting the model name and record ids when we select the records through context and pass it through to the python file.

Lets us also add the mass edit option using qweb.

<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
   <t t-extend="ListView.buttons">
       <t t-jquery="div.o_list_buttons" t-operation="append">
           <button type="object" class="btn btn-secondary  o_edit_btn">
               Mass Edit

Now let us create a wizard.

from odoo import models, fields
from odoo.exceptions import ValidationError
class MassEditorWizard(models.TransientModel):
   _name = 'mass.editor.wizard'
   _description = 'Mass Editor Wizard'
   def model_set(self):
       """This is the function that sets
          the current model"""
   model_id = fields.Many2one('ir.model', string='Model')
   field_ids = fields.Many2many('ir.model.fields',
                                domain="[('model_id', '=', model_id),"
                                       "('name', '!=', 'id')]")
   def btn_edit(self):
       """Function that is called when button
          is clicked. Passes the context
          model, selected fields and selected records"""
       if not self.field_ids:
           raise ValidationError("Choose the fields to edit !!!!")
       context = {
           'model': self.env.context['active_model'],
           'selected_records': self.env.context['selected_records'],
           'selected_fields': self.field_ids.ids
       res = {
           'name': "Edit Window",
           'type': 'ir.actions.act_window',
           'res_model': 'edit.window.wizard',
           'view_type': 'form',
           'view_mode': 'form',
           'target': 'new',
           'context': {'model': self.model_id.name}
       return res

Here the wizard contains,

A many2one field model_id which is automatically filled in by context through js, then we have the many2many field field_ids which can be used to select the fields of the particular model in model_id.

It then redirects to a secondary wizard from where in we edit the fields we have selected,

Here is the xml view for the primary (first) wizard view,

   <record id="mass_editor_wizard_form" model="ir.ui.view">
       <field name="name">mass.editor.wizard.form</field>
       <field name="model">mass.editor.wizard</field>
       <field name="arch" type="xml">
                   <field name="model_id"/>
                   <field name="field_ids"
                           <field name="field_description"/>
                           <field name="ttype"/>
                   <button name="btn_edit"
                   <button special="cancel" string="Cancel"/>
<record model='ir.actions.act_window' id='action_employee_dynamic_fields'>
   <field name="name">Edit Custom Fields</field>
   <field name="res_model">mass.editor.wizard</field>
   <field name="view_mode">form</field>
   <field name="view_id" ref="mass_editor_wizard_form"/>
   <field name="target">new</field>

Here we have the python file for the edit field (secondary) wizard.

from lxml import etree
from odoo import models, api
class EditWindowWizard(models.TransientModel):
   _name = 'edit.window.wizard'
   _description = 'Edit Window'
   def fields_view_get(
           self, cr, uid, view_id=None, view_type='form', toolbar=False):
       """This function sets the dynamic
          view of the wizard from where
          we can edit the fields"""
       model = self.env[self.env.context.get('model')]
       all_fields = model.fields_get()
       selected_fields = self.env['ir.model.fields'].browse(
       form = etree.Element('form')
       div = etree.SubElement(form, 'div')
       etree.SubElement(div, 'label', {
           'string': '''Actions performed here will
                        affect all the selected records !!!''',
           'style': 'color:red;font-size:18px',
       etree.SubElement(div, 'div')
       model_fields = {}
       for field in selected_fields:
           div_2 = etree.SubElement(form, 'div')
           group = etree.SubElement(div_2, 'group')
           if field.ttype == "many2one":
               etree.SubElement(group, 'field', {
                   'name': field.name,
               model_fields[field.name] = {
                   'type': field.ttype,
                   'string': field.field_description,
                   'relation': field.relation,
           elif field.ttype == "many2many":
               etree.SubElement(group, 'field', {
                   'name': field.name,
               model_fields[field.name] = all_fields[field.name]
           elif field.ttype == "one2many":
               etree.SubElement(group, 'field', {
                   'name': field.name,
               model_fields[field.name] ={
                   'type': field.ttype,
                   'string': field.field_description,
                   'relation': field.relation,
           elif field.ttype == 'selection':
               etree.SubElement(group, 'field', {
                   'name': field.name,
               model_fields[field.name] = {
                   'type': field.ttype,
                   'string': field.field_description,
                   'selection': all_fields[field.name]['selection'],
               etree.SubElement(group, 'field', {
                   'name': field.name,
               model_fields[field.name] = {
                   'type': field.ttype,
                   'string': field.field_description,
       footer = etree.SubElement(div, 'footer')
       etree.SubElement(footer, 'button', {
           'name': 'action_apply_changes',
           'string': 'Apply Changes',
           'class': 'btn-primary',
           'type': 'object',
       etree.SubElement(footer, 'button', {
           'string': 'Close',
           'class': 'btn-default',
           'special': 'cancel',
       res = super(EditWindowWizard, self).fields_view_get()
       res['fields'] = model_fields
       root_tree = form.getroottree()
       res['arch'] = etree.tostring(root_tree)
       return res
   def create(self, vals):
       """Here, we create the records
          with updated vals"""
       model = self.env[self.env.context.get('model')]
       selected_records = model.browse(
       for rec in selected_records:
       return super(EditWindowWizard, self).create({})
   def read(self, fields=None, load='_classic_read'):
       """We update the fields that are in
          self._fields to a dictionary
          with false value and return it"""
       x_fields = {}
       for field in fields:
           if field in self._fields:
               x_fields.update({field: False})
       return super(EditWindowWizard, self).read(x_fields, load=load)
   def action_apply_changes(self):
       """return the button action"""
       return {
           'type': 'ir.actions.client',
           'tag': 'reload'

Here we can see that we can give a warning message in etree statically using python code. First, we assign the etree element to a form and then assign a div variable as a subelement. We can then assign a label to the div element.

Now for dynamic views, we can see that we can check the type of field we have selected in the previous wizard and adjust the view in our secondary fields accordingly. We can change the secondary wizard view according to the type of view we give in the previous wizard. We can assign different view types by assigning string, type, and relation to the model_fields dictionary. For example, as we can see, if it is a many2one field we chose then in the secondary wizard, accordingly we can see that we will get a many2one field to pass the id through. On the other hand, if it is an one2many field, we will get all the fields associated with it for us to edit. The read and create functions are used to update the selected fields. Then finally, we can reload the window on the button using the action_apply_changes function using reload tags.

Finally, don’t forget to add the security access rights for both wizards in ir.access.csv.Thus using Element Tree helps us add dynamic views in special cases where adding it through views is not possible.

If you need any assistance in odoo, we are online, please chat with us.


Leave a comment



Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635



Cybrosys Limited
Alpha House,
100 Borough High Street, London,
SE1 1LB, United Kingdom



Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.



Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message